# Copyright (c) Microsoft. All rights reserved. from unittest.mock import MagicMock, patch 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 semantic_kernel.connectors.qdrant import QdrantCollection, QdrantStore from semantic_kernel.data.vector import DistanceFunction, VectorStoreField from semantic_kernel.exceptions import ( VectorSearchExecutionException, VectorStoreInitializationException, VectorStoreModelValidationError, VectorStoreOperationException, ) BASE_PATH = "qdrant_client.async_qdrant_client.AsyncQdrantClient" @fixture def vector_store(qdrant_unit_test_env): return QdrantStore(env_file_path="test.env") @fixture def collection(qdrant_unit_test_env, definition): return QdrantCollection( record_type=dict, collection_name="test", definition=definition, env_file_path="test.env", ) @fixture def collection_without_named_vectors(qdrant_unit_test_env, definition): return QdrantCollection( record_type=dict, collection_name="test", definition=definition, named_vectors=False, env_file_path="test.env", ) @fixture(autouse=True) def mock_list_collection_names(): with patch(f"{BASE_PATH}.get_collections") as mock_get_collections: from qdrant_client.conversions.common_types import CollectionsResponse from qdrant_client.http.models import CollectionDescription response = MagicMock(spec=CollectionsResponse) response.collections = [CollectionDescription(name="test")] mock_get_collections.return_value = response yield mock_get_collections @fixture(autouse=True) def mock_collection_exists(): with patch(f"{BASE_PATH}.collection_exists") as mock_collection_exists: mock_collection_exists.return_value = True yield mock_collection_exists @fixture(autouse=True) def mock_ensure_collection_exists(): with patch(f"{BASE_PATH}.create_collection") as mock_ensure_collection_exists: yield mock_ensure_collection_exists @fixture(autouse=True) def mock_ensure_collection_deleted(): with patch(f"{BASE_PATH}.delete_collection") as mock_ensure_collection_deleted: mock_ensure_collection_deleted.return_value = True yield mock_ensure_collection_deleted @fixture(autouse=True) def mock_upsert(): with patch(f"{BASE_PATH}.upsert") as mock_upsert: from qdrant_client.conversions.common_types import UpdateResult result = MagicMock(spec=UpdateResult) result.status = "completed" mock_upsert.return_value = result yield mock_upsert @fixture(autouse=True) def mock_get(collection): with patch(f"{BASE_PATH}.retrieve") as mock_retrieve: from qdrant_client.http.models import Record if collection.named_vectors: mock_retrieve.return_value = [ Record(id="id1", payload={"content": "content"}, vector={"vector": [1.0, 2.0, 3.0]}) ] else: mock_retrieve.return_value = [Record(id="id1", payload={"content": "content"}, vector=[1.0, 2.0, 3.0])] yield mock_retrieve @fixture(autouse=True) def mock_delete(): with patch(f"{BASE_PATH}.delete") as mock_delete: yield mock_delete @fixture(autouse=True) def mock_search(): with patch(f"{BASE_PATH}.search") 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] yield mock_search async def test_vector_store_defaults(vector_store): async with vector_store: assert vector_store.qdrant_client is not None assert vector_store.qdrant_client._client.rest_uri == "http://localhost:6333" def test_vector_store_with_client(): qdrant_store = QdrantStore(client=AsyncQdrantClient()) assert qdrant_store.qdrant_client is not None assert qdrant_store.qdrant_client._client.rest_uri == "http://localhost:6333" @mark.parametrize("exclude_list", [["QDRANT_LOCATION"]], indirect=True) def test_vector_store_in_memory(qdrant_unit_test_env): from qdrant_client.local.async_qdrant_local import AsyncQdrantLocal qdrant_store = QdrantStore(api_key="supersecretkey", env_file_path="test.env") assert qdrant_store.qdrant_client is not None assert isinstance(qdrant_store.qdrant_client._client, AsyncQdrantLocal) assert qdrant_store.qdrant_client._client.location == ":memory:" def test_vector_store_fail(): with raises(VectorStoreInitializationException, match="Failed to create Qdrant settings."): QdrantStore(location="localhost", url="localhost", env_file_path="test.env") with raises(VectorStoreInitializationException, match="Failed to create Qdrant client."): QdrantStore(location="localhost", url="http://localhost", env_file_path="test.env") async def test_store_list_collection_names(vector_store): collections = await vector_store.list_collection_names() assert collections == ["test"] def test_get_collection(vector_store: QdrantStore, definition, qdrant_unit_test_env): collection = vector_store.get_collection(collection_name="test", record_type=dict, definition=definition) assert collection.collection_name == "test" assert collection.qdrant_client == vector_store.qdrant_client assert collection.record_type is dict assert collection.definition == definition async def test_collection_init(definition, qdrant_unit_test_env): async with QdrantCollection( record_type=dict, collection_name="test", definition=definition, env_file_path="test.env", ) as collection: assert collection.collection_name == "test" assert collection.qdrant_client is not None assert collection.record_type is dict assert collection.definition == definition assert collection.named_vectors def test_collection_init_fail(definition): with raises(VectorStoreInitializationException, match="Failed to create Qdrant settings."): QdrantCollection( record_type=dict, collection_name="test", definition=definition, url="localhost", env_file_path="test.env", ) with raises(VectorStoreInitializationException, match="Failed to create Qdrant client."): QdrantCollection( record_type=dict, collection_name="test", definition=definition, location="localhost", url="http://localhost", env_file_path="test.env", ) with raises( VectorStoreModelValidationError, match="Only one vector field is allowed when not using named vectors." ): definition.fields.append(VectorStoreField("vector", name="vector2", dimensions=3)) QdrantCollection( record_type=dict, collection_name="test", definition=definition, named_vectors=False, env_file_path="test.env", ) @mark.parametrize("collection_to_use", ["collection", "collection_without_named_vectors"]) async def test_upsert(collection_to_use, request): from qdrant_client.models import PointStruct collection = request.getfixturevalue(collection_to_use) if collection.named_vectors: record = PointStruct(id="id1", payload={"content": "content"}, vector={"vector": [1.0, 2.0, 3.0]}) else: record = PointStruct(id="id1", payload={"content": "content"}, vector=[1.0, 2.0, 3.0]) ids = await collection._inner_upsert([record]) assert ids[0] == "id1" ids = await collection.upsert(records={"id": "id1", "content": "content", "vector": [1.0, 2.0, 3.0]}) assert ids == "id1" async def test_get(collection): records = await collection._inner_get(["id1"]) assert records is not None records = await collection.get("id1") assert records is not None async def test_delete(collection): await collection._inner_delete(["id1"]) async def test_collection_exists(collection): await collection.collection_exists() async def test_ensure_collection_deleted(collection): await collection.ensure_collection_deleted() @mark.parametrize( "collection_to_use, results", [ ( "collection", { "collection_name": "test", "vectors_config": {"vector": VectorParams(size=5, distance=Distance.COSINE, datatype=Datatype.FLOAT32)}, }, ), ( "collection_without_named_vectors", { "collection_name": "test", "vectors_config": VectorParams(size=5, distance=Distance.COSINE, datatype=Datatype.FLOAT32), }, ), ], ) async def test_create_index_with_named_vectors(collection_to_use, results, mock_ensure_collection_exists, request): await request.getfixturevalue(collection_to_use).ensure_collection_exists() mock_ensure_collection_exists.assert_called_once_with(**results) @mark.parametrize("collection_to_use", ["collection", "collection_without_named_vectors"]) async def test_create_index_fail(collection_to_use, request): collection = request.getfixturevalue(collection_to_use) for field in collection.definition.vector_fields: field.distance_function = DistanceFunction.HAMMING with raises(VectorStoreOperationException): await collection.ensure_collection_exists() async def test_search(collection, mock_search): collection.named_vectors = False results = await collection.search(vector=[1.0, 2.0, 3.0], include_vectors=False) async for result in results.results: assert result.record["id"] == "id1" break assert mock_search.call_count == 1 mock_search.assert_called_with( collection_name="test", query_vector=[1.0, 2.0, 3.0], query_filter=None, with_vectors=False, limit=3, offset=0, ) async def test_search_named_vectors(collection, mock_search): collection.named_vectors = True results = await collection.search( vector=[1.0, 2.0, 3.0], vector_property_name="vector", include_vectors=False, ) async for result in results.results: assert result.record["id"] == "id1" break 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=None, with_vectors=False, limit=3, offset=0, ) async def test_search_filter(collection, mock_search): results = await collection.search( vector=[1.0, 2.0, 3.0], include_vectors=False, filter=lambda x: x.id == "id1", ) async for result in results.results: assert result.record["id"] == "id1" break 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")), with_vectors=False, limit=3, offset=0, ) async def test_search_fail(collection): with raises(VectorSearchExecutionException, match="Search requires a vector."): await collection.search(include_vectors=False)