Vector search and indexes
Finding nearest neighbours among millions of vectors
Once documents are vectors, retrieval is nearest-neighbour search (3.4 k-Nearest Neighbors) — exact and trivial at ten thousand vectors, a real systems problem at a hundred million. Knowing how IVF, product quantization and HNSW trade recall for speed and memory, and when a plain Postgres table beats a dedicated vector database, saves you from both over-engineering and silent recall loss. Metadata filtering here is also where access control is enforced (9.9 Production: latency, cost, caching, guardrails, security and privacy).
You'll be able to
- Estimate the cost of exact search and decide when approximate search is necessary
- Explain IVF, product quantization and HNSW, including their key parameters and the recall/latency/memory trade-offs
- Compute index memory for a corpus with and without product quantization
- Choose between a library, a database extension and a dedicated vector database for a given scale and feature set
- Describe how metadata filtering, hybrid search and updates interact with ANN indexes
Key ideas
- Exact (flat) search costs N·d per query and is 100% accurate; it is the right default up to a few million vectors.
- IVF partitions vectors into nlist k-means cells and scans only nprobe of them: cost ≈ nlist·d + nprobe·(N/nlist)·d.
- Product quantization stores m one-byte codes instead of 4d bytes: d = 1024, m = 64 is a 64× memory reduction, with distances computed by m table lookups.
Show 4 more key ideas
- HNSW is a layered proximity graph searched greedily; M sets links per node (memory), efConstruction build quality, efSearch the recall/latency dial at query time.
- Below a few million vectors a Postgres table with pgvector is usually enough; dedicated vector databases pay off at tens of millions of vectors, high QPS or rich filtered search.
- Metadata filters are hard for ANN (post-filtering starves, pre-filtering breaks graphs) — and they are your access-control boundary, so verify how your engine implements them.
- Changing the embedding model invalidates the whole index; store the model version with every vector and plan migrations.
Suggested path through this lesson
Nearest neighbours, again
A query embedding and document vectors : find the largest . This is -nearest neighbours (3.4 k-Nearest Neighbors) in embedding space. Exact (brute-force, "flat") search costs multiply-adds: for and that is about operations — roughly a millisecond on a GPU and tens of milliseconds on a CPU with a good BLAS library — and it is 100% accurate. Flat search is the right choice more often than people think: up to a few million vectors, or whenever recall must be perfect. Beyond that, memory bandwidth and latency force approximate nearest neighbour (ANN) search, which gives up a little recall for orders of magnitude in speed or memory.
Two families dominate: partition-based methods (IVF, with product quantization for compression) and graph-based methods (HNSW).
Inverted file index (IVF)
Run -means (3.11 Clustering: k-means, GMMs/EM, DBSCAN and hierarchical) on the vectors with nlist centroids (typically between and ). Each vector is stored in the list of its nearest centroid. At query time, compare with the nlist centroids, pick the nprobe closest cells, and scan only the vectors in those cells:
With , nlist and nprobe : about instead of — an 80× speed-up. Recall drops when the true neighbour sits in a cell you did not probe (common near cell boundaries); raising nprobe trades speed back for recall.
Product quantization: the memory math
IVF speeds up search but still stores full vectors. Product quantization (PQ) compresses them. Split each -dimensional vector into sub-vectors of dimensions. For each sub-space learn a codebook of centroids with -means. Encode a vector as centroid ids, one byte each:
For and : 4,096 bytes become 64 bytes, a 64× reduction; a billion vectors fit in 64 GB instead of 4 TB. Distances are computed asymmetrically: for a query, precompute a table of distances between each query sub-vector and each centroid; the distance to any stored vector is then the sum of table lookups, with no multiplications per candidate. The price is quantization error, which lowers recall. IVF-PQ, with a final re-scoring of the top candidates using a few full-precision vectors, is how FAISS scales to billions of vectors on GPUs.
HNSW: a navigable graph
Hierarchical Navigable Small World graphs build a multi-layer proximity graph. Every vector is a node in the bottom layer, linked to about near neighbours (up to at layer 0 in common implementations); a geometrically shrinking subset of nodes also appears in higher layers, which act like the express lanes of a skip list. Search starts at the top layer from an entry point, greedily hops to the neighbour closest to , drops down a layer when it cannot improve, and in the bottom layer runs a best-first search that keeps a candidate list of size efSearch. The number of hops grows like and each hop costs a handful of distance computations, so queries take well under a millisecond to a few milliseconds at millions of vectors, with recall above 95% when tuned.
Three knobs:
- M (links per node): higher means better recall and more memory — roughly bytes of neighbour ids per vector at the base layer, on top of the vector itself. Typical values are 16–64.
- efConstruction: candidate-list size while building; higher gives a better graph and a slower build (hundreds is typical).
- efSearch (must be at least ): candidate-list size at query time; the live recall-versus-latency dial.
HNSW's weaknesses are memory (vectors and graph live in RAM, although PQ-compressed and disk-based variants exist) and slow, allocation-heavy builds; deletes are usually tombstones that require periodic rebuilds.
Choosing an index
| Index | Memory | Recall | Query speed | Build | Updates | Use when |
|---|---|---|---|---|---|---|
| Flat (exact) | 100% | slow at scale | none | trivial | up to a few million vectors, or recall must be exact | |
| IVF | + centroids | high (tune nprobe) | fast | k-means | fine | millions of vectors, memory not a concern |
| IVF-PQ | + codebooks | medium to high | very fast | k-means twice | fine | hundreds of millions to billions |
| HNSW | + graph | very high | fastest | slow | inserts fine, deletes costly | up to tens of millions, latency-critical |
| HNSW + PQ / disk-based | compressed | high | fast | slow | limited | large corpora on limited RAM |
Vector databases versus a table in Postgres
A vector database wraps an index with persistence, metadata filtering, hybrid (lexical plus dense) search, replication, multi-tenancy and an API. The options fall into three groups: libraries you embed in your process (FAISS, hnswlib, Annoy, usearch); extensions to databases you already run (pgvector for Postgres with HNSW and IVF-Flat indexes; OpenSearch and Elasticsearch with kNN next to BM25); and dedicated engines or managed services (Qdrant, Weaviate, Milvus, Chroma, Pinecone and others).
Honest guidance: below a few million vectors, a Postgres table with pgvector is usually enough and gives you transactions, joins with your real data and one backup story. Dedicated engines earn their complexity when you need tens of millions of vectors, high query rates, rich filtered search or multi-tenant isolation.
Filtering, hybrid search and updates
Metadata filters ("only documents this user may read, from 2025, in English") interact badly with ANN: post-filtering the top- can return nothing when the filter is selective, and pre-filtering destroys HNSW's graph connectivity. Engines solve this with filter-aware graph traversal, payload indexes, or by switching to flat search inside small filtered subsets — check how yours does it, because access-control filters are a security boundary (9.9 Production: latency, cost, caching, guardrails, security and privacy). Hybrid search keeps a BM25 index next to the vectors and fuses the results (9.5 Retrieval quality: hybrid search, reranking, query transforms and metrics). Updates: new documents are inserted (HNSW handles inserts well), changed documents are re-embedded, deleted documents are tombstoned and compacted; a change of embedding model means re-embedding everything, so record the model version with every vector.
Intuition. An ANN index is a map with highways. IVF draws districts and searches a few of them; PQ shrinks the map by rounding addresses; HNSW builds roads between neighbours and express lanes between regions.
Where this shows up later
- What goes into the index comes from chunking — 9.4 Chunking and ingestion; how you query it (hybrid, reranking) is 9.5 Retrieval quality: hybrid search, reranking, query transforms and metrics.
- Filtering by permissions is a production security control — 9.9 Production: latency, cost, caching, guardrails, security and privacy; the same indexes back agent memory — 10.4 Memory and context engineering.
- Vector search is k-NN (3.4 k-Nearest Neighbors) built on k-means (3.11 Clustering: k-means, GMMs/EM, DBSCAN and hierarchical) and the geometry of 1.1 Vectors, dot products and norms.
Common pitfalls (4)
- Reaching for a managed vector database for 50,000 vectors; a NumPy matrix product or pgvector would be faster to build and easier to run.
- Tuning for speed without measuring recall — ANN indexes silently drop true neighbours when nprobe or efSearch is too small.
- Applying a selective metadata filter after retrieving the top-k, then wondering why results are empty or low quality.
- Upgrading the embedding model and re-embedding only new documents; old and new vectors are not comparable.
Recap: all 7 key points
- Exact (flat) search costs N·d per query and is 100% accurate; it is the right default up to a few million vectors.
- IVF partitions vectors into nlist k-means cells and scans only nprobe of them: cost ≈ nlist·d + nprobe·(N/nlist)·d.
- Product quantization stores m one-byte codes instead of 4d bytes: d = 1024, m = 64 is a 64× memory reduction, with distances computed by m table lookups.
- HNSW is a layered proximity graph searched greedily; M sets links per node (memory), efConstruction build quality, efSearch the recall/latency dial at query time.
- Below a few million vectors a Postgres table with pgvector is usually enough; dedicated vector databases pay off at tens of millions of vectors, high QPS or rich filtered search.
- Metadata filters are hard for ANN (post-filtering starves, pre-filtering breaks graphs) — and they are your access-control boundary, so verify how your engine implements them.
- Changing the embedding model invalidates the whole index; store the model version with every vector and plan migrations.
Watch
The HNSW section walks through skip lists, navigable small worlds and the layered search.
Read
Exercises
- Flat vs IVF vs HNSW
Install
faiss-cpu. Embed (or randomly generate) 200,000 vectors of dimension 384 and 1,000 queries. BuildIndexFlatIP,IndexIVFFlatandIndexHNSWFlat. Using flat results as ground truth, plot recall@10 against queries per second while sweeping nprobe (1–64) and efSearch (16–256). Expected: HNSW reaches above 95% recall at a fraction of flat's latency; IVF needs nprobe well above 1 to match. - Memory budget
For your own corpus (or 50M vectors of dimension 1,024), compute the memory of flat float32 storage, int8 storage, PQ with and , and HNSW with (vectors plus roughly bytes of links per vector). Decide which configuration fits in 32 GB of RAM and what recall you would expect to give up.
- pgvector with filters
Run Postgres with the pgvector extension (Docker is fine). Create a table with an embedding column and a
tenant_idandcreated_at, insert 100k rows, build an HNSW index, and time a nearest-neighbour query with and without a selectiveWHERE tenant_id = ...filter. Read the pgvector README section on filtering and explain what you observed.
Quiz
Flashcards
These cards are now in your spaced-repetition queue. Review all due cards →