diff --git a/qdrant_client/local/distances.py b/qdrant_client/local/distances.py index 36394d99a..e999da4d6 100644 --- a/qdrant_client/local/distances.py +++ b/qdrant_client/local/distances.py @@ -127,16 +127,19 @@ def cosine_similarity(query: types.NumpyArray, vectors: types.NumpyArray) -> typ Returns: distances """ + vectors = np.array(vectors, copy=True) + query = np.array(query, copy=True) + vectors_norm = np.linalg.norm(vectors, axis=-1)[:, np.newaxis] - vectors /= np.where(vectors_norm != 0.0, vectors_norm, EPSILON) + vectors = vectors / np.where(vectors_norm > EPSILON, vectors_norm, 1.0) if len(query.shape) == 1: query_norm = np.linalg.norm(query) - query /= np.where(query_norm != 0.0, query_norm, EPSILON) + query = query / np.where(query_norm > EPSILON, query_norm, 1.0) return np.dot(vectors, query) query_norm = np.linalg.norm(query, axis=-1)[:, np.newaxis] - query /= np.where(query_norm != 0.0, query_norm, EPSILON) + query = query / np.where(query_norm > EPSILON, query_norm, 1.0) return np.dot(query, vectors.T) diff --git a/qdrant_client/local/local_collection.py b/qdrant_client/local/local_collection.py index b21c18414..583e0e2a1 100644 --- a/qdrant_client/local/local_collection.py +++ b/qdrant_client/local/local_collection.py @@ -2442,7 +2442,7 @@ def _update_point(self, point: models.PointStruct) -> None: if params.distance == models.Distance.COSINE: vector_norm = np.linalg.norm(vector, axis=-1)[:, np.newaxis] - vector /= np.where(vector_norm != 0.0, vector_norm, EPSILON) + vector /= np.where(vector_norm > EPSILON, vector_norm, 1.0) self.multivectors[vector_name][idx] = np.array(vector) self.deleted_per_vector[vector_name][idx] = 0 else: @@ -2541,7 +2541,7 @@ def _add_point(self, point: models.PointStruct) -> None: params = self.get_vector_params(vector_name) if params.distance == models.Distance.COSINE: vector_norm = np.linalg.norm(vector_np, axis=-1)[:, np.newaxis] - vector_np /= np.where(vector_norm != 0.0, vector_norm, EPSILON) + vector_np /= np.where(vector_norm > EPSILON, vector_norm, 1.0) named_vectors[idx] = vector_np self.deleted_per_vector[vector_name] = np.append( self.deleted_per_vector[vector_name], 0 @@ -2686,7 +2686,7 @@ def _update_named_vectors( else: if params.distance == models.Distance.COSINE: vector_norm = np.linalg.norm(vector_np, axis=-1)[:, np.newaxis] - vector_np /= np.where(vector_norm != 0.0, vector_norm, EPSILON) + vector_np /= np.where(vector_norm > EPSILON, vector_norm, 1.0) self.multivectors[vector_name][idx] = vector_np def update_vectors( diff --git a/qdrant_client/local/tests/test_distances.py b/qdrant_client/local/tests/test_distances.py index ba4dc7ed6..894126817 100644 --- a/qdrant_client/local/tests/test_distances.py +++ b/qdrant_client/local/tests/test_distances.py @@ -55,3 +55,27 @@ def test_distances() -> None: multivector_query = np.array([[1, 2, 3], [3, 4, 5]]) docs = [np.array([[1, 2, 3], [0, 1, 2]])] assert calculate_multi_distance(multivector_query, docs, models.Distance.DOT)[0] == 40.0 + + +def test_cosine_similarity_keeps_near_zero_vectors_unchanged() -> None: + tiny = np.array([5e-11] * 4, dtype=np.float32) + vectors = tiny.copy() + query = np.array([1.0] * 4, dtype=np.float32) + + result = calculate_distance(query, vectors[None, :], models.Distance.COSINE) + + assert np.allclose(vectors, tiny) + assert np.allclose(result, [1e-10], atol=1e-12) + + +def test_cosine_similarity_does_not_mutate_inputs() -> None: + query = np.array([1.0, 1.0], dtype=np.float32) + vectors = np.array([[1.0, 1.0], [0.0, 0.0]], dtype=np.float32) + query_before = query.copy() + vectors_before = vectors.copy() + + result = calculate_distance(query, vectors, models.Distance.COSINE) + + assert np.allclose(query, query_before) + assert np.allclose(vectors, vectors_before) + assert np.allclose(result, [1.0, 0.0])