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
27 changes: 25 additions & 2 deletions haystack/dataclasses/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import hashlib
import json
import math
from dataclasses import asdict, dataclass, field, fields
from typing import Any

Expand Down Expand Up @@ -44,6 +45,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
Expand Down Expand Up @@ -93,11 +116,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:
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
58 changes: 58 additions & 0 deletions test/dataclasses/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_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})
Expand Down