Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
50 changes: 50 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,50 @@
# 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


@fixture
async def local_collection():
async with QdrantCollection(record_type=Record, collection_name="test", location=":memory:") 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


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(local_collection):
results = await 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