Qdrant in production
Vector Databases places Qdrant against the field. This is the operational half: the decisions you make once and cannot cheaply undo, and the ones an interviewer probes because they separate reading about a vector database from running one.
Verified 2026-08Verified 2026-08. Rust engine, Apache 2.0, self-hosted or Qdrant Cloud.
The model: points, payload, named vectors
A point is an id, one or more vectors, and a JSON payload. The payload is not metadata bolted on — it is indexed and filterable, which is the whole reason to choose Qdrant over a bare index.
from qdrant_client import QdrantClient, models
client = QdrantClient(url=URL, api_key=KEY)
client.create_collection(
"docs",
vectors_config={
"dense": models.VectorParams(
size=1536, distance=models.Distance.COSINE,
),
},
sparse_vectors_config={"bm25": models.SparseVectorParams()},
)Two vectors in one collection, named. That is how hybrid search works here — dense and sparse live on the same point rather than in two systems you have to keep in sync.
Filtering is the differentiator
Post-filtering (retrieve 100, throw most away) silently returns fewer results
than asked for. Qdrant’s filterable HNSW applies the condition during graph
traversal, so limit=10 with a filter returns ten:
client.query_points(
"docs",
query=embedding,
using="dense",
limit=10,
query_filter=models.Filter(
must=[
models.FieldCondition(
key="tenant_id",
match=models.MatchValue(value=tenant),
),
],
),
)Create a payload index on anything you filter by, or the filter is a scan:
client.create_payload_index(
"docs", "tenant_id",
field_schema=models.PayloadSchemaType.KEYWORD,
)Gotcha:
tenant_idin the payload with an index is the standard multi-tenant pattern, and it is a filter — not an isolation boundary. A bug that drops the filter returns another tenant’s documents. Where isolation must be structural, use a collection per tenant and accept the overhead.
Hybrid search, in one request
client.query_points(
"docs",
prefetch=[
models.Prefetch(query=dense_vec, using="dense", limit=50),
models.Prefetch(query=sparse_vec, using="bm25", limit=50),
],
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=10,
)Each branch retrieves independently and Reciprocal Rank Fusion merges by rank rather than score — which is what makes it work when the two scores are on incomparable scales. Qdrant also offers DBSF when you want score-based fusion. The retrieval-quality argument is in Hybrid search and reranking.
Quantization is the cost lever
Vectors dominate memory: a million 1536-dimension float32 vectors is roughly 6 GB before the index. Quantization trades a little recall for a large reduction:
| Mode | Memory | Use |
|---|---|---|
| None | 1× | small collections |
| Scalar (int8) | ~4× less | the usual default |
| Binary | ~32× less | very large, high-dim, with rescoring |
quantization_config=models.ScalarQuantization(
scalar=models.ScalarQuantizationConfig(
type=models.ScalarType.INT8,
# quantized in RAM, originals on disk
always_ram=True,
),
)always_ram=True with originals on disk is the configuration that matters:
search runs against the small quantized vectors in memory, then rescores the
top candidates against the full-precision ones. You keep most of the recall and
pay a fraction of the RAM.
Measure recall against an exact search before and after — Qdrant will run
one with search_params=models.SearchParams(exact=True), which is the ground
truth to compare against.
Operational facts
- Snapshots are the backup story, per collection or whole-node.
- Distributed mode shards by point id with configurable replication; a single node handles far more than most teams need first.
- gRPC over REST for ingestion —
QdrantClient(prefer_grpc=True)is a measurable difference on bulk upserts. - Upserts are idempotent by point id, so a re-run of a failed ingestion is safe. Deterministic ids from the source document are what make that true.
- Changing the embedding model means re-embedding everything, exactly as in What are embeddings?. Named vectors let you add the new model alongside the old and cut over per query.
Related
Interview angle 6
- “Why Qdrant over pgvector?” - filtering and scale. Filterable HNSW applies conditions during traversal, so a filtered top-10 returns ten rather than whatever survived post-filtering, and quantization keeps large collections in memory. Below roughly 10M vectors on an existing Postgres, pgvector is the cheaper answer.
- “How do you do hybrid search?” - dense and sparse vectors as named vectors on the same point, retrieved in parallel with
prefetchand merged by Reciprocal Rank Fusion in one request. RRF merges by rank, which is what makes incomparable score scales combine sensibly. - “How do you handle multi-tenancy?” - a
tenant_idpayload field with a payload index, filtered on every query. It is a filter, not an isolation boundary — if a dropped filter leaking another tenant’s data is unacceptable, use a collection per tenant. - “Your collection no longer fits in RAM. What do you do?” - scalar int8 quantization with
always_ram=Trueand originals on disk: search the quantized vectors in memory, rescore the top candidates at full precision. Then measure recall against an exact search rather than assuming. - “What’s the trap with filters?” - an unindexed payload field. The filter still works and quietly becomes a scan, so latency degrades with collection size and nothing errors. Create a payload index for every field you filter on.
- “How do you make ingestion re-runnable?” - deterministic point ids derived from the source document. Upserts are idempotent by id, so a failed batch is safe to replay without duplicating.