Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Product quantization,
explained.
A billion embeddings at full precision is terabytes of memory — too much to search fast. Product quantization is the trick that shrinks them, and it's cleverer than rounding: chop each vector into a few pieces, and replace every piece with the nearest entry from a tiny catalog of typical pieces. Store the catalog indices, not the numbers, and a 512-byte vector becomes 8 bytes — while a handful of small catalogs still tell billions of vectors apart. Here's how, and why the distances stay usable.
What changes against storing vectors: the vectors stop being stored
Rung 3 gives one vector per item, which turns retrieval into nearest-neighbour search and immediately creates a memory problem. Vectors are typically hundreds of dimensions in four-byte floats, so a billion of them is a quantity of memory no single machine has. The obvious fix — keep them on disk — destroys the latency that made the approach attractive.
The change is to not keep the vectors at all, and to keep a short code per vector instead. Naively, quantising a whole high-dimensional vector to a codebook would need astronomically many codewords to be accurate. So split the vector into several contiguous sub-vectors and quantise each one separately with its own small codebook. A vector becomes a list of codebook indices — one small integer per sub-vector.
The reason this is a good idea rather than merely a compact one is combinatorial, and it is the rung's whole argument. With m sub-vectors and k entries per codebook, the scheme can represent k^m distinct vectors while storing only m·k codewords — the expressible set is the product of the parts, and the storage is their sum. That is what buys compression ratios of the order of 64× and beyond. And distances stay fast because a query's distance to each codeword can be precomputed into small lookup tables, so scoring a compressed vector is a handful of table reads and additions, with the query kept at full precision so its own resolution is never thrown away.
The memory wall
Nearest-neighbour search over embeddings — find the vectors closest to a query — is the engine under semantic search, recommendations, and retrieval. At small scale you just store every vector and compare. But a modern embedding might be 128 or 768 dimensions of float32, and a billion of them is hundreds of gigabytes to terabytes. That won't fit in fast memory, and scanning it is slow. The bottleneck is not the math; it's the bytes.
The obvious fix — store fewer bits per number — is crude: uniformly rounding each dimension loses too much accuracy for too little saving. Product quantization does something smarter. It compresses groups of dimensions together by learning what typical groups look like, so each group is replaced by a pointer to its closest learned prototype. The savings are enormous and the distances survive well enough to search — which is exactly the balance vector search needs.
Cut each vector into pieces, replace every piece with the nearest entry from a small learned codebook, and store the tiny indices — huge compression, distances mostly intact.
Split and quantize
The recipe: split a D-dimensional vector into m contiguous subvectors. For each subspace, learn a codebook of k centroids (by k-means over the training data). To compress a vector, replace each subvector with the index of its nearest centroid in that subspace's codebook. The stored representation is just m indices. With the conventional k = 256, each index is one byte:
A 128-dim float vector is 512 bytes; split into m = 8 subvectors with 256-entry codebooks, it becomes 8 bytes — 64× smaller. The subvectors are quantized independently, which is what keeps the codebooks tiny (each covers only D/m dimensions) while still capturing the structure within each slice. The runnable version below computes the compression and the reconstruction.
A huge code space
Here's the beautiful part, and the reason it's called a product quantizer. Each subspace has only k centroids — a small catalog. But a full vector is an independent choice of centroid in every subspace, so the number of distinct vectors the scheme can represent is the product of the per-subspace choices: k^m. Small catalogs, combined, span an astronomically large space.
Eight one-byte codebooks of 256 entries each yield 256⁸ ≈ 1.8 × 10¹⁹ possible codes — far more than any dataset you'd ever index, all from storing 8 bytes. That combinatorial explosion is why product quantization compresses so aggressively without collapsing distinct vectors onto the same code. You get the resolution of an enormous codebook at the storage cost of a tiny one.
Fast distances
Compression is only useful if you can still compare quickly. Product quantization uses asymmetric distance computation: keep the query at full precision, but for each subspace precompute the distance from the query's subvector to every centroid in that subspace's codebook — a small lookup table of m × k numbers, built once per query.
So scoring a billion compressed vectors is a billion sums of m small lookups — cache-friendly and blazing fast. Keeping the query uncompressed (the "asymmetric" part) makes the approximation noticeably more accurate than the symmetric alternative of quantizing the query too, at no extra storage cost.
How IVFADC indexes and searches, step by step
The paper (Jégou, Douze and Schmid, IEEE TPAMI 2011) describes two search systems built on product quantization. ADC scans every compressed vector. IVFADC adds a coarse inverted file so that only a few cells are scanned, and encodes each vector's offset from its cell centre rather than the vector itself. IVFADC is the design FAISS later shipped as IVFPQ, and its procedure is below.
- Train the coarse quantizer. Run k-means on a training sample to get k′ centroids; the paper's million-vector experiments use coarse vocabularies of 1,024 and 8,192 cells. Each centroid owns one inverted list.
- Train the product quantizer on residuals. For each training vector compute its residual r = y − qc(y), the offset from its nearest coarse centroid. Split the residuals into m sub-vectors of D/m dimensions and run k-means separately in each subspace, giving m codebooks of k* = 256 centroids. One product quantizer is shared by every cell.
- Encode the database. Assign each vector y to its nearest coarse centroid, encode its residual with the product quantizer, and append the pair (id, code) to that centroid's list. With m = 8 a code is 8 bytes, so an entry is 12 bytes with a 4-byte id (the paper allows 8 to 32 bits for the identifier); the cell itself costs nothing to store, because the list a vector sits in records it.
- Choose cells at query time. Find the w coarse centroids nearest to the query x — the paper's multiple assignment, which FAISS calls nprobe — and visit only their lists. With k′ = 1,024 and w = 8, a query touches about 0.8% of the database if the cells are balanced.
- Build a distance table per visited cell. For cell i, take the query's residual x − ci, split it into m sub-vectors, and compute the squared distance from each to all 256 sub-centroids of its subspace: an m × 256 table. Each cell needs its own table, because the query residual changes with the cell.
- Scan and rank. For each entry in a visited list, the approximate squared distance is the sum of m table lookups indexed by its code bytes — no decompression, no full-precision arithmetic. A heap keeps the best k across all visited lists, and their ids are returned.
Symmetric versus asymmetric distances
The paper compares two ways to estimate a distance from codes. Symmetric distance computation (SDC) quantizes the query too and reads centroid-to-centroid distances from tables computed once for the whole index. Asymmetric distance computation (ADC) keeps the query exact and builds the m × k* table per query. Both store identical codes. By the triangle inequality, ADC's error is bounded by the database vector's quantization error alone, while SDC's bound adds the query's error as well, and in the experiments ADC gave better recall at every code size for a small extra cost per query. The paper also shows that which dimensions are grouped into a sub-vector affects accuracy, with a random ordering doing worse than one that keeps related dimensions together — the observation that later work turned into learned rotations.
Product quantization's distance estimators
ADC: d̃(x, y)^2 = Σj ‖ uj(x) − qj(uj(y)) ‖^2 SDC: d̂(x, y)^2 = Σj ‖ qj(uj(x)) − qj(uj(y)) ‖^2
| d(x, y) − d(x, q(y)) | ≤ d(y, q(y)) | d(x, y) − d(q(x), q(y)) | ≤ d(x, q(x)) + d(y, q(y))
IVFADC: d̈(x, y)^2 = Σj ‖ uj(x − qc(y)) − qp,j(uj(y − qc(y))) ‖^2
uj(·)— the j-th sub-vector: D/m consecutive dimensions.qj— the k-means quantizer of subspace j, with k* centroids; the code stores only the index.d̃(ADC) — query exact, database quantized: one m × k* table per query, or per cell in IVFADC.d̂(SDC) — both quantized: k* × k* centroid-to-centroid tables per subspace, computed once.qc, qp— in IVFADC, the coarse quantizer and the product quantizer that encodes residuals.
The two bounds are the triangle inequality, and they carry the whole SDC-versus-ADC argument: ADC's estimate is off by at most the database vector's own quantization error, SDC's by both errors added. At the extremes the product quantizer turns into familiar things. With m = 1 it is ordinary k-means with only 256 possible codes, far too coarse for any real collection. With m = D it is a scalar quantizer with 256 levels per dimension — 128 bytes for a 128-dimensional SIFT vector, only a 4× saving. Product quantization is the range in between, where the code space has (k*)^m points but the codebooks cost only m · k* centroids to learn and store.
What product quantization was tested on, and what it showed
The benchmarks were introduced with this work and are still standard: SIFT1M, a million 128-dimensional SIFT descriptors with 10,000 queries and a separate 100,000-vector training set, and GIST1M, about a million 960-dimensional GIST descriptors with 500 queries. Ground truth is the exact Euclidean nearest neighbour, and the measure is recall@R: the fraction of queries whose true nearest neighbour appears in the first R results. Baselines were spectral hashing and Hamming embedding at equal code length, and FLANN, a tree-based method that keeps the full vectors; the abstract reports that the approach outperformed all three. A final experiment scaled IVFADC to two billion vectors.
| Method, SIFT1M, 64-bit codes | Bytes per vector | Codes scored per query | Finding |
|---|---|---|---|
| Raw float32, exhaustive | 512 | 1,000,000 | Exact, and the memory problem |
| SDC, exhaustive | 8 | 1,000,000 | Lowest recall of the PQ variants |
| ADC, exhaustive | 8 | 1,000,000 | Better recall than SDC at every code size |
| IVFADC, k′ = 1,024, w = 8 | 8 + 4-byte id | about 7,800 | Scans roughly 1% of the data if cells are balanced; residual codes are more precise |
| IVFADC, k′ = 8,192, w = 64 | 8 + 4-byte id | about 7,800 | Same scan size with finer cells |
The scan sizes assume balanced cells; real lists vary. Three results carry the paper. ADC beat SDC, as the error bounds predict, for the price of one small table per query. Encoding residuals beat encoding raw vectors, because the coarse centroid already says roughly where a vector is, so the same eight bytes describe a much smaller region. And the number of probed cells mattered: with w = 1 a query loses every true neighbour that fell into an adjacent cell, so recall levels off below 1 however long the code, and raising w buys it back at proportional cost. GIST, at 960 dimensions, was markedly harder than SIFT at equal code length — the cost of squeezing a much richer vector into the same few bytes.
What the authors admit, and what happened next
The method is lossy and that is the deal: the returned neighbours are approximate, ranking near the top can be wrong, and the compression rate trades directly against recall. The paper is clear about this and about the tuning involved, and it is the reason production systems re-rank a candidate set with exact distances — the compressed index narrows, something precise decides.
The independence assumption is the technical weakness: quantising slices separately assumes the slices are not strongly correlated, and in real embeddings they are. Later work addressed this by rotating the space before splitting so that variance is spread evenly across the slices, which measurably improves recall at the same code length and is now standard. A rotation is a free change of basis — a clean case of getting accuracy from geometry rather than from bits.
How it held up: it is inside the infrastructure. The compressed-code index is a standard option in every serious vector store, generally combined with rung 5's graph or with a coarse partitioning so that compression handles memory and structure handles the search. Fifteen years on, the sum-versus-product argument above is still the reason billion-scale search fits on hardware you can rent.
Compress a vector, keep the distances
Product quantization is a few lines of arithmetic. This prices the compression — a 128-dim float vector (512 bytes) stored as 8 one-byte codes, 64× smaller — and confirms the combinatorial code space (256⁸ ≈ 1.8×10¹⁹ from 8 bytes). It then quantizes subvectors to their nearest centroid and scores a full-precision query by asymmetric distance: summing per-subspace lookups, where the correct code lands at distance 0 and a wrong code doesn't. Change m, k, or the codebooks and watch the storage and distances move.
Write the body of asymmetric_distance. Everything else in the file is
already written; this is the function the paper is about. Press ▶ Run to check it against
the 5 assertions below — they start red.
- A PQ code is m bytes at k=256
- Compression ratio is 4D over m bytes
- The code space is k^m
- Nearest_centroid picks the closest centroid
- Asymmetric distance: the correct code beats a wrong one
Why it endures
Product quantization predates the deep-learning embedding boom — it came from image retrieval in 2011 — but it became foundational precisely when everything turned into vectors. When you embed a billion documents, images, or products, you hit exactly the memory wall PQ was built for, and its combination of aggressive compression with fast, decompression-free distances is hard to beat. Libraries like FAISS made it a default, and it lives inside many production vector databases today.
Its lasting idea is compression by learned structure. Generic bit-reduction throws away information blindly; product quantization instead learns what the data actually looks like, subspace by subspace, and spends its bits describing which learned prototype each piece resembles. That "quantize against a learned codebook" pattern recurs across the field — in model weight quantization, in KV-cache compression, and beyond — and this paper is one of its clearest, most enduring expressions.
The same "product of small codebooks" idea powers residual and additive quantization (used to compress LLM weights to a few bits): stack several codebooks and sum their contributions for even finer resolution at low bit-rates.
Why it still matters
Product Quantization (PQ) is the compression trick that makes billion-scale vector search affordable. Storing millions of high-dimensional float vectors in full precision is expensive in memory; PQ shrinks them dramatically by splitting each vector into several sub-vectors and replacing each sub-vector with the id of its nearest entry in a small learned codebook. A vector becomes a short list of codebook indices — often an order of magnitude or more smaller.
The cleverness is that distances can be computed without decompressing. You precompute, for the query, the distance to every codebook entry per sub-space (a small lookup table), then approximate the distance to any database vector by summing table lookups. Search becomes cheap table additions over compact codes instead of full-precision math over full vectors.
The trade-off is accuracy: PQ is lossy, so retrieved neighbors are approximate. In practice it is tuned (number of sub-quantizers, codebook size) and combined with a coarse index (IVF) to prune the search space, then often re-ranked with exact distances on a shortlist — the classic IVF-PQ pipeline.
PQ matters because it is foundational infrastructure under modern vector databases and ANN libraries (FAISS and its kin), which power RAG retrieval, semantic search, and recommendation at scale. Whenever a system searches hundreds of millions of embeddings on a memory budget, some descendant of product quantization is usually doing the heavy lifting.
Questions people ask about Product Quantization
What is residual encoding in IVFPQ, and why does it improve accuracy?
IVFPQ, called IVFADC in the original product quantization paper by Jégou, Douze and Schmid, first assigns each vector to its nearest centroid in a coarse k-means quantizer, then encodes not the vector itself but its residual: the vector minus that centroid. The residual has far less spread than the raw vector, because the coarse centroid has already captured where the vector sits, so the same m bytes of product-quantization code describe a much smaller region and the reconstruction error drops. At query time the query is turned into a residual against each probed centroid, a distance table is built for that residual, and the stored codes are scored by table lookups. The extra cost is one table per probed cell instead of one per query, which is small next to the scan.
What is the difference between asymmetric and symmetric distance computation in product quantization?
Both estimate the distance between a query and a compressed database vector from its product-quantization code. Symmetric distance computation, SDC, quantizes the query as well and reads centroid-to-centroid distances from tables computed once for the index. Asymmetric distance computation, ADC, keeps the query at full precision and builds, for each query, a table of distances from each query sub-vector to every centroid of its subspace, m × k* entries. By the triangle inequality, ADC's error is bounded by the database vector's quantization error alone, while SDC's bound adds the query's own error. In the paper's experiments ADC gave better recall at every code size, storage is identical, and the extra cost is one small table per query, which is why ADC became the default.
How many cells should an IVFPQ index probe?
The number of probed cells, w in the paper and nprobe in FAISS, trades recall for time. With w = 1 a query scans only the list of its nearest coarse centroid, so any true neighbour that landed in an adjacent cell is lost no matter how good the codes are, and recall levels off below 1. Each extra cell adds roughly n / k′ vectors to scan plus one distance table, for n database vectors and k′ cells. The product quantization paper used coarse quantizers of 1,024 and 8,192 cells and probed between 1 and 64 of them on its million-vector sets; both k′ = 1,024 with w = 8 and k′ = 8,192 with w = 64 would scan about 0.8% of a million vectors if cells were balanced; the paper's measured GIST averages were higher. A practical method is to measure recall at a few values of w on held-out queries and stop where it plateaus.
Why is 256 the standard codebook size in product quantization?
With k* = 256 centroids per subspace, every code index fits in exactly one byte, so a vector with m sub-quantizers costs m bytes and the scan reads one byte per lookup. The per-query distance table is m × 256 numbers, a few kilobytes for typical m, which stays in fast cache while millions of codes are scanned. Larger codebooks lower the quantization error for the same number of sub-quantizers, but the tables, the k-means training and the table-building cost per query all grow with k*, and smaller codebooks waste the byte. The paper's experiments settled on 256 centroids per subspace as the standard choice, with m chosen to fit the memory budget, and FAISS kept eight bits per sub-quantizer as its default.
How is OPQ different from product quantization?
Product quantization splits a vector into consecutive slices and quantizes each independently, which works best when the slices are roughly independent and carry similar variance. Real embeddings rarely oblige: some dimensions are correlated and some carry much more variance than others. The original paper already observed that how dimensions are grouped affects accuracy. Optimized Product Quantization, by Ge, He, Ke and Sun in 2013, learns a rotation of the space before splitting, chosen jointly with the codebooks to minimise quantization error, so variance is spread more evenly across subspaces. The codes and the lookup-table search stay exactly the same; only a matrix multiplication is added when encoding vectors and queries. FAISS exposes it as an OPQ pre-transform in front of a PQ or IVFPQ index.
5 questions to test what you just read
Work each one out before you open it — a number, a prediction or a reason, not a definition. Producing the answer is what makes it stick. Your progress is kept on this device.
A 960-dimensional GIST vector in float32 is encoded with m = 8 and k* = 256. How many bytes before and after, and how many dimensions does each sub-quantizer cover?
Show answer
Before: 960 × 4 = 3,840 bytes. After: 8 bytes, a 480× reduction. Each sub-quantizer covers 960 / 8 = 120 dimensions with only 256 centroids, against 16 dimensions per sub-vector for SIFT at the same code length — one reason GIST is the harder benchmark.
For SIFT with m = 8 and k* = 256, how much arithmetic does ADC spend building one query table, and what does each database vector cost after that?
Show answer
The table holds 8 × 256 = 2,048 squared distances, each over 128 / 8 = 16 dimensions: about 32,768 multiply-adds, the cost of 256 full distance computations. Each database vector then costs 8 lookups and 7 additions, so over a million vectors the table is a rounding error.
With k′ = 1,024 cells and w = 8 probes over a million vectors, about how many codes does one query scan, and what does the coarse step cost?
Show answer
1,000,000 × 8 / 1,024 ≈ 7,800 codes if the cells are balanced, about 0.8% of the database. The coarse step compares the query with all 1,024 centroids at full precision and builds 8 residual tables, one per probed cell.
A database vector and the query would each have a quantization error of 0.3. What is the worst-case distance error under ADC and under SDC, and which is used in practice?
Show answer
ADC: at most 0.3, because only the database vector is quantized. SDC: up to 0.6, both errors added. Storage is the same, so ADC is the usual choice; SDC’s only advantage is that its tables are computed once rather than per query.
Why does encoding the residual y − c instead of y make the same 8 bytes more accurate?
Show answer
The coarse centroid already says roughly where y is, so the residual is a small offset with much less spread than y. Sub-quantizers trained on residuals place their 256 centroids over that smaller region, so the gap between a residual and its nearest centroid — the quantization error — shrinks, at no extra storage.
From a word vector to RAG
The longest ladder here, and the most linear: nine papers that carry one idea — text as geometry — from a 2013 word embedding to a graph-structured retrieval system.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Paper Breakdowns
- Attention Is All You NeedThe paper that introduced the Transformer — the architecture underneath GPT, BERT, Claude, and Gemini. Self-attention, Query/Key/Value, multi-head attention, positional encoding, and the real BLEU results, explained in depth with analogies and animated visuals, not just equations.Read →
- Retrieval-Augmented GenerationThe paper behind every "chat with your docs" product. Pair a generator with a neural retriever so the model looks knowledge up instead of memorizing it — dense retrieval, top-k documents, RAG-Sequence vs RAG-Token, and why retrieval cuts hallucination.Read →
- Word2VecHow words became math. Dense word vectors learned from the company a word keeps, so king − man + woman lands near queen. Skip-gram, CBOW, the distributional hypothesis, and the origin of every embedding model powering search and RAG.Read →
- ResNetThe skip connection that made deep learning deep. Why stacking layers made networks worse, and how residual learning let them reach 150+ layers and win ImageNet — the identity shortcut every Transformer still uses inside every block.Read →
- Vision Transformer (ViT)When an image became 16×16 words. Cut a picture into patches, treat each patch as a token, and feed them to a plain Transformer — no convolutions. Patch embeddings, the class token, and why data scale lets attention beat the CNNs that ruled computer vision for a decade.Read →
- Latent DiffusionThe paper behind Stable Diffusion. Compress an image into a small latent space with a VAE, run the diffusion denoising there instead of on pixels for a tens-of-times efficiency win, and steer it with text via cross-attention. The architecture that put text-to-image generation on consumer GPUs.Read →