Skip to content

Commit fecb346

Browse files
committed
fix(vector-store): normalize Milvus cosine scores
1 parent 4eddfe3 commit fecb346

2 files changed

Lines changed: 54 additions & 3 deletions

File tree

src/vector_store/milvus.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,16 +376,16 @@ async def query(
376376

377377
results: list[VectorQueryResult] = []
378378
for hit in batches[0] if batches else []:
379-
distance = float(hit.get("distance", 0.0))
380-
if max_distance is not None and distance > max_distance:
379+
cosine_distance = self._hit_cosine_distance(hit)
380+
if max_distance is not None and cosine_distance > max_distance:
381381
continue
382382

383383
entity = cast(dict[str, Any], hit.get("entity") or {})
384384
vector_id = str(hit.get(ID_FIELD) or entity.get(ID_FIELD))
385385
results.append(
386386
VectorQueryResult(
387387
id=vector_id,
388-
score=distance,
388+
score=cosine_distance,
389389
metadata=self._entity_metadata(entity),
390390
)
391391
)
@@ -397,6 +397,14 @@ async def query(
397397
)
398398
return results
399399

400+
def _hit_cosine_distance(self, hit: dict[str, Any]) -> float:
401+
"""Return Honcho cosine distance from a Milvus search hit."""
402+
if "distance" in hit:
403+
return float(hit["distance"])
404+
if "score" in hit:
405+
return 1.0 - float(hit["score"])
406+
return 0.0
407+
400408
def _output_fields(self, include_attributes: bool | list[str]) -> list[str] | None:
401409
"""Translate Honcho projection settings to Milvus output fields."""
402410
if include_attributes is True:

tests/vector_store/test_milvus.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import re
77
from pathlib import Path
8+
from typing import Any, cast
89

910
import pytest
1011

@@ -23,6 +24,29 @@ def _helper_store() -> MilvusVectorStore:
2324
return object.__new__(MilvusVectorStore)
2425

2526

27+
class _ScoreOnlySearchClient:
28+
def has_collection(self, *, collection_name: str) -> bool:
29+
assert collection_name
30+
return True
31+
32+
def search(self, **kwargs: Any) -> list[list[dict[str, Any]]]:
33+
assert kwargs["search_params"] == {"metric_type": "COSINE"}
34+
return [
35+
[
36+
{
37+
"id": "vec_close",
38+
"score": 0.9,
39+
"entity": {"message_id": "msg_1"},
40+
},
41+
{
42+
"id": "vec_far",
43+
"score": 0.2,
44+
"entity": {"message_id": "msg_2"},
45+
},
46+
]
47+
]
48+
49+
2650
def test_collection_name_is_valid_stable_and_bounded() -> None:
2751
store = _helper_store()
2852
namespace = "honcho.doc." + ("workspace.with-dashes" * 30)
@@ -72,6 +96,25 @@ def test_projection_settings_map_to_milvus_output_fields() -> None:
7296
]
7397

7498

99+
@pytest.mark.asyncio
100+
async def test_query_converts_milvus_score_to_cosine_distance(
101+
monkeypatch: pytest.MonkeyPatch,
102+
) -> None:
103+
monkeypatch.setattr(settings.EMBEDDING, "VECTOR_DIMENSIONS", 4)
104+
store = _helper_store()
105+
store.client = cast(Any, _ScoreOnlySearchClient())
106+
107+
results = await store.query(
108+
"honcho.msg.test",
109+
[1.0, 0.0, 0.0, 0.0],
110+
max_distance=0.5,
111+
)
112+
113+
assert [result.id for result in results] == ["vec_close"]
114+
assert abs(results[0].score - 0.1) < 1e-12
115+
assert results[0].metadata == {"message_id": "msg_1"}
116+
117+
75118
@pytest.mark.asyncio
76119
async def test_milvus_lite_round_trip(
77120
monkeypatch: pytest.MonkeyPatch, tmp_path: Path

0 commit comments

Comments
 (0)