feat(qdrant): provision the chunks collection as an explicit deployment step
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.
This commit is contained in:
52
tests/integration/qdrant/conftest.py
Normal file
52
tests/integration/qdrant/conftest.py
Normal file
@@ -0,0 +1,52 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user