Files
qdrant-client/tests/congruence_tests/test_sparse_recommend.py
George 27acfd0d75 new: remove vectors_count, update http and grpc models (#1069)
* new: remove vectors_count, update http and grpc models

* fix: update inspection cache

* new: add conversions and update interface

* fix: fix some conversions

* fix: fix typo

* fix: fix isinstance

* fix: regen async

* fix: fix update_filter usage, fix isinstance

* tests: collection metadata test

* fix: address backward compatibility in test

* new: update models, add max payload index count and copy vectors

* fix; update _inspection_cache

* new: add read consistency to count points

* Allow uuids in interface (#1085)

* new: direct uuid support

* tests: add uuid tests

* fix: update inspection cache

* new: add collection metadata and tests to local mode (#1089)

* new: add collection metadata and tests to local mode

* fix: regen async client

* new: implement parametrized rrf in local mode (#1087)

* new: implement parametrized rrf in local mode

* refactoring: use a variable for a magic value

* fix: adjust conversion according to AI

* Update filter (#1090)

* new: add missing update_filter, implement it in local mode

* fix: fix type hint, fix update operation, fix rest uploader, add tests

* fix: fix update filter is None case

* fix: mypy was not a good boy

* Text any filter (#1091)

* new: add match text any local mode

* tests: add match text any tests

* new: update models, remove init_from and locks (#1100)

* new: update models, remove init_from and locks

* deprecate: remove init from tests

* deprecate: remove lock tests

* new: convert ascii_folding

* fix: fix type stub

* new: convert acorn

* new: convert shard key with fallback

* new: update grpcio and grpcio tools in generator (#1106)

* new: update grpcio and grpcio tools in generator

* fix: bind grpcio and tools versions to 1.62.0 in generator

* Remove deprecated methods (#1103)

* deprecate: remove old api methods

* deprecate: remove type stub for removed methods

* deprecate: remove old api methods from test_qdrant_client

* deprecate: replace search with query points in test_in_memory

* deprecate: replace search methods in fastembed mixin with query points

* deprecate: replace old api methods in test async qdrant client

* deprecate: replace search with query points in test delete points

* deprecate: replace discover and context with query points in test_discovery

* deprecate: replace recommend_groups with query_points_groups in test_group_recommend

* deprecate: replace search_groups in test_group_search

* deprecate: replace recommend with query points in test_recommendation

* deprecate: replace search with query points in test search

* deprecate: replace context and discover with query points in test sparse discovery

* deprecate: replace search with query points in test sparse idf search

* deprecate: replace recommend with query points in test sparse recommend

* deprecate: replace search with query points in test sparse search

* deprecate: replace missing search request with query request in qdrant_fastembed

* deprecate: replace search with query points in test multivector search queries

* deprecate: replace upload records with upload points in test_updates

* deprecate: remove redundant structs (#1104)

* deprecate: remove redundant structs

* fix: do not use removed conversions in local mode

* fix: remove redundant conversions, simplify types.QueryRequest

* deprecate: replace old style grpc vector conversion to a new one (#1105)

* deprecate: replace old style grpc vector conversion to a new one

* fix: ignore union attr in conversion

* review fixes

---------

Co-authored-by: generall <andrey@vasnetsov.com>

---------

Co-authored-by: generall <andrey@vasnetsov.com>

---------

Co-authored-by: generall <andrey@vasnetsov.com>

* new: deprecate add, query, query_batch in fastembed mixin (#1102)

* new: deprecate add, query, query_batch in fastembed mixin

* 1.16 -> 1.17

---------

Co-authored-by: generall <andrey@vasnetsov.com>

---------

Co-authored-by: generall <andrey@vasnetsov.com>

* new: yet another update

* new: add initial_state to create shard key (#1109)

* chore: remove obsolete imports

* fix: add metadata parameter to recreate collection in local

* fix: fix metadata handling in local more

---------

Co-authored-by: generall <andrey@vasnetsov.com>
2025-11-14 17:01:17 +07:00

424 lines
14 KiB
Python

import numpy as np
import pytest
from qdrant_client.client_base import QdrantBase
from qdrant_client.http.exceptions import UnexpectedResponse
from qdrant_client.http.models import models
from tests.congruence_tests.test_common import (
COLLECTION_NAME,
compare_client_results,
generate_sparse_fixtures,
init_client,
init_local,
init_remote,
sparse_image_vector_size,
sparse_vectors_config,
)
from tests.fixtures.filters import one_random_filter_please
from tests.fixtures.points import random_sparse_vectors
secondary_collection_name = "congruence_secondary_collection"
class TestSimpleRecommendation:
__test__ = False
def __init__(self):
self.query_image = random_sparse_vectors({"sparse-image": sparse_image_vector_size})[
"sparse-image"
]
@classmethod
def simple_recommend_image(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(positive=[10], negative=[])
),
with_payload=True,
limit=10,
using="sparse-image",
).points
@classmethod
def many_recommend(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(recommend=models.RecommendInput(positive=[10, 19])),
with_payload=True,
limit=10,
using="sparse-image",
).points
@classmethod
def simple_recommend_negative(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(positive=[10], negative=[15, 7])
),
with_payload=True,
limit=10,
using="sparse-image",
).points
@classmethod
def recommend_from_another_collection(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(positive=[10], negative=[15, 7])
),
with_payload=True,
limit=10,
using="sparse-image",
lookup_from=models.LookupLocation(
collection=secondary_collection_name,
vector="sparse-image",
),
).points
@classmethod
def filter_recommend_text(
cls, client: QdrantBase, query_filter: models.Filter
) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(recommend=models.RecommendInput(positive=[10])),
query_filter=query_filter,
with_payload=True,
limit=10,
using="sparse-text",
).points
@classmethod
def best_score_recommend(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=[10, 20], negative=[], strategy=models.RecommendStrategy.BEST_SCORE
)
),
with_payload=True,
limit=10,
using="sparse-image",
).points
@classmethod
def best_score_recommend_euclid(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=[10, 20],
negative=[11, 21],
strategy=models.RecommendStrategy.BEST_SCORE,
)
),
with_payload=True,
limit=10,
using="sparse-code",
).points
@classmethod
def only_negatives_best_score_recommend(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=None, negative=[10, 12], strategy=models.RecommendStrategy.BEST_SCORE
)
),
with_payload=True,
limit=10,
using="sparse-image",
).points
@classmethod
def only_negatives_best_score_recommend_euclid(
cls, client: QdrantBase
) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=None, negative=[10, 12], strategy=models.RecommendStrategy.BEST_SCORE
)
),
with_payload=True,
limit=10,
using="sparse-code",
).points
@classmethod
def sum_scores_recommend(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=[10, 20], negative=[], strategy=models.RecommendStrategy.SUM_SCORES
)
),
with_payload=True,
limit=10,
using="sparse-image",
).points
@classmethod
def sum_scores_recommend_euclid(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=[10, 20],
negative=[11, 21],
strategy=models.RecommendStrategy.SUM_SCORES,
)
),
with_payload=True,
limit=10,
using="sparse-code",
).points
@classmethod
def only_negatives_sum_scores_recommend(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=None, negative=[10, 12], strategy=models.RecommendStrategy.SUM_SCORES
)
),
with_payload=True,
limit=10,
using="sparse-image",
).points
@classmethod
def only_negatives_sum_scores_recommend_euclid(
cls, client: QdrantBase
) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=None, negative=[10, 12], strategy=models.RecommendStrategy.SUM_SCORES
)
),
with_payload=True,
limit=10,
using="sparse-code",
).points
@classmethod
def avg_vector_recommend(cls, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=[10, 13],
negative=[],
strategy=models.RecommendStrategy.AVERAGE_VECTOR,
)
),
with_payload=True,
limit=10,
using="sparse-image",
).points
def recommend_from_raw_vectors(self, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(positive=[self.query_image], negative=[])
),
with_payload=True,
limit=10,
using="sparse-image",
).points
def recommend_from_raw_vectors_and_ids(self, client: QdrantBase) -> list[models.ScoredPoint]:
return client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(positive=[self.query_image, 10], negative=[])
),
with_payload=True,
limit=10,
using="sparse-image",
).points
@staticmethod
def recommend_batch(client: QdrantBase) -> list[models.QueryResponse]:
return client.query_batch_points(
collection_name=COLLECTION_NAME,
requests=[
models.QueryRequest(
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=[3],
negative=[],
strategy=models.RecommendStrategy.AVERAGE_VECTOR,
)
),
limit=1,
using="sparse-image",
),
models.QueryRequest(
query=models.RecommendQuery(
recommend=models.RecommendInput(
positive=[10],
negative=[],
strategy=models.RecommendStrategy.BEST_SCORE,
)
),
limit=2,
using="sparse-image",
lookup_from=models.LookupLocation(
collection=secondary_collection_name,
vector="sparse-image",
),
),
],
)
def test_simple_recommend() -> None:
fixture_points = generate_sparse_fixtures()
secondary_collection_points = generate_sparse_fixtures(100)
searcher = TestSimpleRecommendation()
local_client = init_local()
init_client(
local_client,
fixture_points,
vectors_config={},
sparse_vectors_config=sparse_vectors_config,
)
init_client(
local_client,
secondary_collection_points,
secondary_collection_name,
vectors_config={},
sparse_vectors_config=sparse_vectors_config,
)
remote_client = init_remote()
init_client(
remote_client,
fixture_points,
vectors_config={},
sparse_vectors_config=sparse_vectors_config,
)
init_client(
remote_client,
secondary_collection_points,
secondary_collection_name,
vectors_config={},
sparse_vectors_config=sparse_vectors_config,
)
compare_client_results(local_client, remote_client, searcher.simple_recommend_image)
compare_client_results(local_client, remote_client, searcher.many_recommend)
compare_client_results(local_client, remote_client, searcher.simple_recommend_negative)
compare_client_results(local_client, remote_client, searcher.recommend_from_another_collection)
compare_client_results(local_client, remote_client, searcher.best_score_recommend)
compare_client_results(local_client, remote_client, searcher.best_score_recommend_euclid)
compare_client_results(
local_client, remote_client, searcher.only_negatives_best_score_recommend
)
compare_client_results(
local_client, remote_client, searcher.only_negatives_best_score_recommend_euclid
)
compare_client_results(local_client, remote_client, searcher.sum_scores_recommend)
compare_client_results(local_client, remote_client, searcher.sum_scores_recommend_euclid)
compare_client_results(
local_client, remote_client, searcher.only_negatives_sum_scores_recommend
)
compare_client_results(
local_client, remote_client, searcher.only_negatives_sum_scores_recommend_euclid
)
compare_client_results(local_client, remote_client, searcher.avg_vector_recommend)
compare_client_results(local_client, remote_client, searcher.recommend_from_raw_vectors)
compare_client_results(
local_client, remote_client, searcher.recommend_from_raw_vectors_and_ids
)
compare_client_results(local_client, remote_client, searcher.recommend_batch)
for _ in range(10):
query_filter = one_random_filter_please()
try:
compare_client_results(
local_client,
remote_client,
searcher.filter_recommend_text,
query_filter=query_filter,
)
except AssertionError as e:
print(f"\nFailed with filter {query_filter}")
raise e
def test_query_with_nan():
fixture_points = generate_sparse_fixtures()
sparse_vector_dict = random_sparse_vectors({"sparse-image": sparse_image_vector_size})
sparse_vector = sparse_vector_dict["sparse-image"]
sparse_vector.values[0] = np.nan
using = "sparse-image"
local_client = init_local()
remote_client = init_remote()
init_client(
local_client,
fixture_points,
vectors_config={},
sparse_vectors_config=sparse_vectors_config,
)
init_client(
remote_client,
fixture_points,
vectors_config={},
sparse_vectors_config=sparse_vectors_config,
)
with pytest.raises(AssertionError):
local_client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(positive=[sparse_vector], negative=[])
),
using=using,
)
with pytest.raises(UnexpectedResponse):
remote_client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(positive=[sparse_vector], negative=[])
),
using=using,
)
with pytest.raises(AssertionError):
local_client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(positive=[1], negative=[sparse_vector])
),
using=using,
)
with pytest.raises(UnexpectedResponse):
remote_client.query_points(
collection_name=COLLECTION_NAME,
query=models.RecommendQuery(
recommend=models.RecommendInput(positive=[1], negative=[sparse_vector])
),
using=using,
)