From ebc3165389da094f455344ee741cd25f36325d7e Mon Sep 17 00:00:00 2001 From: Sun Haoyuan <58774225+Shy7777@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:50:44 +0800 Subject: [PATCH] Python: preserve integer precision in in-memory Hamming search --- .../core/agent_framework/_in_memory.py | 9 +++-- .../core/tests/core/test_in_memory.py | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/python/packages/core/agent_framework/_in_memory.py b/python/packages/core/agent_framework/_in_memory.py index 01b5ef6d448..cc8f8046cb7 100644 --- a/python/packages/core/agent_framework/_in_memory.py +++ b/python/packages/core/agent_framework/_in_memory.py @@ -96,7 +96,7 @@ def _numeric_vector(value: Any, *, field_name: str) -> tuple[float, ...]: number = float(item) if not math.isfinite(number): raise ValueError(f"Vector field '{field_name}' must contain only finite numbers.") - vector.append(number) + vector.append(item) if not vector: raise ValueError(f"Vector field '{field_name}' cannot be empty.") return tuple(vector) @@ -115,6 +115,11 @@ def _paired_vectors(left: Vector, right: Vector) -> tuple[tuple[float, ...], tup def _calculate_score(left: Vector, right: Vector, distance_function: DistanceFunction) -> float: left_values, right_values = _paired_vectors(left, right) + if distance_function == "hamming": + # Compare coordinates before float conversion can round distinct large integers to the same value. + return sum(a != b for a, b in zip(left_values, right_values, strict=True)) / len(left_values) + left_values = tuple(float(value) for value in left_values) + right_values = tuple(float(value) for value in right_values) if distance_function in ("cosine_similarity", "cosine_distance", "DEFAULT"): left_scale = max(abs(value) for value in left_values) right_scale = max(abs(value) for value in right_values) @@ -140,8 +145,6 @@ def _calculate_score(left: Vector, right: Vector, distance_function: DistanceFun score = sum((a - b) * (a - b) for a, b in zip(left_values, right_values, strict=True)) elif distance_function == "manhattan": score = sum(abs(a - b) for a, b in zip(left_values, right_values, strict=True)) - elif distance_function == "hamming": - score = sum(a != b for a, b in zip(left_values, right_values, strict=True)) / len(left_values) else: raise NotImplementedError(f"Distance function '{distance_function}' is not supported by InMemoryCollection.") if not math.isfinite(score): diff --git a/python/packages/core/tests/core/test_in_memory.py b/python/packages/core/tests/core/test_in_memory.py index 6477548c689..1253a189519 100644 --- a/python/packages/core/tests/core/test_in_memory.py +++ b/python/packages/core/tests/core/test_in_memory.py @@ -531,6 +531,39 @@ async def test_hamming_scores_and_thresholds_use_mismatch_proportions() -> None: ] +@pytest.mark.parametrize( + ("vector_type", "coordinate"), + [("int64", 2**53), ("int64", -(2**53) - 1), ("uint64", 2**64 - 2)], +) +@pytest.mark.parametrize("threshold", [None, 0.0]) +async def test_hamming_preserves_large_integer_coordinates( + vector_type: str, coordinate: int, threshold: float | None +) -> None: + definition = VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id"), + VectorStoreField("vector", name="vector", type_=vector_type, dimensions=2, distance_function="hamming"), + ], + collection_name="integer-hamming", + ) + collection: InMemoryCollection[str, dict[str, Any]] = InMemoryCollection(dict, definition=definition) + await collection.ensure_collection_exists() + await collection.upsert( + [ + {"id": "different", "vector": [coordinate + 1, 0]}, + {"id": "same", "vector": [coordinate, 0]}, + ], + generate_vectors=False, + ) + + results = await collection.search(vector=[coordinate, 0], score_threshold=threshold) + + expected = [("same", 0.0)] + if threshold is None: + expected.append(("different", 0.5)) + assert [(result["record"]["id"], result["score"]) async for result in results] == expected + + @pytest.mark.parametrize("contents", ["empty", "missing_vector", "filtered_out"]) @pytest.mark.parametrize("score_threshold", [None, 0.5]) async def test_unsupported_distance_is_rejected_independently_of_records(