diff --git a/haystack/dataclasses/document.py b/haystack/dataclasses/document.py index 7792eaee610..c65c4a7b88e 100644 --- a/haystack/dataclasses/document.py +++ b/haystack/dataclasses/document.py @@ -93,11 +93,16 @@ def __eq__(self, other: object) -> bool: """ Compares Documents for equality. - Two Documents are considered equals if their dictionary representation is identical. + Two Documents are considered equal if their field values, including the `meta` mapping, are + identical. + + The unflattened dictionary representation is used so that metadata keys that collide with + top-level Document fields (e.g. ``id``, ``content``, ``score``) are preserved during the + comparison instead of being silently overwritten by the Document's own fields. See #11969. """ if type(self) != type(other): return False - return self.to_dict() == other.to_dict() + return self.to_dict(flatten=False) == other.to_dict(flatten=False) def __post_init__(self) -> None: """ diff --git a/releasenotes/notes/document-eq-metadata-collision-fix-11969.yaml b/releasenotes/notes/document-eq-metadata-collision-fix-11969.yaml new file mode 100644 index 00000000000..631d63e5c1b --- /dev/null +++ b/releasenotes/notes/document-eq-metadata-collision-fix-11969.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed `Document.__eq__` reporting two Documents as equal when their `meta` mapping contained + keys that collide with top-level Document fields such as `id`, `content`, or `score`. The + equality comparison now uses the unflattened dictionary representation, so metadata keys + that share a name with a Document field are preserved during comparison instead of being + silently overwritten by the Document's own field values. diff --git a/test/dataclasses/test_document.py b/test/dataclasses/test_document.py index 18e6566328c..2c7a151ba9f 100644 --- a/test/dataclasses/test_document.py +++ b/test/dataclasses/test_document.py @@ -123,6 +123,41 @@ def test_basic_equality_id(): assert doc1 != doc2 +def test_equality_preserves_meta_keys_colliding_with_top_level_fields(): + """ + Regression test for #11969. + + Equality must not be reported for two Documents whose `meta` contains keys that collide with + top-level Document fields (``id``, ``content``, ``score``). Previously ``Document.__eq__`` + compared the flattened ``to_dict()`` output, which silently overwrote colliding meta keys + with the Document's own field values during comparison. + """ + doc1 = Document(id="same_id", content="hello", meta={"id": "different1"}) + doc2 = Document(id="same_id", content="hello", meta={"id": "different2"}) + + assert doc1 != doc2 + + doc3 = Document(content="hello", meta={"score": 0.5}) + doc4 = Document(content="hello", meta={"score": 0.9}) + assert doc3 != doc4 + + doc5 = Document(content="hello", meta={"content": "from_meta"}) + doc6 = Document(content="hello", meta={"content": "also_from_meta_but_different"}) + assert doc5 != doc6 + + +def test_equality_with_colliding_meta_keys_when_meta_actually_matches(): + """ + Companion to the regression test for #11969. + + When two Documents have the same top-level fields AND the same colliding meta key, they + must still be considered equal. + """ + doc1 = Document(id="same_id", content="hello", meta={"id": "x"}) + doc2 = Document(id="same_id", content="hello", meta={"id": "x"}) + assert doc1 == doc2 + + def test_id_is_independent_of_meta_key_order(): doc1 = Document(content="hello", meta={"a": 1, "b": 2}) doc2 = Document(content="hello", meta={"b": 2, "a": 1})