---
description: Qdrant vector search conventions
globs: ["**/*.py","**/*.ts"]
alwaysApply: false
---

# Qdrant

- One `QdrantClient` (or `AsyncQdrantClient` in async apps) per process; URL and API key from env. Local dev via `docker run -p 6333:6333 qdrant/qdrant` pinned to a version.
- Create collections in a setup/migration script, not on the request path: `vectors_config` with size matching the model and `distance=Cosine` (or Dot for normalized vectors).
- Use named vectors when storing more than one embedding (e.g. `dense` and `sparse` for hybrid search with BM25/SPLADE).
- Create payload indexes for every field used in filters (`create_payload_index(field_name="tenant_id", field_schema="keyword")`). Mark the tenant field with `is_tenant=True` for multitenant collections.
- Point ids are UUIDs or integers derived deterministically from `docId + chunk` so re-ingest upserts in place.
- Search with `query_points(..., query=vector, query_filter=Filter(must=[...]), limit=k, with_payload=[...])`; return only needed payload fields.
- Hybrid: use the Query API `prefetch` for dense + sparse and fuse with `FusionQuery(fusion=Fusion.RRF)`.
- Upsert in batches (`upload_points` with `parallel`), `wait=True` only in tests.
- Enable scalar/binary quantization for large collections and rescore for accuracy.

## Don't
- Don't delete collections or drop payload fields in shared environments without approval.

## Done when
Integration tests run against a local Qdrant container and filtered search returns only the caller's tenant data.
