Skip to content
Open
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
38 changes: 36 additions & 2 deletions haystack/utils/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,32 @@ def _not(document: Document | ByteStream, conditions: list[dict[str, Any]]) -> b


def _equal(value: Any, filter_value: Any) -> bool:
value, filter_value = _prepare_equality_comparison(value=value, filter_value=filter_value)
return value == filter_value


def _not_equal(value: Any, filter_value: Any) -> bool:
return not _equal(value=value, filter_value=filter_value)


def _prepare_ordering_comparison(value: Any, filter_value: Any) -> tuple[Any, Any]:
"""Normalize both values for ordering comparisons, parsing strings as dates."""
def _looks_like_iso_date(value: Any) -> bool:
return (
isinstance(value, str)
and len(value) >= 10
and value[:4].isdigit()
and value[4] == "-"
and value[5:7].isdigit()
and value[7] == "-"
and value[8:10].isdigit()
and (len(value) == 10 or value[10] in {"T", "t", " "})
)


def _is_datetime_operand(value: Any) -> bool:
return isinstance(value, datetime) or _looks_like_iso_date(value)


def _normalize_datetime_comparison(value: Any, filter_value: Any) -> tuple[Any, Any]:
if isinstance(value, str) or isinstance(filter_value, str):
if not isinstance(value, datetime):
value = _parse_date(value)
Expand All @@ -58,6 +75,23 @@ def _prepare_ordering_comparison(value: Any, filter_value: Any) -> tuple[Any, An
if isinstance(value, datetime) and isinstance(filter_value, datetime):
value, filter_value = _ensure_both_dates_naive_or_aware(value, filter_value)

return value, filter_value


def _prepare_equality_comparison(value: Any, filter_value: Any) -> tuple[Any, Any]:
if not (_is_datetime_operand(value) and _is_datetime_operand(filter_value)):
return value, filter_value

try:
return _normalize_datetime_comparison(value=value, filter_value=filter_value)
except FilterError:
return value, filter_value


def _prepare_ordering_comparison(value: Any, filter_value: Any) -> tuple[Any, Any]:
"""Normalize both values for ordering comparisons, parsing strings as dates."""
value, filter_value = _normalize_datetime_comparison(value=value, filter_value=filter_value)

if isinstance(filter_value, list):
msg = f"Filter value can't be of type {type(filter_value)} using operators '>', '>=', '<', '<='"
raise FilterError(msg)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
fixes:
- |
Fixed the ``==``, ``!=``, ``in``, and ``not in`` metadata filter operators to
normalize ISO 8601 datetime values before comparing them. Equivalent values
such as ``"2025-02-03T12:45:46Z"`` and ``"2025-02-03T12:45:46+00:00"`` now
match consistently across ISO strings and ``datetime`` objects, aligning
equality-style filters with the ordering operators and preventing silent
misses in components that rely on ``document_matches_filter``.
16 changes: 16 additions & 0 deletions test/document_stores/test_in_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ def cosine_document_store(self):
yield store
store.shutdown()

def test_filter_documents_date_equality_with_equivalent_iso_formats(
self, document_store: InMemoryDocumentStore
) -> None:
docs = [Document(id="1", content="doc", meta={"date": "2025-02-03T12:45:46Z"})]
document_store.write_documents(docs)

equal_result = document_store.filter_documents(
filters={"field": "meta.date", "operator": "==", "value": "2025-02-03T12:45:46+00:00"}
)
in_result = document_store.filter_documents(
filters={"field": "meta.date", "operator": "in", "value": ["2025-02-03T12:45:46+00:00"]}
)

self.assert_documents_are_equal(equal_result, docs)
self.assert_documents_are_equal(in_result, docs)

def test_to_dict(self, in_memory_doc_store):
data = in_memory_doc_store.to_dict()
assert data == {
Expand Down
58 changes: 58 additions & 0 deletions test/utils/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,64 @@
True,
id="== operator with ISO 8601 datetime Document value",
),
pytest.param(
{"field": "meta.date", "operator": "==", "value": "2025-02-03T12:45:46+00:00"},
Document(meta={"date": "2025-02-03T12:45:46Z"}),
True,
id="== operator with equal instant in different UTC syntax",
),
pytest.param(
{"field": "meta.date", "operator": "==", "value": "1969-07-21"},
Document(meta={"date": "1969-07-21T00:00:00"}),
True,
id="== operator with equal datetime in different ISO 8601 format",
),
pytest.param(
{"field": "meta.date", "operator": "==", "value": datetime(2025, 2, 3, 12, 45, 46, tzinfo=timezone.utc)},
Document(meta={"date": "2025-02-03T12:45:46Z"}),
True,
id="== operator with datetime filter value and ISO 8601 string Document value",
),
pytest.param(
{"field": "meta.date", "operator": "==", "value": datetime(2025, 2, 3, 12, 45, 46, tzinfo=timezone.utc)},
Document(meta={"date": datetime(2025, 2, 3, 12, 45, 46)}),
True,
id="== operator with aware datetime filter value and naive datetime Document value",
),
pytest.param(
{"field": "meta.date", "operator": "!=", "value": "2025-02-03T12:45:46+00:00"},
Document(meta={"date": "2025-02-03T12:45:46Z"}),
False,
id="!= operator with equal instant in different UTC syntax",
),
pytest.param(
{"field": "meta.date", "operator": "in", "value": ["2025-02-03T12:45:46+00:00", "2025-02-04T12:45:46+00:00"]},
Document(meta={"date": "2025-02-03T12:45:46Z"}),
True,
id="in operator with equivalent ISO 8601 datetime in filter value",
),
pytest.param(
{
"field": "meta.date",
"operator": "not in",
"value": ["2025-02-03T12:45:46+00:00", "2025-02-04T12:45:46+00:00"],
},
Document(meta={"date": "2025-02-03T12:45:46Z"}),
False,
id="not in operator with equivalent ISO 8601 datetime in filter value",
),
pytest.param(
{"field": "meta.page", "operator": "==", "value": "1"},
Document(meta={"page": 1}),
False,
id="== operator with numeric string filter value and numeric Document value",
),
pytest.param(
{"field": "meta.page", "operator": "in", "value": ["1", "2"]},
Document(meta={"page": 1}),
False,
id="in operator with numeric string filter value and numeric Document value",
),
pytest.param(
{"field": "meta.date", "operator": ">=", "value": "2025-02-01"},
Document(meta={"date": "2025-02-03T12:45:46.435816Z"}),
Expand Down
Loading