fix(vector_stores/s3_vectors): make search score metric-aware (#6547)
This commit is contained in:
@@ -69,6 +69,16 @@ class S3Vectors(VectorStoreBase):
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _distance_to_score(self, raw_distance: Optional[float]) -> Optional[float]:
|
||||||
|
if raw_distance is None:
|
||||||
|
return None
|
||||||
|
# Euclidean distance is unbounded, so 1 - distance would collapse most
|
||||||
|
# scores to 0. Use a bounded monotonic map instead. Cosine distance is
|
||||||
|
# in [0, 2], where 1 - distance is already a valid similarity.
|
||||||
|
if self.distance_metric == "euclidean":
|
||||||
|
return 1.0 / (1.0 + raw_distance)
|
||||||
|
return max(0.0, 1.0 - raw_distance)
|
||||||
|
|
||||||
def _parse_output(self, vectors: List[Dict]) -> List[OutputData]:
|
def _parse_output(self, vectors: List[Dict]) -> List[OutputData]:
|
||||||
results = []
|
results = []
|
||||||
for v in vectors:
|
for v in vectors:
|
||||||
@@ -81,7 +91,7 @@ class S3Vectors(VectorStoreBase):
|
|||||||
logger.warning(f"Failed to parse metadata for key {v.get('key')}")
|
logger.warning(f"Failed to parse metadata for key {v.get('key')}")
|
||||||
payload = {}
|
payload = {}
|
||||||
raw_distance = v.get("distance")
|
raw_distance = v.get("distance")
|
||||||
score = max(0.0, 1.0 - raw_distance) if raw_distance is not None else None
|
score = self._distance_to_score(raw_distance)
|
||||||
results.append(OutputData(id=v.get("key"), score=score, payload=payload))
|
results.append(OutputData(id=v.get("key"), score=score, payload=payload))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,45 @@ def test_search(mock_boto_client):
|
|||||||
assert results[0].score == pytest.approx(0.1)
|
assert results[0].score == pytest.approx(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_score_cosine_uses_one_minus_distance(mock_boto_client):
|
||||||
|
"""Cosine metric keeps the 1 - distance similarity mapping."""
|
||||||
|
mock_boto_client.query_vectors.return_value = {"vectors": [{"key": "id1", "distance": 0.25, "metadata": {}}]}
|
||||||
|
store = S3Vectors(
|
||||||
|
vector_bucket_name=BUCKET_NAME,
|
||||||
|
collection_name=INDEX_NAME,
|
||||||
|
embedding_model_dims=EMBEDDING_DIMS,
|
||||||
|
distance_metric="cosine",
|
||||||
|
)
|
||||||
|
|
||||||
|
results = store.search(query="test", vectors=[0.1, 0.2], top_k=1)
|
||||||
|
|
||||||
|
assert results[0].score == pytest.approx(0.75)
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_score_euclidean_uses_bounded_map(mock_boto_client):
|
||||||
|
"""Euclidean distance is unbounded; scores must not collapse to 0."""
|
||||||
|
mock_boto_client.query_vectors.return_value = {
|
||||||
|
"vectors": [
|
||||||
|
{"key": "near", "distance": 0.5, "metadata": {}},
|
||||||
|
{"key": "far", "distance": 4.0, "metadata": {}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
store = S3Vectors(
|
||||||
|
vector_bucket_name=BUCKET_NAME,
|
||||||
|
collection_name=INDEX_NAME,
|
||||||
|
embedding_model_dims=EMBEDDING_DIMS,
|
||||||
|
distance_metric="euclidean",
|
||||||
|
)
|
||||||
|
|
||||||
|
results = store.search(query="test", vectors=[0.1, 0.2], top_k=2)
|
||||||
|
|
||||||
|
# 1 / (1 + d): a distance > 1 would give a negative (clamped 0) score under
|
||||||
|
# the old cosine-only formula; the bounded map keeps ranking intact.
|
||||||
|
assert results[0].score == pytest.approx(1.0 / 1.5)
|
||||||
|
assert results[1].score == pytest.approx(1.0 / 5.0)
|
||||||
|
assert results[0].score > results[1].score > 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_get(mock_boto_client):
|
def test_get(mock_boto_client):
|
||||||
"""Test retrieving a vector by ID."""
|
"""Test retrieving a vector by ID."""
|
||||||
mock_boto_client.get_vectors.return_value = {
|
mock_boto_client.get_vectors.return_value = {
|
||||||
|
|||||||
Reference in New Issue
Block a user