Production patterns for Qdrant vector search — distilled from operating 380M+ vectors in production. Every pattern is a runnable file; every claim below has a benchmark script you can re-run.
On this dataset m=16 strictly dominates m=32: same recall, ~2× lower latency, 2.7× faster index build (72 s vs 195 s) and half the graph RAM. ef=32 already hits 99.8% recall@10. The lesson that saves real money: measure before you raise m — defaults are better than folklore.
| ef=16 | ef=32 | ef=64 | ef=128 | ef=256 | |
|---|---|---|---|---|---|
| m=16 recall@10 | 0.975 | 0.998 | 1.000 | 1.000 | 1.000 |
| m=16 p50 | 2.6 ms | 2.4 ms | 2.6 ms | 3.6 ms | 5.2 ms |
| m=32 recall@10 | 0.982 | 0.999 | 1.000 | 1.000 | 1.000 |
| m=32 p50 | 3.6 ms | 5.1 ms | 5.2 ms | 5.6 ms | 8.3 ms |
200k clustered vectors, 384d, cosine, single local Qdrant, recall vs exact ground truth. Reproduce: benchmarks/hnsw_recall_latency.py
Exact search scores every candidate that survives the filter — its cost grows linearly with filter width, while ANN stays flat. This chart is a warm-cache laptop run; in production with a cold page cache each scored candidate can be a disk read (~0.1 ms), which turns a "small radius" geo query over a big metro area into a multi-second stall. That failure mode is invisible until traffic hits a dense region. Reproduce: benchmarks/exact_vs_ann.py
| File | What it covers |
|---|---|
01_hnsw_tuning.py |
HNSW parameter selection, RAM estimation, ef at search time |
02_geo_filtered_search.py |
Geo-filtered ANN search — payload geo index + radius filter |
03_batch_upsert.py |
Bulk upload with chunking, retry and backpressure |
04_snapshot_s3.py |
Collection backup: snapshot → download → S3 → restore |
05_payload_indexing.py |
Keyword/integer payload indexes for fast filtered search |
pip install -r requirements.txt
docker run -d -p 6333:6333 qdrant/qdrant
python patterns/01_hnsw_tuning.py # any pattern is standalone
python benchmarks/hnsw_recall_latency.py # ~10 min: full recall/latency gridGeo index before search — Qdrant evaluates payload filters before touching the HNSW graph. Without a geo index, geo filtering is an O(n) scan. With it, candidates are resolved in the index first, and ANN runs only on them.
Snapshots over re-embedding — a snapshot of a live collection takes seconds and restores 10–100× faster than re-embedding. Back up daily; seed new nodes from snapshots, not from the embedding pipeline.
Payload indexes are selective — index only fields you filter on. Every index costs RAM and write amplification; high-cardinality ID fields don't benefit.
RAM budget formula — HNSW graph memory ≈ m × 8 × num_vectors bytes on top of the vectors themselves. At 380M vectors the difference between m=16 and m=32 is ~49 GB of RAM. See docs/hnsw_guide.md.
Qdrant 1.9+, Python 3.12+

