Module 3 of 10
Module 3Algorithm Architecture

ANN Indexing Algorithms: HNSW, IVF & Product Quantization

Explore how modern vector databases achieve sub-5ms search across millions of high-dimensional vectors using skip-list graph networks, Voronoi cell clustering, and sub-vector quantization.

1. The Need for Approximate Nearest Neighbors (ANN)

An Exact Nearest Neighbor (kNN / Flat) index compares the query vector against every single vector in the database sequentially (exhaustive linear scan). While recall is 100% perfect, its operational cost is prohibitive for real-time production:

  • Query Latency: Scanning 5 million vectors of 1,536 dimensions takes 1,200ms to 4,000ms on multi-core CPUs.
  • Throughput (QPS): A single server can process fewer than 2 queries per second before saturating all CPU cores.

Approximate Nearest Neighbors (ANN) algorithms bypass exhaustive scans by organizing vectors into geometric data structures (graphs, clusters, trees). By accepting an imperceptible recall trade-off (e.g., retrieving 97% to 99% of the true closest documents), ANN algorithms reduce query time to sub-5ms with logarithmic O(log N) complexity.

2. HNSW: Hierarchical Navigable Small World Graphs

HNSW is the gold standard vector index used by Pinecone, ChromaDB, Qdrant, Weaviate, and PostgreSQL pgvector. It combines the logarithmic navigation of Skip-Lists with the cluster connectivity of Navigable Small World (NSW) graphs.

The Multi-Layer Graph Architecture

HNSW maintains a hierarchy of graph layers, where higher layers have fewer nodes and longer link distances:

  • Top Layer (Layer L): Contains a sparse subset of vectors connected by long-range links. Search begins here with massive "express train" jumps across the vector space.
  • Intermediate Layers: Progressively more dense nodes with shorter link lengths. When greedy routing cannot find a closer neighbor on the current layer, the search drops down to the layer below.
  • Bottom Layer (Layer 0): Contains 100% of all vectors connected by local nearest-neighbor edges. Here, fine-grained local navigation finds the final top-K nearest candidates.
Critical HNSW Tuning Parameters:
ParameterStandard RangeImpact on Performance & Memory
M (Max Edges per Node)16 to 64 (Default: 16)Higher M increases recall on complex datasets, but increases RAM usage and index build time.
efConstruction64 to 512 (Default: 128)Size of candidate exploration list during index creation. Higher values create a more accurate graph at the expense of slower build time.
efSearch (Query Time)16 to 256 (Default: 64)Size of dynamic candidate queue during search. Directly controls trade-off between search latency and retrieval recall.

3. IVF: Inverted File Index & Voronoi Partitioning

While HNSW stores the entire graph in RAM, IVF (Inverted File Index) is a cluster-based index ideal for large-scale datasets with tighter memory budgets:

  1. Clustering Phase (Offline Training): The database runs k-Means clustering over vector samples to compute nlist centroids, partitioning the vector space into Voronoi cells.
  2. Posting Lists: Each vector is assigned to its closest centroid. Vectors belonging to the same centroid are stored together in an inverted list.
  3. Query Phase (Online Search): The query vector is compared against all nlist centroids. The database selects the top nprobe closest centroids, and only searches through the vectors stored within those specific lists.
The nprobe Trade-Off: If nprobe = 1, search is instantaneous, but if the true nearest neighbor lies just across a Voronoi boundary in an adjacent cell, it will be missed. Setting nprobe = 10 to 32dramatically boosts recall while remaining far faster than exhaustive scanning.

4. Product Quantization (PQ): Compressing Vector RAM by 90%

Storing 10 million vectors of 1,536 float32 values requires:

10,000,000 vectors × 1,536 floats × 4 bytes = 61.44 GB RAM

Adding HNSW graph edges increases this to over 90 GB of RAM. Product Quantization (PQ)solves this memory explosion through lossy vector compression:

  • Sub-Vector Slicing: A 1,536-dimensional vector is divided into m = 96 sub-vectors of 16 dimensions each.
  • Codebook Quantization: For each sub-vector subspace, k-Means builds a codebook of 256 centroids (represented by an 8-bit byte: 0 to 255).
  • 8-Bit Byte Indexing: The original 6,144-byte vector is compressed into 96 bytes (a 98.4% reduction!).

5. Indexing Strategy Decision Matrix

Index TypeRecall AccuracyQuery LatencyRAM FootprintBuild / Training Time
Flat (Exact kNN)100% (Baseline)Very Slow (O(N))Low (Raw vectors only)Zero (Instant insertion)
HNSW98% - 99.8%Ultra-Fast (sub-3ms)High (Vectors + Graph edges)Moderate
IVFFlat92% - 97%Fast (5ms - 15ms)Low (Inverted lists)Requires k-Means training
IVF-PQ85% - 94%Very Fast (3ms - 8ms)Extremely Low (90% savings)Longer (Clustering + Quantization)

6. Hands-On: Building HNSW and IVF Indexes with Faiss

Here is a practical Python demonstration using Meta's faiss library to compare HNSW and IVF indices:

import numpy as np
import faiss

DIMENSION = 512
NUM_VECTORS = 50000
K_NEIGHBORS = 5

np.random.seed(42)
data_vectors = np.random.random((NUM_VECTORS, DIMENSION)).astype(np.float32)
query_vector = np.random.random((1, DIMENSION)).astype(np.float32)

# Normalize for Cosine Similarity
faiss.normalize_L2(data_vectors)
faiss.normalize_L2(query_vector)

# --- 1. Building an HNSW Index ---
M = 32  # Number of bi-directional links per node
index_hnsw = faiss.IndexHNSWFlat(DIMENSION, M, faiss.METRIC_INNER_PRODUCT)
index_hnsw.hnsw.efConstruction = 128
index_hnsw.hnsw.efSearch = 64

index_hnsw.add(data_vectors)
distances_hnsw, indices_hnsw = index_hnsw.search(query_vector, K_NEIGHBORS)

print("=== HNSW Search Results ===")
for rank, (idx, dist) in enumerate(zip(indices_hnsw[0], distances_hnsw[0]), 1):
    print(f"Rank {rank}: Vector ID {idx} | Cosine Sim: {dist:.4f}")

# --- 2. Building an IVFFlat Index ---
NLIST = 100  # Number of Voronoi clusters
quantizer = faiss.IndexFlatIP(DIMENSION)  # Flat Inner Product for centroids
index_ivf = faiss.IndexIVFFlat(quantizer, DIMENSION, NLIST, faiss.METRIC_INNER_PRODUCT)

# IVF requires training to learn centroid coordinates
index_ivf.train(data_vectors)
index_ivf.add(data_vectors)

index_ivf.nprobe = 16  # Probe 16 out of 100 clusters
distances_ivf, indices_ivf = index_ivf.search(query_vector, K_NEIGHBORS)

print("\n=== IVFFlat Search Results ===")
for rank, (idx, dist) in enumerate(zip(indices_ivf[0], distances_ivf[0]), 1):
    print(f"Rank {rank}: Vector ID {idx} | Cosine Sim: {dist:.4f}")
Next Chapter

ChromaDB Embedded Local DB

Continue to the next advanced retrieval and vector search engineering module.

Continue Learning →

Explore Related Tools

Hand-picked utilities and calculators related to this tool.

Related Tools