Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ postgres = [
"psycopg[binary,pool] ~= 3.2"
]
qdrant = [
"qdrant-client ~= 1.9"
"qdrant-client ~= 1.10"
]
redis = [
"redis[hiredis] >= 6,< 8",
Expand Down
15 changes: 9 additions & 6 deletions python/semantic_kernel/connectors/qdrant.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,6 @@ async def _inner_search(
vector: Sequence[float | int] | None = None,
**kwargs: Any,
) -> KernelSearchResults[VectorSearchResult[TModel]]:
query_vector: tuple[str, Sequence[float | int]] | Sequence[float | int] | None = None

if not vector:
vector = await self._generate_vector_from_values(values, options)

Expand All @@ -287,19 +285,24 @@ async def _inner_search(
raise VectorStoreOperationException(
f"Vector field {options.vector_property_name} not found in data model definition."
)
query_vector = (vector_field.storage_name or vector_field.name, vector) if self.named_vectors else vector
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
filter: Filter | None = (
Filter(must=filters if isinstance(filters, list) else [filters]) # type: ignore
if filters and not isinstance(filters, Filter)
else filters
)
if search_type == SearchType.VECTOR:
results = await self.qdrant_client.search(
points = await self.qdrant_client.query_points(
collection_name=self.collection_name,
query_vector=query_vector, # type: ignore
query=vector, # type: ignore
using=(vector_field.storage_name or vector_field.name) if self.named_vectors else None,
query_filter=filter,
with_vectors=options.include_vectors,
limit=options.top,
offset=options.skip,
**kwargs,
)
results = points.points
else:
# Hybrid search: vector + keywords (RRF fusion)
# 1. Get keywords and text field
Expand Down
17 changes: 10 additions & 7 deletions python/tests/unit/connectors/memory/test_qdrant.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from pytest import fixture, mark, raises
from qdrant_client.async_qdrant_client import AsyncQdrantClient
from qdrant_client.models import Datatype, Distance, FieldCondition, MatchValue, VectorParams
from qdrant_client.models import Datatype, Distance, FieldCondition, Filter, MatchValue, VectorParams

from semantic_kernel.connectors.qdrant import QdrantCollection, QdrantStore
from semantic_kernel.data.vector import DistanceFunction, VectorStoreField
Expand Down Expand Up @@ -109,12 +109,12 @@ def mock_delete():

@fixture(autouse=True)
def mock_search():
with patch(f"{BASE_PATH}.search") as mock_search:
with patch(f"{BASE_PATH}.query_points") as mock_search:
from qdrant_client.models import ScoredPoint

response1 = ScoredPoint(id="id1", version=1, score=0.0, payload={"content": "content"})
response2 = ScoredPoint(id="id2", version=1, score=0.0, payload={"content": "content"})
mock_search.return_value = [response1, response2]
mock_search.return_value = MagicMock(points=[response1, response2])
yield mock_search


Expand Down Expand Up @@ -285,7 +285,8 @@ async def test_search(collection, mock_search):
assert mock_search.call_count == 1
mock_search.assert_called_with(
collection_name="test",
query_vector=[1.0, 2.0, 3.0],
query=[1.0, 2.0, 3.0],
using=None,
query_filter=None,
with_vectors=False,
limit=3,
Expand All @@ -307,7 +308,8 @@ async def test_search_named_vectors(collection, mock_search):
assert mock_search.call_count == 1
mock_search.assert_called_with(
collection_name="test",
query_vector=("vector", [1.0, 2.0, 3.0]),
query=[1.0, 2.0, 3.0],
using="vector",
query_filter=None,
with_vectors=False,
limit=3,
Expand All @@ -328,8 +330,9 @@ async def test_search_filter(collection, mock_search):
assert mock_search.call_count == 1
mock_search.assert_called_with(
collection_name="test",
query_vector=("vector", [1.0, 2.0, 3.0]),
query_filter=FieldCondition(key="id", match=MatchValue(value="id1")),
query=[1.0, 2.0, 3.0],
using="vector",
query_filter=Filter(must=[FieldCondition(key="id", match=MatchValue(value="id1"))]),
with_vectors=False,
limit=3,
offset=0,
Expand Down
63 changes: 63 additions & 0 deletions python/tests/unit/connectors/memory/test_qdrant_local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright (c) Microsoft. All rights reserved.

from dataclasses import dataclass
from typing import Annotated

from pytest import fixture

from semantic_kernel.connectors.qdrant import QdrantCollection
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel


@vectorstoremodel
@dataclass
class Record:
id: Annotated[int, VectorStoreField("key")]
text: Annotated[str, VectorStoreField("data", is_full_text_indexed=True)]
temperature: Annotated[int, VectorStoreField("data", is_indexed=True)]
vector: Annotated[list[float] | None, VectorStoreField("vector", dimensions=2)] = None


async def _populated_collection(named_vectors: bool):
async with QdrantCollection(
record_type=Record, collection_name="test", location=":memory:", named_vectors=named_vectors
) as collection:
await collection.ensure_collection_exists()
await collection.upsert([
Record(1, "a", 1, [1.0, 0.2]),
Record(2, "a", 5, [0.0, 1.0]),
Record(3, "a", 9, [1.0, 1.0]),
])
yield collection


@fixture(params=[True, False], ids=["named_vectors", "unnamed_vectors"])
async def local_collection(request):
async for collection in _populated_collection(request.param):
yield collection


@fixture
async def named_local_collection():
async for collection in _populated_collection(True):
yield collection


async def test_local_vector_search(local_collection):
results = await local_collection.search(vector=[1.0, 1.0])
assert [r.record.id async for r in results.results] == [3, 1, 2]


async def test_local_vector_search_single_comparison_filter(local_collection):
results = await local_collection.search(vector=[1.0, 1.0], filter="lambda x: x.temperature > 1")
assert [r.record.id async for r in results.results] == [3, 2]


async def test_local_hybrid_search_single_comparison_filter(named_local_collection):
results = await named_local_collection.hybrid_search(
values=["a"],
vector=[1.0, 1.0],
additional_property_name="text",
filter="lambda x: x.temperature > 1",
)
assert sorted([r.record.id async for r in results.results]) == [2, 3]
2 changes: 1 addition & 1 deletion python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading