HNSW, IVF, PQ, scalar & binary quantisation; pgvector, Qdrant, Weaviate, LanceDB, Pinecone, Milvus — the full decision space for 1 k to 1 B vectors.
Vector databases are not a single category. They differ fundamentally on four axes: storage architecture, indexing algorithm, filter capability, and operational model. Before choosing, classify your workload:
Pinecone, Weaviate Standalone, Qdrant. Purpose-built. Best recall and filter latency. No SQL. Operational complexity: separate service, separate backup, separate auth.
LanceDB (DuckDB lineage), ChromaDB, Faiss (raw library). Zero extra infra. LanceDB stores data in Parquet via Lance format on S3/GCS — serverless-friendly. Faiss is a library, not a server.
pgvector (HNSW + IVFFLAT), pgvecto.rs (Rust, faster build). Use if you already run Postgres — one less service, full ACID, SQL JOINs across metadata and vectors. 10–20% slower recall than native vector DBs at >1 M vectors.
Milvus 2.x is the open-source choice for >100 M vectors: disaggregated storage/compute via S3 + etcd + Pulsar, GPU-accelerated IVF-PQ and RAFT-CAGRA indexes, horizontal scaling to billions of vectors. Zilliz Cloud is the managed version. If you are below 10 M vectors and don't need GPU indexing, Milvus's operational complexity is not warranted.
All ANN algorithms trade recall (fraction of true nearest neighbours returned) against latency and memory. The tradeoff surface has a characteristic knee; good production systems sit just above the knee.
| Algorithm | Index build time | Query latency | Memory | Incremental adds? |
|---|---|---|---|---|
| IVF-Flat | Fast (k-means) | Medium — scans nprobe clusters | Full float32 vectors | Yes, but nlist rebuild recommended |
| IVF-PQ | Medium (k-means + PQ training) | Fast — compressed distance | 8–32× compression vs float32 | Yes |
| HNSW | Slow (graph build O(N log N)) | Very fast — graph traversal | Full vectors + graph overhead (~50%) | Yes, amortised O(log N) |
| Scalar quant (SQ8) | Fast | Fast — int8 dot-product | 4× compression vs float32 | Yes |
| Binary quant | Very fast | Very fast — popcount | 32× compression | Yes |
Qdrant's binary quantisation compresses each float32 dimension to a single bit (sign only). The 32× size reduction means the entire index may fit in RAM. Precision loss is recovered via rescoring: ANN returns 4× more candidates from binary search, then full-precision float32 vectors are loaded for the final scoring pass. Net result: >40× memory reduction with <5% recall drop at >96% recall@10 on most benchmarks.
HNSW (Hierarchical Navigable Small World, Malkov & Yashunin 2016) is a layered proximity graph. Understanding its three key parameters is essential for production tuning.
| Parameter | Effect | Typical range | Rule of thumb |
|---|---|---|---|
M | Max outgoing edges per node. Controls graph connectivity and memory overhead. | 8–64 | 16 for general retrieval; 32+ for high-recall applications |
ef_construction | Size of the dynamic candidate list during index build. Higher = better graph, slower build. | 100–400 | 200 is a safe default; double for production quality |
ef_search | Size of the dynamic candidate list during query. Controls the recall–latency tradeoff at query time. | 50–400 | Start at 2×k (where k is the number of results wanted) |
HNSW memory ≈ N × (dim × 4 bytes + M × 2 × 8 bytes). For 1 M documents at 768 dims and M=16: 1M × (3072 + 256) bytes ≈ 3.2 GB. The graph pointer overhead is around 8% at M=16; at M=64 it rises to 25%. pgvector's HNSW uses hnsw.m and hnsw.ef_search as session parameters.
Inverted File Index (IVF) partitions the vector space into Voronoi cells via k-means. Product Quantisation (PQ, Jégou et al. 2011) compresses each vector by splitting it into M sub-vectors and quantising each sub-vector against a codebook of 256 centroids, yielding M bytes per vector.
| Scenario | nlist | nprobe | PQ bytes | Recall@10 | RAM for 100M × 768d |
|---|---|---|---|---|---|
| Fast/low recall | 4096 | 32 | 64 | ~80% | ~6.4 GB |
| Balanced | 4096 | 128 | 96 | ~92% | ~9.6 GB |
| High recall | 8192 | 256 | 192 | ~97% | ~19.2 GB |
| Baseline (IVF-Flat) | 4096 | 128 | n/a (full) | ~99% | 307 GB |
FAISS uses a compact DSL to specify index types. IVF4096,PQ96 means IVF with 4096 clusters and PQ with 96 bytes. IVF4096_HNSW32,PQ96 uses an HNSW index over the centroids instead of exhaustive centroid search — faster for nlist > 1000. OPQ96_192,IVF4096,PQ96 adds an orthogonal rotation preprocessing step that improves PQ reconstruction accuracy by ~1–2 NDCG points at no runtime cost.
Most production RAG queries include at least one metadata filter: user_id, doc_type, date_range, tenant_id. The strategy for applying these filters determines recall and latency far more than the ANN algorithm choice.
Run ANN unrestricted; filter results afterward. Simple to implement. Fails when selectivity is high (e.g. 0.1% of documents match filter): ANN returns k results, all may be filtered out, requiring fallback to brute-force scan. Never use for high-selectivity filters.
Fetch all doc IDs matching the metadata filter; run exact nearest-neighbour only within that set. Correct by definition. Expensive when the matching set is large. Qdrant's payload indexes (B-tree or hash) make this efficient for selectivities > 10%.
Qdrant's implementation: traverse the HNSW graph normally, but skip nodes that fail the payload filter. Requires that the filter predicate be checkable in O(1). Best of both worlds when filter selectivity is 1–50%. Configurable via m_payload_m and full_scan_threshold.
from qdrant_client import QdrantClient, models
client = QdrantClient("localhost", port=6333)
results = client.query_points(
collection_name="docs",
query=query_vector, # 1024-dim float list
query_filter=models.Filter(
must=[
models.FieldCondition(
key="tenant_id",
match=models.MatchValue(value="acme-corp"),
),
models.FieldCondition(
key="doc_date",
range=models.Range(gte=1704067200), # 2024-01-01 Unix
),
]
),
limit=20,
with_payload=True,
)
Most vector DBs are append-optimised. Understanding the mutation model prevents data-loss incidents and painful re-index operations.
| System | Delete strategy | Update strategy | Snapshot / backup |
|---|---|---|---|
| Qdrant | Soft-delete via tombstone in the WAL; compacted on segment merge. O(1) delete API. | Upsert by ID: delete old + insert new in one atomic operation. | PUT /collections/{name}/snapshots → tar.gz. Supports S3 upload directly. |
| Weaviate | Immediate delete; segment compaction in background (HNSW lazy delete). | PUT /v1/objects/{id}: full document replacement. | Backup API to S3/GCS/Azure Blob; incremental since v1.23. |
| Milvus | Mark-and-compact; auto-compaction threshold configurable. | Delete + insert (no in-place). Use upsert() API in pymilvus ≥ 2.3. | Milvus Backup tool; etcd + S3 for metadata + segments. |
| pgvector | Standard Postgres MVCC. No special handling. | Standard UPDATE. HNSW index updated in place; VACUUM reclaims space. | Standard Postgres WAL / pg_dump / PITR. |
| LanceDB | Append-only Lance format; deletions written as separate delete files merged lazily. | table.merge_insert() in LanceDB 0.5+. | Lance files are Parquet-like; back up with standard object-store sync. |
HNSW does not support efficient in-place deletion. Qdrant and Weaviate both use a lazy strategy: deleted node IDs are tombstoned in memory and the graph retains stale edges until a segment rewrite is triggered. If you delete >20% of a collection, trigger compaction manually or your recall will degrade as searches traverse dead nodes. Qdrant: POST /collections/{name}/points/delete + monitor optimizers_status.
pgvector (v0.7, 2024) added HNSW support, making it a serious option for <5 M vector workloads already running Postgres. The two index types behave very differently under load.
Build: CREATE INDEX ... USING ivfflat (embedding vector_cosine_ops) WITH (lists=256). Fast build, slow query (scan probes lists). Set SET ivfflat.probes = 10 per session. Recommend building after bulk load: SET maintenance_work_mem = '2GB'. Rule of thumb: lists = sqrt(N) for N ≤ 1 M; lists = N/1000 for N > 1 M.
Build: CREATE INDEX ... USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64). Slower build, faster and higher-recall query. Uses hnsw.ef_search. Memory during build: pgvector's HNSW build requires ~2–4 GB for 1 M × 768d. Set maintenance_work_mem = '4GB'. Index is stored in Postgres heap pages — no separate file.
-- Extension + table
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
id bigserial PRIMARY KEY,
content text,
metadata jsonb,
embedding vector(768)
);
-- Build HNSW index after bulk load
SET maintenance_work_mem = '4GB';
CREATE INDEX docs_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m=16, ef_construction=128);
-- Query: top-10 nearest with metadata filter
SET hnsw.ef_search = 80;
SELECT id, content, 1 - (embedding <=> '[...]'::vector) AS score
FROM documents
WHERE metadata->>'tenant' = 'acme'
ORDER BY embedding <=> '[...]'::vector
LIMIT 10;
There is no universally best choice. The right system depends on scale, latency SLA, filter complexity, and what you already run in production.
| System | Sweet spot | Index types | Managed option | Filter quality | Notable gap |
|---|---|---|---|---|---|
| pgvector | <5 M; already on Postgres | HNSW, IVFFLAT | RDS/Supabase/Neon | Full SQL WHERE | Slower than native VDBs; no binary quant |
| Qdrant | 1 k – 100 M; strong filter needs | HNSW + binary/scalar quant | Qdrant Cloud | Excellent (payload index) | No GPU indexing |
| Weaviate | 1 k – 50 M; multi-modal, hybrid | HNSW, flat | Weaviate Cloud | Good (BM25 hybrid built-in) | Heavier RAM footprint |
| LanceDB | Serverless; S3-native; analytics | IVF-PQ, HNSW | LanceDB Cloud | SQL via DuckDB | No real-time sub-millisecond queries |
| Pinecone | Zero-ops; variable load | Proprietary (HNSW-like) | SaaS only | Metadata filters | Vendor lock-in; no self-host |
| Milvus | >100 M; GPU workloads | IVF-PQ, HNSW, GPU RAFT | Zilliz Cloud | Good (scalar index) | Operational complexity |
For teams starting fresh: use pgvector on your existing Postgres. When you hit >5 M documents or need binary quantisation, migrate to Qdrant. LanceDB is the right default for data-science teams using DuckDB and storing data on S3. Pinecone is justified only if you have strict zero-ops requirements and can accept the cost and lock-in.
Deck 03 covers hybrid search — combining BM25 sparse retrieval with dense vectors via Reciprocal Rank Fusion — and reranking pipelines including BGE-reranker-v2-m3, Cohere Rerank 3.5, and ColBERT-as-reranker via PLAID.