Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/basic_memory/repository/metadata_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,12 @@ def parse_metadata_filters(filters: dict[str, Any]) -> List[ParsedMetadataFilter
continue

if op in {"$gt", "$gte", "$lt", "$lte"}:
normalized = _normalize_scalar(value)
comparison = "numeric" if _is_numeric_value(normalized) else "text"
if _is_numeric_value(value):
normalized = float(value)
comparison = "numeric"
else:
normalized = _normalize_scalar(value)
comparison = "text"
parsed.append(
ParsedMetadataFilter(path_parts, op.lstrip("$"), normalized, comparison)
)
Expand All @@ -94,8 +98,12 @@ def parse_metadata_filters(filters: dict[str, Any]) -> List[ParsedMetadataFilter
if op == "$between":
if not isinstance(value, list) or len(value) != 2:
raise ValueError(f"$between requires [min, max] for '{raw_key}'")
normalized = [_normalize_scalar(v) for v in value]
comparison = "numeric" if _is_numeric_collection(normalized) else "text"
if _is_numeric_collection(value):
normalized = [float(v) for v in value]
comparison = "numeric"
else:
normalized = [_normalize_scalar(v) for v in value]
comparison = "text"
parsed.append(ParsedMetadataFilter(path_parts, "between", normalized, comparison))
continue

Expand Down
15 changes: 6 additions & 9 deletions src/basic_memory/repository/postgres_search_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,22 +332,19 @@ async def search(
like_param_single = f"{base_param}_{j}_like_single"
params[like_param_single] = f"%'{val}'%"
tag_conditions.append(
f"({json_expr} @> :{tag_param}::jsonb "
f"({json_expr} @> CAST(:{tag_param} AS jsonb) "
f"OR {text_expr} LIKE :{like_param} "
f"OR {text_expr} LIKE :{like_param_single})"
)
conditions.append(" AND ".join(tag_conditions))
continue

if filt.op in {"gt", "gte", "lt", "lte", "between"}:
if filt.comparison == "numeric":
numeric_expr = (
f"CASE WHEN ({text_expr}) ~ '^-?\\\\d+(\\\\.\\\\d+)?$' "
f"THEN ({text_expr})::double precision END"
)
compare_expr = numeric_expr
else:
compare_expr = text_expr
compare_expr = (
f"({metadata_expr} #>> '{path}')::double precision"
if filt.comparison == "numeric"
Comment on lines 342 to +345

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore numeric-guard to avoid cast failures

Casting #>> directly to double precision will raise invalid input syntax for type double precision if any row has non-numeric text at that metadata path (e.g., "N/A"), causing the entire search query to error out even though the filter value is numeric. The previous CASE+regex avoided this by returning NULL for non-numeric values; with the new unconditional cast, numeric comparisons now fail for mixed-type metadata.

Useful? React with 👍 / 👎.

else text_expr
)

if filt.op == "between":
min_param = f"meta_val_{idx}_min"
Expand Down
4 changes: 2 additions & 2 deletions tests/repository/test_metadata_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ def test_parse_in_operator():

def test_parse_comparison_numeric():
parsed = parse_metadata_filters({"schema.confidence": {"$gt": 0.7}})
assert parsed == [ParsedMetadataFilter(["schema", "confidence"], "gt", "0.7", "numeric")]
assert parsed == [ParsedMetadataFilter(["schema", "confidence"], "gt", 0.7, "numeric")]


def test_parse_between_numeric():
parsed = parse_metadata_filters({"score": {"$between": [0.3, 0.6]}})
assert parsed == [ParsedMetadataFilter(["score"], "between", ["0.3", "0.6"], "numeric")]
assert parsed == [ParsedMetadataFilter(["score"], "between", [0.3, 0.6], "numeric")]


def test_parse_between_text():
Expand Down
Loading