What happens
QdrantCollection._inner_search in python/semantic_kernel/connectors/qdrant.py has two problems that break Qdrant vector search.
1. AsyncQdrantClient.search no longer exists. The vector search path calls self.qdrant_client.search(...). That method was removed in qdrant-client 1.16.0. I compared the wheels: async def search( is in 1.15.1 and not in 1.16.0, and query_points is in both. The extra is declared as qdrant-client ~= 1.9 (python/pyproject.toml line 139), so a fresh install resolves to 1.19.1 and every vector search fails with:
VectorSearchExecutionException: An error occurred during the search: 'AsyncQdrantClient' object has no attribute 'search'
uv.lock pins 1.12.1, and tests/unit/connectors/memory/test_qdrant.py mocks AsyncQdrantClient.search, so CI does not see it. With qdrant-client 1.16 or newer the existing unit tests error at setup, because patch("...AsyncQdrantClient.search") has nothing to patch. Hybrid search already uses query_points (added in qdrant-client 1.10.0).
2. A single-comparison filter is passed as a bare FieldCondition. A filter such as lambda x: x.temperature > 1 is parsed to a FieldCondition, and _inner_search hands it to Qdrant unchanged:
filters: Filter | list[Filter] | None = self._build_filter(options.filter) # type: ignore
filter: Filter | None = Filter(must=filters) if filters and isinstance(filters, list) else filters # type: ignore
Only a list gets wrapped. Qdrant expects a Filter here. The local client fails with 'FieldCondition' object has no attribute 'must', and the qdrant-client request models reject it too (models.Prefetch(filter=FieldCondition(...)) raises ValidationError: Input should be a valid dictionary or instance of Filter). Hybrid search fails the same way at keyword_filter.must. Filters built with and, or or not already return a Filter, so they work. test_search_filter asserts that the bare FieldCondition is passed through.
Where
python/semantic_kernel/connectors/qdrant.py QdrantCollection._inner_search on current main (64005c7), the self.qdrant_client.search( call at line 294 and the filters and filter lines above it.
Steps to reproduce
Uses the in-process Qdrant that ships with qdrant-client, no server needed.
import asyncio
from dataclasses import dataclass
from typing import Annotated
from semantic_kernel.connectors.qdrant import QdrantCollection
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
@vectorstoremodel
@dataclass
class Record:
id: Annotated[int, VectorStoreField("key")]
temperature: Annotated[int, VectorStoreField("data", is_indexed=True)]
vector: Annotated[list[float] | None, VectorStoreField("vector", dimensions=2)] = None
async def main():
collection = QdrantCollection(record_type=Record, collection_name="t", location=":memory:")
await collection.ensure_collection_exists()
await collection.upsert([Record(1, 1, [1.0, 0.2]), Record(2, 5, [0.0, 1.0]), Record(3, 9, [1.0, 1.0])])
for kwargs in ({}, {"filter": "lambda x: x.temperature > 1"}):
try:
results = await collection.search(vector=[1.0, 1.0], **kwargs)
print("search", kwargs, [r.record.id async for r in results.results])
except Exception as ex:
print("search", kwargs, type(ex).__name__, ex)
asyncio.run(main())
Actual
On main:
qdrant-client 1.12.1 / 1.15.1
search {} [3, 1, 2]
search {'filter': 'lambda x: x.temperature > 1'} VectorSearchExecutionException: ... 'FieldCondition' object has no attribute 'must'
hybrid_search with the same filter VectorSearchExecutionException: ... 'FieldCondition' object has no attribute 'must'
qdrant-client 1.16.2 / 1.19.1
search {} VectorSearchExecutionException: ... 'AsyncQdrantClient' object has no attribute 'search'
search {'filter': 'lambda x: x.temperature > 1'} VectorSearchExecutionException: ... 'AsyncQdrantClient' object has no attribute 'search'
hybrid_search with the same filter VectorSearchExecutionException: ... 'FieldCondition' object has no attribute 'must'
Expected
- Vector search uses
query_points(query=vector, using=<vector name or None>, ...) and reads .points, so it works on every qdrant-client release that has query_points (1.10 and newer). The lower bound of the extra becomes ~= 1.10, which hybrid search needs already.
- A single comparison filter is wrapped as
Filter(must=[condition]), so x.temperature > 1 returns records 3 and 2 for both search and hybrid_search.
I checked open and closed issues and PRs for Qdrant, query_points, FieldCondition and qdrant-client versions and found nothing covering this. I am opening a PR right after filing this.
What happens
QdrantCollection._inner_searchinpython/semantic_kernel/connectors/qdrant.pyhas two problems that break Qdrant vector search.1.
AsyncQdrantClient.searchno longer exists. The vector search path callsself.qdrant_client.search(...). That method was removed in qdrant-client 1.16.0. I compared the wheels:async def search(is in 1.15.1 and not in 1.16.0, andquery_pointsis in both. The extra is declared asqdrant-client ~= 1.9(python/pyproject.tomlline 139), so a fresh install resolves to 1.19.1 and every vector search fails with:uv.lockpins 1.12.1, andtests/unit/connectors/memory/test_qdrant.pymocksAsyncQdrantClient.search, so CI does not see it. With qdrant-client 1.16 or newer the existing unit tests error at setup, becausepatch("...AsyncQdrantClient.search")has nothing to patch. Hybrid search already usesquery_points(added in qdrant-client 1.10.0).2. A single-comparison filter is passed as a bare
FieldCondition. A filter such aslambda x: x.temperature > 1is parsed to aFieldCondition, and_inner_searchhands it to Qdrant unchanged:Only a list gets wrapped. Qdrant expects a
Filterhere. The local client fails with'FieldCondition' object has no attribute 'must', and the qdrant-client request models reject it too (models.Prefetch(filter=FieldCondition(...))raisesValidationError: Input should be a valid dictionary or instance of Filter). Hybrid search fails the same way atkeyword_filter.must. Filters built withand,orornotalready return aFilter, so they work.test_search_filterasserts that the bareFieldConditionis passed through.Where
python/semantic_kernel/connectors/qdrant.pyQdrantCollection._inner_searchon currentmain(64005c7), theself.qdrant_client.search(call at line 294 and thefiltersandfilterlines above it.Steps to reproduce
Uses the in-process Qdrant that ships with qdrant-client, no server needed.
Actual
On
main:Expected
query_points(query=vector, using=<vector name or None>, ...)and reads.points, so it works on every qdrant-client release that hasquery_points(1.10 and newer). The lower bound of the extra becomes~= 1.10, which hybrid search needs already.Filter(must=[condition]), sox.temperature > 1returns records 3 and 2 for bothsearchandhybrid_search.I checked open and closed issues and PRs for Qdrant,
query_points,FieldConditionand qdrant-client versions and found nothing covering this. I am opening a PR right after filing this.