Why: - The chunks collection needs four named vectors (dense_nomic, dense_openai, sparse, late_interaction) and payload indexes defined at creation time per ADR-0001; sparse/multivector fields cannot be added to an existing collection without recreating it, so schema drift here is expensive. - Creating it at FastAPI startup would mirror the DDL-at-boot anti-pattern ADR-0009 already rejects for Postgres and ADR-0012 rejects for LangGraph's setup(), so it is a deployment step instead. Changes: - src/infrastructure/qdrant/collection.py: ensure_chunks_collection(), idempotent and schema-verifying (raises on dimension/modifier mismatch rather than silently accepting a misconfigured collection). - src/cli/qdrant_bootstrap.py: the operator entry point (python -m src.cli.qdrant_bootstrap). - QdrantSettings gains collection/upsert_batch_size/upsert_concurrency. Impact: - Deployments must run the new bootstrap command before the first upload; see ADR-0001's new "Collection provisioning" section.
53 lines
1.9 KiB
Python
53 lines
1.9 KiB
Python
import uuid
|
|
from collections.abc import AsyncIterator, Iterator
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from qdrant_client import AsyncQdrantClient
|
|
from testcontainers.community.qdrant import QdrantContainer
|
|
|
|
from src.config import QdrantSettings
|
|
from src.infrastructure.qdrant.client import create_client
|
|
|
|
# Pinned to match the `qdrant-client` major/minor in pyproject.toml. The
|
|
# testcontainers default image trails it far enough that the client emits an
|
|
# incompatibility warning, and testing against a version we do not deploy is
|
|
# the wrong signal anyway.
|
|
_QDRANT_IMAGE = "qdrant/qdrant:v1.19.0"
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def qdrant_container() -> Iterator[QdrantContainer]:
|
|
with QdrantContainer(image=_QDRANT_IMAGE) as container:
|
|
yield container
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def qdrant_url(qdrant_container: QdrantContainer) -> str:
|
|
"""The container's REST URL, pinned to IPv4.
|
|
|
|
Same gotcha as postgres_url/minio_settings (see their conftests):
|
|
testcontainers reports the host as `localhost`, which resolves to `::1`
|
|
first, but Docker publishes the mapped port on IPv4 only. The IPv6 SYN is
|
|
dropped rather than refused, so the client hangs until its timeout instead
|
|
of falling back to the second address -- the connection does not fail, it
|
|
hangs.
|
|
"""
|
|
host = qdrant_container.get_container_host_ip().replace("localhost", "127.0.0.1")
|
|
return f"http://{host}:{qdrant_container.get_exposed_port(6333)}"
|
|
|
|
|
|
@pytest.fixture
|
|
def qdrant_settings(qdrant_url: str) -> QdrantSettings:
|
|
"""Settings naming a collection unique to this test (ADR-0016 isolation)."""
|
|
return QdrantSettings(url=qdrant_url, collection=f"chunks_{uuid.uuid4().hex}")
|
|
|
|
|
|
@pytest_asyncio.fixture(loop_scope="session")
|
|
async def qdrant_client(qdrant_settings: QdrantSettings) -> AsyncIterator[AsyncQdrantClient]:
|
|
client = create_client(qdrant_settings)
|
|
try:
|
|
yield client
|
|
finally:
|
|
await client.close()
|