AAI Agentic Handbook
Retrieval & Search

Product quantization (PQ)

Staff levelReviewed Oct 2026In 2 interview questions

Compress a vector by splitting it into m sub-vectors and storing each as a 1-byte codebook index: 3,072 bytes become 64, with fast approximate distances.

In plain English

Product quantization (PQ) shrinks vectors so far more of them fit in memory. It cuts each vector into, say, 64 small pieces, and for each piece stores only the number of the closest entry in a learned 'codebook' of 256 typical pieces. Distances are then estimated from those codes using small lookup tables. You lose some accuracy, so systems usually re-check the best candidates with the full vectors.

Analogy

Describing a face as 'nose type 17, eyes type 203, mouth type 4…' from a catalogue of standard features instead of sending the full photo.

Common pitfall

Reporting PQ recall from codes alone and concluding 'quantization is too lossy', or the opposite — shipping without rescoring and losing 10–20 recall points silently. PQ is a candidate generator; always pair it with a rescoring pass and measure after it.

How it actually works

Split a D-dim vector into m sub-vectors of D/m dims; run k-means with 2^nbits centroids (usually 256 → 1 byte) per subspace. A vector becomes m bytes (PQ64 at 768-d → 64 B, 48× smaller than float32). At query time, asymmetric distance computation (ADC) builds an m × 256 table of query-subvector-to-centroid distances once, then each database distance is m table lookups and adds — cache-friendly and SIMD-able (FAISS PQ64x4fs fast-scan uses 4-bit codes in registers). Error comes from subspaces being quantized independently; OPQ learns a rotation first so variance is balanced across subspaces (OPQ64,IVF65536,PQ64). Recall@10 from PQ codes alone is 0.6–0.85 depending on m; rescoring the top 100–1,000 with full-precision vectors from SSD or RAM recovers 0.95+. PQ is almost always combined with IVF (inverted file index) (IVFADC, Jégou et al. 2011) so only a few lists are scanned. DiskANN, ScaNN and Milvus/Zilliz use PQ-style codes in memory with full vectors on disk.

What I'd ship on day one

FAISS OPQ{m},IVF{nlist},PQ{m} with m = D/8–D/12 (64–96 at 768-d), 8-bit codes, train on ≥ 256 × 256 vectors per subspace sample (~1M vectors is safe), rescore the top 200–1,000 with float16 vectors. Use fast-scan (PQ{m}x4fs) when CPU-bound.

Worked example

Clinical literature corpus, 500M passages × 768-d. Float32: 1.5 TB. OPQ96,IVF262144,PQ96: 96 B code + 8 B id ≈ 52 GB → fits one 64 GB node. Query: nprobe 64, ADC over ~120k codes (2 ms CPU), top 1,000 rescored from float16 vectors on NVMe (768 KB/query read, ~3 ms) → recall@10 0.96 vs exact. Without rescoring: 0.81.

In the interview

Frame PQ as a memory/recall dial with a rescoring escape hatch: "PQ codes pick candidates, full vectors decide the order; I size m so codes fit RAM and keep float16 originals on SSD." Expect: how do you pick m? Measure recall@10 after rescoring at m = 32/64/96 on 1,000 queries and pick the smallest m that clears the target.

01768-d float323,072 bytes
02Split into 6412 dims each
03Nearest of 256per sub-codebook
0464 × 1-byte codes48× smaller
05ADC lookup table64 adds/distance
06Rescore top 1,000full vectors
PQ encode → approximate distance via lookup → exact rescoring of a shortlist.

Numbers worth knowing

Compression
48× (3,072 B → 64 B)768-d float32, PQ64 with 8-bit codes
Recall@10 from codes alone
0.6–0.85m = 32–96 at 768-d; rises to 0.95+ after rescoring top 1,000
Rescore cost
~2–5 ms1,000 float16 vectors read from NVMe per query

How to decide

WhenDo this
Vectors > ~5× available RAMIVF-PQ in RAM + full vectors on SSD for rescoring.
Fits in RAM with int8Prefer scalar quantization (Scalar & binary quantization) — simpler, ~99% recall retained.
Recall after rescoring < targetRaise m or rescore depth before raising nprobe; add OPQ.

Cost maths

RAM per vector = m + 8 (id) bytes. 500M × (96 + 8) = 52 GB → one 64 GB node (~$310/month at ~$0.43/hr, illustrative Oct 2026) vs float32 1.5 TB → ~24 nodes (~$7,400/month) per replica. SSD for float16 originals: 768 GB ≈ $60/month at $0.08/GB-month. Training: k-means on 1M samples ≈ minutes on a GPU, < 1 hour on CPU.

Operating it

Time to scale up when…
  • Rescore depth needed for target recall > 2,000 — raise m or switch to int8 SQ if RAM allows
  • SSD read p99 > 10 ms during rescoring — move originals to RAM tier or a faster volume
  • New embedding model (different D) — retrain codebooks; codes are model-specific
What to monitor
  • Recall@10 after rescoring vs exact — alert below 0.95
  • Rescore stage latency p99 — alert above 10 ms
  • Codebook model version vs embedding model version — page on mismatch

When it goes wrong

What users saw

After compressing a 300M-vector research index to save RAM, answer quality fell but retrieval latency improved, so nobody noticed for a week.

How it was caught

Faithfulness on the nightly eval set dropped from 0.88 to 0.79; ANN-vs-exact recall@10 audit read 0.78.

Root cause

The migration switched to IVF,PQ32 without a rescoring stage; final ranking used PQ approximate distances.

Fix

Added float16 rescoring of the top 500 from NVMe and OPQ rotation; recall@10 returned to 0.96 for +3 ms p50. Recall gate added to the index CI.

Sources

  1. Product Quantization for Nearest Neighbor Search — Jégou, Douze & Schmid, 2011paper
  2. The Faiss library — Douze et al., 2024paper
  3. Faiss wiki: The index factory — Meta AI (FAISS), 2026docs

Check yourself

Three questions. Every option is explained once you answer.

1ConceptWhy is distance computation over PQ codes fast enough to scan hundreds of thousands of candidates per query?
2ScenarioAn IVF-PQ index meets latency but recall@10 after rescoring is 0.91 against a 0.95 target. What should you try first, per the page?
3Calculation500M passages at 768-d. PQ uses m = 96 one-byte codes plus an 8-byte id per vector. What is the in-RAM size versus raw float32?