fix(vector_stores): normalize scores to similarity (higher = better) across all backends (#5391)
This commit is contained in:
@@ -313,13 +313,10 @@ class AzureMySQL(VectorStoreBase):
|
||||
|
||||
for row in results:
|
||||
vec = np.array(json.loads(row['vector']))
|
||||
# Cosine similarity
|
||||
similarity = np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec))
|
||||
distance = 1 - similarity
|
||||
scored_results.append((row['id'], distance, row['payload']))
|
||||
similarity = float(np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec)))
|
||||
scored_results.append((row['id'], similarity, row['payload']))
|
||||
|
||||
# Sort by distance and apply limit
|
||||
scored_results.sort(key=lambda x: x[1])
|
||||
scored_results.sort(key=lambda x: x[1], reverse=True)
|
||||
scored_results = scored_results[:top_k]
|
||||
|
||||
return [
|
||||
|
||||
@@ -14,7 +14,15 @@ class VectorStoreBase(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def search(self, query, vectors, top_k=5, filters=None):
|
||||
"""Search for similar vectors."""
|
||||
"""Search for similar vectors.
|
||||
|
||||
All implementations must return similarity scores where higher values
|
||||
indicate greater similarity (range [0, 1] preferred). Implementations
|
||||
using distance metrics must convert to similarity before returning:
|
||||
- Cosine distance: score = max(0.0, 1.0 - distance)
|
||||
- L2 distance: score = 1.0 / (1.0 + distance)
|
||||
- Inner product: score = value (already higher = better)
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -258,10 +258,8 @@ class CassandraDB(VectorStoreBase):
|
||||
continue
|
||||
|
||||
vec = np.array(row.vector)
|
||||
|
||||
# Cosine similarity
|
||||
similarity = np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec))
|
||||
distance = 1 - similarity
|
||||
|
||||
similarity = float(np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec)))
|
||||
|
||||
# Apply filters if provided
|
||||
if filters:
|
||||
@@ -273,10 +271,9 @@ class CassandraDB(VectorStoreBase):
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
scored_results.append((row.id, distance, row.payload))
|
||||
scored_results.append((row.id, similarity, row.payload))
|
||||
|
||||
# Sort by distance and apply limit
|
||||
scored_results.sort(key=lambda x: x[1])
|
||||
scored_results.sort(key=lambda x: x[1], reverse=True)
|
||||
scored_results = scored_results[:top_k]
|
||||
|
||||
return [
|
||||
|
||||
@@ -97,9 +97,11 @@ class ChromaDB(VectorStoreBase):
|
||||
|
||||
result = []
|
||||
for i in range(max_length):
|
||||
raw_distance = distances[i] if isinstance(distances, list) and distances and i < len(distances) else None
|
||||
score = 1.0 / (1.0 + raw_distance) if raw_distance is not None else None
|
||||
entry = OutputData(
|
||||
id=ids[i] if isinstance(ids, list) and ids and i < len(ids) else None,
|
||||
score=(distances[i] if isinstance(distances, list) and distances and i < len(distances) else None),
|
||||
score=score,
|
||||
payload=(metadatas[i] if isinstance(metadatas, list) and metadatas and i < len(metadatas) else None),
|
||||
)
|
||||
result.append(entry)
|
||||
|
||||
@@ -279,7 +279,11 @@ class FAISS(VectorStoreBase):
|
||||
|
||||
payload_copy = payload.copy()
|
||||
|
||||
score = float(scores[i])
|
||||
raw_score = float(scores[i])
|
||||
if self.distance_strategy.lower() == "euclidean":
|
||||
score = 1.0 / (1.0 + raw_score)
|
||||
else:
|
||||
score = raw_score
|
||||
entry = OutputData(
|
||||
id=vector_id,
|
||||
score=score,
|
||||
|
||||
@@ -11,7 +11,14 @@ try:
|
||||
except ImportError:
|
||||
raise ImportError("The 'pymilvus' library is required. Please install it using 'pip install pymilvus'.")
|
||||
|
||||
from pymilvus import CollectionSchema, DataType, FieldSchema, Function, FunctionType, MilvusClient
|
||||
from pymilvus import (
|
||||
CollectionSchema,
|
||||
DataType,
|
||||
FieldSchema,
|
||||
Function,
|
||||
FunctionType,
|
||||
MilvusClient,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -167,11 +174,14 @@ class MilvusDB(VectorStoreBase):
|
||||
memory = []
|
||||
|
||||
for value in data:
|
||||
uid, score, metadata = (
|
||||
value.get("id"),
|
||||
value.get("distance"),
|
||||
value.get("entity", {}).get("metadata"),
|
||||
)
|
||||
uid = value.get("id")
|
||||
raw_distance = value.get("distance")
|
||||
metadata = value.get("entity", {}).get("metadata")
|
||||
|
||||
if raw_distance is not None and self.metric_type in (MetricType.L2, "L2"):
|
||||
score = 1.0 / (1.0 + raw_distance)
|
||||
else:
|
||||
score = raw_distance
|
||||
|
||||
memory_obj = OutputData(id=uid, score=score, payload=metadata)
|
||||
memory.append(memory_obj)
|
||||
|
||||
@@ -347,7 +347,7 @@ class PGVector(VectorStoreBase):
|
||||
)
|
||||
|
||||
results = cur.fetchall()
|
||||
return [OutputData(id=str(r[0]), score=float(r[1]), payload=r[2]) for r in results]
|
||||
return [OutputData(id=str(r[0]), score=max(0.0, 1.0 - float(r[1])), payload=r[2]) for r in results]
|
||||
|
||||
def keyword_search(self, query, top_k=5, filters=None):
|
||||
"""
|
||||
|
||||
@@ -158,7 +158,7 @@ class RedisDB(VectorStoreBase):
|
||||
return [
|
||||
MemoryResult(
|
||||
id=result["memory_id"],
|
||||
score=float(result["vector_distance"]),
|
||||
score=max(0.0, 1.0 - float(result["vector_distance"])),
|
||||
payload={
|
||||
"hash": result["hash"],
|
||||
"data": result["memory"],
|
||||
|
||||
@@ -80,7 +80,9 @@ class S3Vectors(VectorStoreBase):
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Failed to parse metadata for key {v.get('key')}")
|
||||
payload = {}
|
||||
results.append(OutputData(id=v.get("key"), score=v.get("distance"), payload=payload))
|
||||
raw_distance = v.get("distance")
|
||||
score = max(0.0, 1.0 - raw_distance) if raw_distance is not None else None
|
||||
results.append(OutputData(id=v.get("key"), score=score, payload=payload))
|
||||
return results
|
||||
|
||||
def insert(self, vectors, payloads=None, ids=None):
|
||||
|
||||
@@ -135,7 +135,7 @@ class Supabase(VectorStoreBase):
|
||||
data=vectors, limit=top_k, filters=filters, include_metadata=True, include_value=True
|
||||
)
|
||||
|
||||
return [OutputData(id=str(result[0]), score=float(result[1]), payload=result[2]) for result in results]
|
||||
return [OutputData(id=str(result[0]), score=max(0.0, 1.0 - float(result[1])), payload=result[2]) for result in results]
|
||||
|
||||
def delete(self, vector_id: str):
|
||||
"""
|
||||
|
||||
@@ -388,8 +388,8 @@ class ValkeyDB(VectorStoreBase):
|
||||
"""
|
||||
memory_results = []
|
||||
for doc in results.docs:
|
||||
# Extract the score
|
||||
score = float(doc.vector_score) if hasattr(doc, "vector_score") else None
|
||||
raw_distance = float(doc.vector_score) if hasattr(doc, "vector_score") else None
|
||||
score = max(0.0, 1.0 - raw_distance) if raw_distance is not None else None
|
||||
|
||||
# Create the payload
|
||||
payload = {
|
||||
|
||||
@@ -114,10 +114,12 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
results = data.get("nearestNeighbors", {}).get("neighbors", [])
|
||||
output_data = []
|
||||
for result in results:
|
||||
raw_distance = result.get("distance")
|
||||
score = max(0.0, 1.0 - raw_distance) if raw_distance is not None else None
|
||||
output_data.append(
|
||||
OutputData(
|
||||
id=result.get("datapoint").get("datapointId"),
|
||||
score=result.get("distance"),
|
||||
score=score,
|
||||
payload=result.get("datapoint").get("metadata"),
|
||||
)
|
||||
)
|
||||
@@ -264,7 +266,8 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
logger.debug("Adding %s: %s", restrict.name, restrict.allow_tokens[0])
|
||||
payload[restrict.name] = restrict.allow_tokens[0]
|
||||
|
||||
output_data = OutputData(id=neighbor.id, score=neighbor.distance, payload=payload)
|
||||
score = max(0.0, 1.0 - neighbor.distance) if neighbor.distance is not None else None
|
||||
output_data = OutputData(id=neighbor.id, score=score, payload=payload)
|
||||
results.append(output_data)
|
||||
|
||||
logger.debug("Returning %d results", len(results))
|
||||
@@ -413,7 +416,8 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
if restrict.allow_list:
|
||||
payload[restrict.namespace] = restrict.allow_list[0]
|
||||
|
||||
return OutputData(id=neighbor.datapoint.datapoint_id, score=neighbor.distance, payload=payload)
|
||||
score = max(0.0, 1.0 - neighbor.distance) if neighbor.distance is not None else None
|
||||
return OutputData(id=neighbor.datapoint.datapoint_id, score=score, payload=payload)
|
||||
|
||||
logger.debug("No results found")
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user