Skip to content

Commit 7e5340e

Browse files
gauri nigamgauri nigam
authored andcommitted
fix: Update Document.__eq__ to intelligently compare floats
1 parent 40f04ac commit 7e5340e

3 files changed

Lines changed: 87 additions & 2 deletions

File tree

haystack/dataclasses/document.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
import hashlib
6+
import math
67
from dataclasses import asdict, dataclass, field, fields
78
from typing import Any
89

@@ -43,6 +44,28 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any:
4344
return super().__call__(*args, **kwargs)
4445

4546

47+
def _recursive_is_close(val1: Any, val2: Any) -> bool:
48+
if val1 == val2:
49+
return True
50+
is_f1 = isinstance(val1, float)
51+
is_f2 = isinstance(val2, float)
52+
if (
53+
(is_f1 or is_f2)
54+
and isinstance(val1, (int, float))
55+
and isinstance(val2, (int, float))
56+
and not isinstance(val1, bool)
57+
and not isinstance(val2, bool)
58+
):
59+
return math.isclose(float(val1), float(val2), rel_tol=1e-7, abs_tol=1e-7)
60+
if isinstance(val1, dict) and isinstance(val2, dict):
61+
return len(val1) == len(val2) and all(k in val2 and _recursive_is_close(val1[k], val2[k]) for k in val1)
62+
if isinstance(val1, (list, tuple)) and isinstance(val2, (list, tuple)):
63+
return len(val1) == len(val2) and all(
64+
_recursive_is_close(item1, item2) for item1, item2 in zip(val1, val2, strict=True)
65+
)
66+
return False
67+
68+
4669
@_warn_on_inplace_mutation
4770
@dataclass
4871
class Document(metaclass=_BackwardCompatible): # noqa: PLW1641
@@ -92,11 +115,11 @@ def __eq__(self, other: object) -> bool:
92115
"""
93116
Compares Documents for equality.
94117
95-
Two Documents are considered equals if their dictionary representation is identical.
118+
Two Documents are considered equal if their dictionary representations are close.
96119
"""
97120
if type(self) != type(other):
98121
return False
99-
return self.to_dict() == other.to_dict()
122+
return _recursive_is_close(self.to_dict(), other.to_dict())
100123

101124
def __post_init__(self) -> None:
102125
"""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
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.

test/dataclasses/test_document.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,64 @@ def test_basic_equality_id():
123123
assert doc1 != doc2
124124

125125

126+
def test_equality_float_precision_score():
127+
doc1 = Document(content="test text", score=0.123456782, id="1")
128+
doc2 = Document(content="test text", score=0.123456780, id="1")
129+
assert doc1 == doc2
130+
131+
doc3 = Document(content="test text", score=0.123456782, id="1")
132+
doc4 = Document(content="test text", score=0.234567890, id="1")
133+
assert doc3 != doc4
134+
135+
doc5 = Document(content="test text", score=None, id="1")
136+
doc6 = Document(content="test text", score=0.123456782, id="1")
137+
assert doc5 != doc6
138+
139+
140+
def test_equality_float_precision_embedding():
141+
doc1 = Document(content="test text", embedding=[0.123456782, 0.987654321], id="1")
142+
doc2 = Document(content="test text", embedding=[0.123456780, 0.987654320], id="1")
143+
assert doc1 == doc2
144+
145+
doc3 = Document(content="test text", embedding=[0.123456782, 0.987654321], id="1")
146+
doc4 = Document(content="test text", embedding=[0.123456782, 0.5], id="1")
147+
assert doc3 != doc4
148+
149+
doc5 = Document(content="test text", embedding=[0.123456782], id="1")
150+
doc6 = Document(content="test text", embedding=[0.123456782, 0.987654321], id="1")
151+
assert doc5 != doc6
152+
153+
doc7 = Document(content="test text", embedding=None, id="1")
154+
doc8 = Document(content="test text", embedding=[0.123456782], id="1")
155+
assert doc7 != doc8
156+
157+
158+
def test_equality_float_precision_sparse_embedding():
159+
from haystack.dataclasses.sparse_embedding import SparseEmbedding
160+
161+
se1 = SparseEmbedding(indices=[0, 1], values=[0.123456782, 0.987654321])
162+
se2 = SparseEmbedding(indices=[0, 1], values=[0.123456780, 0.987654320])
163+
doc1 = Document(content="test text", sparse_embedding=se1, id="1")
164+
doc2 = Document(content="test text", sparse_embedding=se2, id="1")
165+
assert doc1 == doc2
166+
167+
se3 = SparseEmbedding(indices=[0, 1], values=[0.123456782, 0.987654321])
168+
se4 = SparseEmbedding(indices=[0, 1], values=[0.123456782, 0.5])
169+
doc3 = Document(content="test text", sparse_embedding=se3, id="1")
170+
doc4 = Document(content="test text", sparse_embedding=se4, id="1")
171+
assert doc3 != doc4
172+
173+
174+
def test_equality_float_precision_nested_meta():
175+
doc1 = Document(content="test text", meta={"float_val": 0.123456782}, id="1")
176+
doc2 = Document(content="test text", meta={"float_val": 0.123456780}, id="1")
177+
assert doc1 == doc2
178+
179+
doc3 = Document(content="test text", meta={"float_val": 0.123456782}, id="1")
180+
doc4 = Document(content="test text", meta={"float_val": 0.5}, id="1")
181+
assert doc3 != doc4
182+
183+
126184
def test_to_dict():
127185
doc = Document()
128186
assert doc.to_dict() == {

0 commit comments

Comments
 (0)