fix(valkey): escape special chars in FT.SEARCH tag filter values (#5750)

This commit is contained in:
Hrushikesh Yadav
2026-06-25 15:20:13 +05:30
committed by GitHub
parent 6bb1d328ad
commit 3d06006f36
2 changed files with 44 additions and 54 deletions
+19 -34
View File
@@ -41,7 +41,20 @@ class OutputData(BaseModel):
payload: Dict
_VALKEY_TAG_SPECIAL = set(r',.<>{}[]"\':;!@#$%^&*()-+=~| ')
class ValkeyDB(VectorStoreBase):
@staticmethod
def _escape_tag_value(value):
"""Escape special characters in a Valkey FT.SEARCH tag filter value.
Without escaping, characters like * (wildcard) or | (OR) alter query
semantics and can bypass tenant-isolation filters.
"""
s = str(value)
return "".join(f"\\{c}" if c in _VALKEY_TAG_SPECIAL else c for c in s)
def __init__(
self,
valkey_url: str,
@@ -329,8 +342,8 @@ class ValkeyDB(VectorStoreBase):
knn_part (str): The KNN part of the query.
filters (dict, optional): Filters to apply to the search. Each key-value pair
becomes a tag filter (@key:{value}). None values are ignored.
Values are used as-is (no validation) - wildcards, lists, etc. are
passed through literally to Valkey search. Multiple filters are
Values are escaped via _escape_tag_value() before interpolation
to prevent wildcard/operator injection. Multiple filters are
combined with AND logic (space-separated).
Returns:
@@ -345,8 +358,8 @@ class ValkeyDB(VectorStoreBase):
filter_parts = []
for key, value in filters.items():
if value is not None:
# Use the correct filter syntax for Valkey
filter_parts.append(f"@{key}:{{{value}}}")
escaped = self._escape_tag_value(value)
filter_parts.append(f"@{key}:{{{escaped}}}")
# No valid filter parts
if not filter_parts:
@@ -750,34 +763,6 @@ class ValkeyDB(VectorStoreBase):
logger.exception(f"Error resetting index {self.collection_name}: {e}")
raise
def _build_list_query(self, filters=None):
"""
Build a query for listing vectors.
Args:
filters (dict, optional): Filters to apply to the list. Each key-value pair
becomes a tag filter (@key:{value}). None values are ignored.
Values are used as-is (no validation) - wildcards, lists, etc. are
passed through literally to Valkey search.
Returns:
str: The query string. Returns "*" if no valid filters provided.
"""
# Default query
q = "*"
# Add filters if provided
if filters and any(value is not None for key, value in filters.items()):
filter_conditions = []
for key, value in filters.items():
if value is not None:
filter_conditions.append(f"@{key}:{{{value}}}")
if filter_conditions:
q = " ".join(filter_conditions)
return q
def list(self, filters: dict = None, top_k: int = None) -> list:
"""
List all recent created memories from the vector store.
@@ -785,8 +770,8 @@ class ValkeyDB(VectorStoreBase):
Args:
filters (dict, optional): Filters to apply to the list. Each key-value pair
becomes a tag filter (@key:{value}). None values are ignored.
Values are used as-is without validation - wildcards, special characters,
lists, etc. are passed through literally to Valkey search.
Values are escaped via _escape_tag_value() before interpolation
to prevent wildcard/operator injection.
Multiple filters are combined with AND logic.
top_k (int, optional): Maximum number of results to return. Defaults to 1000
if not specified.