From a022b3849df9436279497f46592fad7945807a62 Mon Sep 17 00:00:00 2001 From: gauri nigam Date: Fri, 10 Jul 2026 14:38:19 +0530 Subject: [PATCH] fix: Update Document.__eq__ to intelligently compare floats --- haystack/dataclasses/document.py | 27 ++++++++- ...t-eq-float-precision-d1aa2757eb55c41e.yaml | 4 ++ test/dataclasses/test_document.py | 58 +++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/document-eq-float-precision-d1aa2757eb55c41e.yaml diff --git a/haystack/dataclasses/document.py b/haystack/dataclasses/document.py index 6f6853d8e14..88fce474501 100644 --- a/haystack/dataclasses/document.py +++ b/haystack/dataclasses/document.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 import hashlib +import math from dataclasses import asdict, dataclass, field, fields from typing import Any @@ -43,6 +44,28 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: return super().__call__(*args, **kwargs) +def _recursive_is_close(val1: Any, val2: Any) -> bool: + if val1 == val2: + return True + is_f1 = isinstance(val1, float) + is_f2 = isinstance(val2, float) + if ( + (is_f1 or is_f2) + and isinstance(val1, (int, float)) + and isinstance(val2, (int, float)) + and not isinstance(val1, bool) + and not isinstance(val2, bool) + ): + return math.isclose(float(val1), float(val2), rel_tol=1e-7, abs_tol=1e-7) + if isinstance(val1, dict) and isinstance(val2, dict): + return len(val1) == len(val2) and all(k in val2 and _recursive_is_close(val1[k], val2[k]) for k in val1) + if isinstance(val1, (list, tuple)) and isinstance(val2, (list, tuple)): + return len(val1) == len(val2) and all( + _recursive_is_close(item1, item2) for item1, item2 in zip(val1, val2, strict=True) + ) + return False + + @_warn_on_inplace_mutation @dataclass class Document(metaclass=_BackwardCompatible): # noqa: PLW1641 @@ -92,11 +115,11 @@ 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 dictionary representations are close. """ if type(self) != type(other): return False - return self.to_dict() == other.to_dict() + return _recursive_is_close(self.to_dict(), other.to_dict()) def __post_init__(self) -> None: """ diff --git a/releasenotes/notes/document-eq-float-precision-d1aa2757eb55c41e.yaml b/releasenotes/notes/document-eq-float-precision-d1aa2757eb55c41e.yaml new file mode 100644 index 00000000000..0a3a7c6c986 --- /dev/null +++ b/releasenotes/notes/document-eq-float-precision-d1aa2757eb55c41e.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Updated ``Document.__eq__`` to intelligently compare float values (e.g., scores, embeddings, and nested float metadata) using ``math.isclose``, preventing equality checks from failing due to minor floating-point imprecision. diff --git a/test/dataclasses/test_document.py b/test/dataclasses/test_document.py index 9d7774db5a9..52f245aaa38 100644 --- a/test/dataclasses/test_document.py +++ b/test/dataclasses/test_document.py @@ -123,6 +123,64 @@ def test_basic_equality_id(): assert doc1 != doc2 +def test_equality_float_precision_score(): + doc1 = Document(content="test text", score=0.123456782, id="1") + doc2 = Document(content="test text", score=0.123456780, id="1") + assert doc1 == doc2 + + doc3 = Document(content="test text", score=0.123456782, id="1") + doc4 = Document(content="test text", score=0.234567890, id="1") + assert doc3 != doc4 + + doc5 = Document(content="test text", score=None, id="1") + doc6 = Document(content="test text", score=0.123456782, id="1") + assert doc5 != doc6 + + +def test_equality_float_precision_embedding(): + doc1 = Document(content="test text", embedding=[0.123456782, 0.987654321], id="1") + doc2 = Document(content="test text", embedding=[0.123456780, 0.987654320], id="1") + assert doc1 == doc2 + + doc3 = Document(content="test text", embedding=[0.123456782, 0.987654321], id="1") + doc4 = Document(content="test text", embedding=[0.123456782, 0.5], id="1") + assert doc3 != doc4 + + doc5 = Document(content="test text", embedding=[0.123456782], id="1") + doc6 = Document(content="test text", embedding=[0.123456782, 0.987654321], id="1") + assert doc5 != doc6 + + doc7 = Document(content="test text", embedding=None, id="1") + doc8 = Document(content="test text", embedding=[0.123456782], id="1") + assert doc7 != doc8 + + +def test_equality_float_precision_sparse_embedding(): + from haystack.dataclasses.sparse_embedding import SparseEmbedding + + se1 = SparseEmbedding(indices=[0, 1], values=[0.123456782, 0.987654321]) + se2 = SparseEmbedding(indices=[0, 1], values=[0.123456780, 0.987654320]) + doc1 = Document(content="test text", sparse_embedding=se1, id="1") + doc2 = Document(content="test text", sparse_embedding=se2, id="1") + assert doc1 == doc2 + + se3 = SparseEmbedding(indices=[0, 1], values=[0.123456782, 0.987654321]) + se4 = SparseEmbedding(indices=[0, 1], values=[0.123456782, 0.5]) + doc3 = Document(content="test text", sparse_embedding=se3, id="1") + doc4 = Document(content="test text", sparse_embedding=se4, id="1") + assert doc3 != doc4 + + +def test_equality_float_precision_nested_meta(): + doc1 = Document(content="test text", meta={"float_val": 0.123456782}, id="1") + doc2 = Document(content="test text", meta={"float_val": 0.123456780}, id="1") + assert doc1 == doc2 + + doc3 = Document(content="test text", meta={"float_val": 0.123456782}, id="1") + doc4 = Document(content="test text", meta={"float_val": 0.5}, id="1") + assert doc3 != doc4 + + def test_to_dict(): doc = Document() assert doc.to_dict() == {