From c5e9b1a75633cca79b8bd06ddd566251cb51eaf9 Mon Sep 17 00:00:00 2001 From: Amaaan Chauhan Date: Sun, 30 Aug 2026 16:39:22 +0530 Subject: [PATCH 1/2] fix(local): preserve near-zero cosine vectors --- qdrant_client/local/distances.py | 6 +++--- qdrant_client/local/local_collection.py | 6 +++--- qdrant_client/local/tests/test_distances.py | 11 +++++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/qdrant_client/local/distances.py b/qdrant_client/local/distances.py index 36394d99a..0bd2fb5c3 100644 --- a/qdrant_client/local/distances.py +++ b/qdrant_client/local/distances.py @@ -128,15 +128,15 @@ def cosine_similarity(query: types.NumpyArray, vectors: types.NumpyArray) -> typ distances """ vectors_norm = np.linalg.norm(vectors, axis=-1)[:, np.newaxis] - vectors /= np.where(vectors_norm != 0.0, vectors_norm, EPSILON) + 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 /= 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 /= 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..e1d502e2e 100644 --- a/qdrant_client/local/tests/test_distances.py +++ b/qdrant_client/local/tests/test_distances.py @@ -55,3 +55,14 @@ 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) From ce5a751d77c520ed3bdf54c32407869d6f7dd994 Mon Sep 17 00:00:00 2001 From: Amaaan Chauhan Date: Sun, 30 Aug 2026 20:12:52 +0530 Subject: [PATCH 2/2] fix(local): avoid cosine input mutation near zero --- qdrant_client/local/distances.py | 9 ++++++--- qdrant_client/local/tests/test_distances.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/qdrant_client/local/distances.py b/qdrant_client/local/distances.py index 0bd2fb5c3..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 > EPSILON, vectors_norm, 1.0) + 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 > EPSILON, query_norm, 1.0) + 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 > EPSILON, query_norm, 1.0) + query = query / np.where(query_norm > EPSILON, query_norm, 1.0) return np.dot(query, vectors.T) diff --git a/qdrant_client/local/tests/test_distances.py b/qdrant_client/local/tests/test_distances.py index e1d502e2e..894126817 100644 --- a/qdrant_client/local/tests/test_distances.py +++ b/qdrant_client/local/tests/test_distances.py @@ -66,3 +66,16 @@ def test_cosine_similarity_keeps_near_zero_vectors_unchanged() -> None: 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])