mirror of
https://github.com/qdrant/qdrant-client.git
synced 2026-09-21 13:37:55 -05:00
fix: fix missing multivector placeholder in local mode (#1399)
* fix: fix missing multivector placeholder in local mode * fix: do not use deleted vectors in recommend, etc * fix: fix mypy complaints in point id vector resolution * fix: regen async
This commit is contained in:
@@ -237,14 +237,6 @@ class AsyncQdrantLocal(AsyncQdrantBase):
|
||||
if lookup_from is not None and lookup_from.vector is not None
|
||||
else search_in_vector_name
|
||||
)
|
||||
sparse = vector_name in collection.sparse_vectors
|
||||
multi = vector_name in collection.multivectors
|
||||
if sparse:
|
||||
collection_vectors = collection.sparse_vectors
|
||||
elif multi:
|
||||
collection_vectors = collection.multivectors
|
||||
else:
|
||||
collection_vectors = collection.vectors
|
||||
mentioned_ids: set[types.PointId] = set()
|
||||
|
||||
def input_into_vector(vector_input: types.VectorInput) -> types.VectorInput:
|
||||
@@ -252,15 +244,7 @@ class AsyncQdrantLocal(AsyncQdrantBase):
|
||||
if isinstance(vector_input, uuid.UUID):
|
||||
vector_input = str(vector_input)
|
||||
point_id = vector_input
|
||||
if point_id not in collection.ids:
|
||||
raise ValueError(f"Point {point_id} is not found in the collection")
|
||||
idx = collection.ids[point_id]
|
||||
if vector_name in collection_vectors:
|
||||
vec = collection_vectors[vector_name][idx]
|
||||
else:
|
||||
raise ValueError(f"Vector {vector_name} not found")
|
||||
if isinstance(vec, np.ndarray):
|
||||
vec = vec.tolist()
|
||||
vec = collection._vector_by_point_id(vector_name, point_id)
|
||||
if collection_name == lookup_collection_name:
|
||||
mentioned_ids.add(point_id)
|
||||
return vec
|
||||
|
||||
@@ -307,7 +307,9 @@ class LocalCollection:
|
||||
if v is not None:
|
||||
multivectors[name].append(v)
|
||||
else:
|
||||
multivectors[name].append(np.array([]))
|
||||
multivectors[name].append(
|
||||
np.ones((1, self.multivectors_config[name].size), dtype=np.float32)
|
||||
)
|
||||
deleted_ids.append((idx, name))
|
||||
|
||||
# setup dense vectors by name
|
||||
@@ -1365,14 +1367,7 @@ class LocalCollection:
|
||||
) -> None:
|
||||
for example in examples:
|
||||
if isinstance(example, get_args(types.PointId)):
|
||||
if example not in collection.ids:
|
||||
raise ValueError(f"Point {example} is not found in the collection")
|
||||
|
||||
idx = collection.ids[example]
|
||||
vec = collection_vectors[vector_name][idx]
|
||||
|
||||
if isinstance(vec, np.ndarray):
|
||||
vec = vec.tolist()
|
||||
vec: Any = collection._vector_by_point_id(vector_name, example)
|
||||
acc.append(vec)
|
||||
if collection == self:
|
||||
mentioned_ids.append(example)
|
||||
@@ -1413,15 +1408,12 @@ class LocalCollection:
|
||||
sparse = vector_name in collection.sparse_vectors
|
||||
multi = vector_name in collection.multivectors
|
||||
if sparse:
|
||||
collection_vectors = collection.sparse_vectors
|
||||
examples_into_vectors(positive, sparse_positive_vectors)
|
||||
examples_into_vectors(negative, sparse_negative_vectors)
|
||||
elif multi:
|
||||
collection_vectors = collection.multivectors
|
||||
examples_into_vectors(positive, positive_multivectors)
|
||||
examples_into_vectors(negative, negative_multivectors)
|
||||
else:
|
||||
collection_vectors = collection.vectors
|
||||
examples_into_vectors(positive, positive_vectors)
|
||||
examples_into_vectors(negative, negative_vectors)
|
||||
|
||||
@@ -1766,22 +1758,35 @@ class LocalCollection:
|
||||
|
||||
return ids, scores
|
||||
|
||||
def _vector_by_point_id(
|
||||
self, vector_name: str, point_id: types.PointId
|
||||
) -> list[float] | SparseVector | list[list[float]]:
|
||||
if point_id not in self.ids:
|
||||
raise ValueError(f"Point {point_id} is not found in the collection")
|
||||
|
||||
idx = self.ids[point_id]
|
||||
if vector_name in self.vectors:
|
||||
vector = self.vectors[vector_name][idx].tolist()
|
||||
elif vector_name in self.sparse_vectors:
|
||||
vector = self.sparse_vectors[vector_name][idx]
|
||||
elif vector_name in self.multivectors:
|
||||
vector = self.multivectors[vector_name][idx].tolist()
|
||||
else:
|
||||
raise ValueError(f"Vector {vector_name} not found")
|
||||
|
||||
# Absent vectors are kept as placeholders to keep the storage dense, they are
|
||||
# filtered out of search results by the mask and must not be used as query vectors
|
||||
if self.deleted_per_vector[vector_name][idx]:
|
||||
raise ValueError(f"Vector with name {vector_name} for point {point_id} not found")
|
||||
|
||||
return vector
|
||||
|
||||
@staticmethod
|
||||
def _preprocess_vector_input(
|
||||
target: models.VectorInput | None, collection: "LocalCollection", vector_name: str
|
||||
) -> tuple[models.Vector, types.PointId | None]:
|
||||
if isinstance(target, get_args(types.PointId)):
|
||||
if target not in collection.ids:
|
||||
raise ValueError(f"Point {target} is not found in the collection")
|
||||
|
||||
idx = collection.ids[target]
|
||||
if vector_name in collection.vectors:
|
||||
target_vector = collection.vectors[vector_name][idx].tolist()
|
||||
elif vector_name in collection.sparse_vectors:
|
||||
target_vector = collection.sparse_vectors[vector_name][idx]
|
||||
else:
|
||||
target_vector = collection.multivectors[vector_name][idx].tolist()
|
||||
|
||||
target_vector = collection._vector_by_point_id(vector_name, target)
|
||||
return target_vector, target
|
||||
|
||||
return target, None
|
||||
@@ -1797,19 +1802,11 @@ class LocalCollection:
|
||||
multi_context_vectors = []
|
||||
|
||||
for pair in context:
|
||||
pair_vectors = []
|
||||
# holds a dense, sparse or multi vector, dispatched on by type below
|
||||
pair_vectors: list[Any] = []
|
||||
for example in [pair.positive, pair.negative]:
|
||||
if isinstance(example, get_args(types.PointId)):
|
||||
if example not in collection.ids:
|
||||
raise ValueError(f"Point {example} is not found in the collection")
|
||||
|
||||
idx = collection.ids[example]
|
||||
if vector_name in collection.vectors:
|
||||
vector = collection.vectors[vector_name][idx].tolist()
|
||||
elif vector_name in collection.sparse_vectors:
|
||||
vector = collection.sparse_vectors[vector_name][idx]
|
||||
else:
|
||||
vector = collection.multivectors[vector_name][idx].tolist()
|
||||
vector = collection._vector_by_point_id(vector_name, example)
|
||||
|
||||
pair_vectors.append(vector)
|
||||
if collection == self:
|
||||
@@ -2554,11 +2551,15 @@ class LocalCollection:
|
||||
if len(named_vectors) <= idx:
|
||||
diff = idx - len(named_vectors) + 1
|
||||
for _ in range(diff):
|
||||
named_vectors.append(np.array([]))
|
||||
named_vectors.append(
|
||||
np.ones((1, self.get_vector_params(vector_name).size), dtype=np.float32)
|
||||
)
|
||||
|
||||
if vector is None:
|
||||
# Add fake vector and mark as removed
|
||||
named_vectors[idx] = np.array([])
|
||||
named_vectors[idx] = np.ones(
|
||||
(1, self.get_vector_params(vector_name).size), dtype=np.float32
|
||||
)
|
||||
self.deleted_per_vector[vector_name] = np.append(
|
||||
self.deleted_per_vector[vector_name], 1
|
||||
)
|
||||
@@ -2965,7 +2966,9 @@ class LocalCollection:
|
||||
|
||||
if config.multivector_config is not None:
|
||||
self.multivectors_config[vector_name] = params
|
||||
self.multivectors[vector_name] = [np.array([]) for _ in range(num_points)]
|
||||
self.multivectors[vector_name] = [
|
||||
np.ones((1, config.size), dtype=np.float32) for _ in range(num_points)
|
||||
]
|
||||
else:
|
||||
self.vectors_config[vector_name] = params
|
||||
self.vectors[vector_name] = np.zeros((num_points, config.size), dtype=np.float32)
|
||||
|
||||
@@ -257,15 +257,6 @@ class QdrantLocal(QdrantBase):
|
||||
else search_in_vector_name
|
||||
)
|
||||
|
||||
sparse = vector_name in collection.sparse_vectors
|
||||
multi = vector_name in collection.multivectors
|
||||
if sparse:
|
||||
collection_vectors = collection.sparse_vectors
|
||||
elif multi:
|
||||
collection_vectors = collection.multivectors
|
||||
else:
|
||||
collection_vectors = collection.vectors
|
||||
|
||||
# mentioned ids in the search collection which should be excluded from search
|
||||
mentioned_ids: set[types.PointId] = set()
|
||||
|
||||
@@ -276,16 +267,7 @@ class QdrantLocal(QdrantBase):
|
||||
if isinstance(vector_input, uuid.UUID):
|
||||
vector_input = str(vector_input)
|
||||
point_id = vector_input # rename for clarity
|
||||
if point_id not in collection.ids:
|
||||
raise ValueError(f"Point {point_id} is not found in the collection")
|
||||
|
||||
idx = collection.ids[point_id]
|
||||
if vector_name in collection_vectors:
|
||||
vec = collection_vectors[vector_name][idx]
|
||||
else:
|
||||
raise ValueError(f"Vector {vector_name} not found")
|
||||
if isinstance(vec, np.ndarray):
|
||||
vec = vec.tolist()
|
||||
vec = collection._vector_by_point_id(vector_name, point_id)
|
||||
if collection_name == lookup_collection_name:
|
||||
mentioned_ids.add(point_id)
|
||||
return vec
|
||||
|
||||
@@ -157,6 +157,22 @@ def test_simple():
|
||||
compare_client_results(local_client, remote_client, searcher.simple_search_code)
|
||||
|
||||
|
||||
def test_simple_opt_vectors_search():
|
||||
fixture_points = generate_multivector_fixtures(100, skip_vectors=True)
|
||||
|
||||
searcher = TestSimpleSearcher()
|
||||
|
||||
local_client = init_local()
|
||||
init_client(local_client, fixture_points, vectors_config=multi_vector_config)
|
||||
|
||||
remote_client = init_remote()
|
||||
init_client(remote_client, fixture_points, vectors_config=multi_vector_config)
|
||||
|
||||
compare_client_results(local_client, remote_client, searcher.simple_search_text)
|
||||
compare_client_results(local_client, remote_client, searcher.simple_search_image)
|
||||
compare_client_results(local_client, remote_client, searcher.simple_search_code)
|
||||
|
||||
|
||||
def test_mmr():
|
||||
fixture_points = generate_multivector_fixtures(10)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||
from qdrant_client.http.models import models
|
||||
from tests.congruence_tests.test_common import (
|
||||
COLLECTION_NAME,
|
||||
@@ -169,3 +171,64 @@ def test_simple_opt_sparse_vectors_search():
|
||||
key=lambda x: x.id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_point_id_input_with_missing_vector():
|
||||
vectors_config = {
|
||||
"dense": models.VectorParams(size=4, distance=models.Distance.COSINE),
|
||||
"multi": models.VectorParams(
|
||||
size=4,
|
||||
distance=models.Distance.COSINE,
|
||||
multivector_config=models.MultiVectorConfig(
|
||||
comparator=models.MultiVectorComparator.MAX_SIM
|
||||
),
|
||||
),
|
||||
}
|
||||
# point 1 has no value for either named vector, it can neither be scored nor be used as a query
|
||||
points = [
|
||||
models.PointStruct(id=1, vector={}),
|
||||
models.PointStruct(
|
||||
id=2, vector={"dense": [1.0, 0.0, 0.0, 0.0], "multi": [[1.0, 0.0, 0.0, 0.0]]}
|
||||
),
|
||||
models.PointStruct(
|
||||
id=3, vector={"dense": [0.0, 1.0, 0.0, 0.0], "multi": [[0.0, 1.0, 0.0, 0.0]]}
|
||||
),
|
||||
]
|
||||
|
||||
local_client = init_local()
|
||||
init_client(local_client, points, vectors_config=vectors_config)
|
||||
|
||||
remote_client = init_remote()
|
||||
init_client(remote_client, points, vectors_config=vectors_config)
|
||||
|
||||
queries = [
|
||||
1,
|
||||
models.RecommendQuery(
|
||||
recommend=models.RecommendInput(
|
||||
positive=[1], strategy=models.RecommendStrategy.BEST_SCORE
|
||||
)
|
||||
),
|
||||
models.DiscoverQuery(
|
||||
discover=models.DiscoverInput(
|
||||
target=1, context=models.ContextPair(positive=2, negative=3)
|
||||
)
|
||||
),
|
||||
models.ContextQuery(context=models.ContextPair(positive=1, negative=3)),
|
||||
models.RelevanceFeedbackQuery(
|
||||
relevance_feedback=models.RelevanceFeedbackInput(
|
||||
target=2,
|
||||
feedback=[models.FeedbackItem(example=1, score=0.9)],
|
||||
strategy=models.NaiveFeedbackStrategy(
|
||||
naive=models.NaiveFeedbackStrategyParams(a=0.5, b=1.0, c=0.7)
|
||||
),
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
for using in ("dense", "multi"):
|
||||
for query in queries:
|
||||
with pytest.raises(ValueError):
|
||||
local_client.query_points(COLLECTION_NAME, query=query, using=using)
|
||||
|
||||
with pytest.raises(UnexpectedResponse):
|
||||
remote_client.query_points(COLLECTION_NAME, query=query, using=using)
|
||||
|
||||
Reference in New Issue
Block a user