Files
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

342 lines
11 KiB
Python

# 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)