AI & ML / RAG & embeddings / 12_qdrant_in_production.md

Qdrant in production

Updated 6 interview angles 4 min read source
On this page7
  1. The model: points, payload, named vectors
  2. Filtering is the differentiator
  3. Hybrid search, in one request
  4. Quantization is the cost lever
  5. Operational facts
  6. Related
  7. Interview angle

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-08

Verified 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.

python
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:

python
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:

python
client.create_payload_index(
    "docs", "tenant_id",
    field_schema=models.PayloadSchemaType.KEYWORD,
)

Gotcha: tenant_id in 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

python
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 small collections
Scalar (int8) ~4× less the usual default
Binary ~32× less very large, high-dim, with rescoring
python
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.

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 prefetch and 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_id payload 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=True and 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.