Indexing: HNSW, IVF, Product Quantization
When your vector store grows past a few thousand documents, brute-force nearest-neighbor search becomes a bottleneck. Approximate Nearest Neighbor (ANN) algorithms trade a small amount of recall for orders-of-magnitude speed improvements. This post examines the three most important indexing families—HNSW, IVF, and Product Quantization—and shows how to compose and tune them in FAISS for production RAG systems.
ANN Overview
Exact k-NN requires comparing a query vector against every vector in the dataset. For n vectors of dimension d, that is O(n·d) per query. At 10 million vectors and d=768, each query scans ~7.6 GB of float32 data. ANN algorithms build auxiliary data structures at index time so that queries only visit a fraction of the dataset.
The three families we cover are:
- HNSW — graph-based; high recall, high memory usage
- IVF — partition-based; lower memory, requires training
- PQ — compression-based; minimal memory, lossy
In practice, production systems combine these. For example, IVF4096,PQ64 partitions the space and compresses each partition, yielding a compact and fast index.
HNSW — Hierarchical Navigable Small World
HNSW organizes vectors in a multi-layer navigable graph. The top layers contain a sparse subset of nodes for coarse navigation, while the bottom layer connects every node to its nearest neighbors. A search starts at the top layer, greedily descends, and refines at the bottom layer.
The two critical construction parameters are:
- M — maximum number of bi-directional edges per node at each layer. Higher M increases recall but also memory. Typical values: 16–64.
- ef_construction — size of the dynamic candidate list during build. Higher values produce a better graph but slow down index construction. Typical: 100–500.
At query time, ef_search controls how many candidates the search explores. It must be ≥ k (the number of neighbors requested).
IVF-Flat — Inverted File Index
IVF partitions the vector space into nlist Voronoi cells using k-means clustering. At query time, only the nprobe closest cells are searched, reducing the scan from n to roughly n·(nprobe/nlist) vectors.
Unlike HNSW, IVF requires a training step — you must run k-means on a representative sample before adding vectors.
Rule of thumb: set nlist ≈ sqrt(n) for datasets up to 10M vectors, and nlist ≈ 4·sqrt(n) for larger datasets. Training sample should be at least 30×nlist vectors.
Product Quantization (PQ)
Product Quantization compresses each vector into a compact code by splitting the d-dimensional vector into m sub-vectors and quantizing each sub-vector independently into one of 2nbits centroids. With m=64 and nbits=8, a 768-dim float32 vector (3072 bytes) is compressed to just 64 bytes — a 48× reduction.
The distance between a query and a PQ code is approximated using Asymmetric Distance Computation (ADC): compute exact sub-vector distances from the query to each centroid, then look up and sum. This avoids decompressing the full vector.
Composite Indexes: IVF + PQ
The real power of FAISS comes from composing index types. IVFn,PQm first partitions the space with IVF, then compresses residual vectors within each cell using PQ. This yields fast search (only nprobe cells scanned) and low memory (vectors are PQ-compressed).
FAISS factory strings make it easy to experiment:
Flat— brute force, perfect recallIVF4096,Flat— IVF partitioning, no compressionIVF4096,PQ64— IVF + product quantizationOPQ64,IVF4096,PQ64— optimized PQ rotation + IVF + PQHNSW32— HNSW with M=32IVF4096,HNSW32,PQ64— IVF coarse quantizer is HNSW, cells use PQ
Tuning Parameters for Production
Tuning an index for a production RAG pipeline means finding the sweet spot between recall@k, latency (p99), memory footprint, and index build time. Here is a systematic approach.
Step 1: Establish a ground-truth baseline
Build a IndexFlatL2 (or IndexFlatIP for cosine similarity with normalized vectors) and compute ground-truth top-k results for a query set of 1,000–10,000 representative queries.
Step 2: Sweep nprobe / ef_search
For a fixed index structure, sweep the query-time parameter and plot recall vs QPS. This is cheap and doesn't require rebuilding the index.
Step 3: Memory budget check
Estimate memory for your index type:
- Flat: 4·d·n bytes (float32)
- HNSW(M): 4·d·n + 8·M·n bytes (vectors + graph edges)
- IVF,PQ(m): m·n bytes (PQ codes) + 4·d·nlist (centroids)
Step 4: GPU acceleration
FAISS supports GPU for IVF-family indexes. Wrap your CPU index with index_cpu_to_gpu:
faiss.write_index() and version it alongside your embedding model.
Choosing the right index structure is one of the highest-leverage decisions in a RAG pipeline. HNSW gives you the best recall out of the box, IVF+PQ gives you the best memory efficiency, and composites let you find the Pareto frontier for your specific recall, latency, and memory constraints.
Continue with worked examples
Continue with these focused notes: