Product Quantization: Vector Compression for ANN Search › Approximate Distance Computation

Michael BrenndoerferJanuary 28, 202612 min read
Language AI HandbookAll chapters
Part 44 of 67Retrieval-Augmented Generation
Chapter 341Product Quantization: Vector Compression for ANN Search › Approximate Distance Computation
Reading section 4 of 12 · Approximate Distance Computation · 12 minSection 4 of 12 · 12 min

Book contents

Section 4 of 12 · 12 min read

Product Quantization: Vector Compression for ANN Search › Approximate Distance ComputationOne section · Sections in this chapter4 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Encoding achieves compression, but the real payoff comes from how PQ enables fast approximate distance computation at query time. Without a clever distance estimation strategy, compressed codes would be useless: you would have to decompress every vector back to full precision before comparing it to a query, negating all the memory savings. PQ avoids this by exploiting the subspace decomposition to precompute everything that depends on the query, so that the per-vector comparison reduces to a handful of integer-indexed memory reads.

The central design choice in PQ distance computation is to invest computation upfront, before scanning the database, and then amortize that investment across every subsequent comparison. This is the same philosophy behind building an index: you do expensive work once so that repeated queries are cheap. The lookup table construction is the "index build" of the distance computation, and it reduces the inner loop from hundreds of floating-point multiplications to a small number of addition operations.

Think of the lookup table as a price list prepared before going shopping. Instead of computing the cost of each item from scratch as you encounter it in the store, you write down the price of each category of item before you enter. Once inside, you just look up prices and add them, without doing any arithmetic. PQ's lookup tables serve exactly this purpose: they precompute the cost (distance contribution) of every possible centroid assignment, so the inner loop is nothing but lookups and additions.

The key insight is that the query is fixed for an entire search, but the database vectors are what you scan. Any computation that depends only on the query can be done once and reused for every database vector. The subspace decomposition makes this possible: the per-subspace query-to-centroid distances depend only on the query and the codebook, neither of which changes during a scan. So you build the lookup tables once and then spend zero arithmetic in the inner loop.

Asymmetric Distance Computation

When a query vector qq arrives, you want to estimate ∥q−xi∥2\| q - x_i \|^2 for each database vector xix_i. If both qq and xix_i were quantized, you would call this Symmetric Distance Computation (SDC). But PQ uses a smarter approach called Asymmetric Distance Computation (ADC) that keeps the query in full precision.

The key insight that makes ADC possible is that squared Euclidean distance decomposes across subspaces exactly when the subvectors partition the dimensions without overlap. This follows from the fact that for non-overlapping index sets S1,S2,…,SMS_1, S_2, \ldots, S_M that together cover all dimensions, ∥u∥2=∑m=1M∥u(m)∥2\|u\|^2 = \sum_{m=1}^{M} \|u^{(m)}\|^2 where u(m)u^{(m)} is the restriction of uu to the dimensions in SmS_m. Applied to u=q−xiu = q - x_i, this gives the exact decomposition of squared Euclidean distance into a sum of per-subspace squared distances:

∥q−xi∥2=∑m=1M∥q(m)−xi(m)∥2\| q - x_i \|^2 = \sum_{m=1}^{M} \left\| q^{(m)} - x_i^{(m)} \right\|^2

where:

  • qq: the full query vector in Rd\mathbb{R}^d
  • xix_i: the ii-th database vector in Rd\mathbb{R}^d
  • q(m)q^{(m)}: the mm-th subvector of qq, a slice of dimensions corresponding to subspace mm
  • xi(m)x_i^{(m)}: the mm-th subvector of xix_i
  • MM: the number of subspaces, where the sum telescopes the full distance into independent per-subspace distances

This decomposition holds exactly because the subvectors partition the dimensions with no overlap, so the squared norms add up to the total squared norm. This is the key property that makes PQ work. If the subspaces shared dimensions, the decomposition would fail and you could not independently precompute per-subspace distances. The non-overlap requirement is therefore a mathematical necessity rather than a convenience: the entire ADC framework rests on this identity, and any architecture that violates it would require fundamentally different distance estimation logic.

Instead of computing ∥q(m)−xi(m)∥2\| q^{(m)} - x_i^{(m)} \|^2 exactly (which would require decompressing xix_i), you approximate it as ∥q(m)−cm,km(i)∥2\| q^{(m)} - c_{m, k_m^{(i)}} \|^2, the distance from the query's mm-th segment to the centroid that represents xix_i's mm-th segment. The approximation is good whenever the centroid is close to the actual subvector it stands in for, which is precisely what the k-means training is designed to ensure.

Before scanning the database, you precompute a lookup table for each subspace. For each subspace mm and each centroid index kk, you store:

Tm[k]=∥q(m)−cm,k∥2for all k∈{0,…,K−1}T_m[k] = \left\| q^{(m)} - c_{m,k} \right\|^2 \quad \text{for all } k \in \{0, \ldots, K-1\}

where:

  • Tm[k]T_m[k]: the precomputed squared distance from the query's mm-th subvector to the kk-th centroid of subspace mm
  • q(m)q^{(m)}: the mm-th subvector of the query, kept in full float32 precision
  • cm,kc_{m,k}: the kk-th centroid in the codebook for subspace mm
  • KK: the number of centroids per subspace (typically 256, one per possible byte value, since each code fits in a single byte)

This gives you MM tables, each with KK entries. Building all tables costs O(M⋅K⋅d∗)O(M \cdot K \cdot d^*) operations, which for typical parameters is in the tens of thousands of multiplications. Think of each table TmT_m as a translation layer: given any centroid index for subspace mm, it instantly tells you how far the query is from that centroid, without any further arithmetic. Once these tables are built, the expensive floating-point work is entirely finished. Every subsequent comparison against a database vector is purely a matter of reading integers and summing precomputed values.

Now, the approximate distance to any database vector xix_i (stored as code (k1(i),…,kM(i))(k_1^{(i)}, \ldots, k_M^{(i)})) is found by summing the precomputed table entries corresponding to each centroid index. No floating-point arithmetic is needed at this stage because all the expensive distance computations happened during the table-building step. The approximate distance is therefore:

d^(q,xi)=∑m=1MTm ⁣[km(i)]\hat{d}(q, x_i) = \sum_{m=1}^{M} T_m\!\left[k_m^{(i)}\right]

where:

  • d^(q,xi)\hat{d}(q, x_i): the approximate squared distance from query qq to database vector xix_i
  • Tm[⋅]T_m[\cdot]: the precomputed lookup table for subspace mm (built once per query)
  • km(i)k_m^{(i)}: the centroid index stored in the mm-th byte of xix_i's compressed code
  • ∑m=1M\sum_{m=1}^{M}: the summation operator that accumulates MM table lookups, replacing all dd floating-point multiplications with MM integer-indexed array lookups. This is the key computational saving: instead of operating on raw floats, we simply add precomputed values

This requires MM table lookups and MM additions. For M=8M = 8, each distance computation requires 8 array lookups and 8 additions. This is enormously faster than computing a full dot product over 768 dimensions and changes which hardware can sustain the required throughput. With ADC, the bottleneck shifts from floating-point compute to memory bandwidth, and even the memory access pattern is cache-friendly: the lookup tables are tiny enough to reside entirely in L1 or L2 cache across the entire scan.

Out[4]:
Visualization
Flow diagram of ADC showing a query vector split into subvectors, lookup tables built from codebooks, and a database vector's integer codes summed via table lookups to produce an approximate distance.
Asymmetric Distance Computation (ADC) overview. The query vector (top left) is split into M subvectors, each compared against the corresponding subspace codebook to populate M lookup tables (built once per query). Each database vector, stored as compact integer centroid codes, then computes an approximate distance via M table lookups and additions, replacing hundreds of floating-point multiplications with a handful of integer-indexed memory reads.

To read the diagram above: the query vector enters from the top left in full float32 precision. It is sliced into MM subvectors, each of which is compared against the corresponding subspace codebook (the purple box in the center) to populate the lookup tables shown in green on the right. This table-building phase is the only moment when floating-point arithmetic occurs. Meanwhile, each database vector arrives as a compact sequence of integer centroid codes. To compute an approximate distance, those codes simply index into the precomputed lookup tables, and the resulting values are summed. The entire comparison path for a database vector, from reading its bytes to producing a distance estimate, involves no multiplication and no subtraction: only integer-indexed array reads and additions.

Why ADC Is Faster Than It Looks

You might wonder: if you are scanning millions of vectors, does building the lookup tables at query time even matter? Yes, significantly. Consider the arithmetic:

  • Without PQ: Computing ∥q−xi∥2\| q - x_i \|^2 for one vector requires 768 multiplications, 768 subtractions, and 767 additions. For 10 million vectors, that is roughly 1.5×10101.5 \times 10^{10} floating-point operations.
  • With PQ (ADC): The lookup table build costs 8×256×96≈200,0008 \times 256 \times 96 \approx 200{,}000 operations, performed once. Each of the 10 million comparisons then costs 8 lookups and 8 additions, totaling 80,000,00080{,}000{,}000 additions. The overall compute is about 200x less.

The memory bandwidth savings are equally dramatic. Instead of reading 3,072 bytes per vector, you read only 8 bytes, a 384x reduction that substantially improves cache efficiency. On modern hardware, memory bandwidth is often the binding constraint for large-scale search: the CPU can perform arithmetic faster than it can fetch data from RAM. By compressing each vector to 8 bytes, PQ ensures that the entire database can cycle through the CPU's cache hierarchy far more efficiently, and the lookup tables themselves (only a few kilobytes) stay warm in L1 cache across the entire scan. This cache-resident lookup table is one of the most elegant aspects of the ADC design: it turns a memory-bound workload into one that is bottlenecked by the tiny, fast cache rather than the large, slow DRAM.

Out[5]:
Visualization
Log-scale line chart comparing floating-point operation counts for flat exhaustive search versus ADC total, scan-only, and table-build costs across M values from 4 to 96, showing ADC is 200x cheaper.
Floating-point operation counts for computing approximate distances over 10 million 768-dimensional vectors, comparing exhaustive flat search (red dashed line) against ADC across numbers of subspaces M on a log-scale y-axis. ADC total cost (blue) and scan-only cost (green) fall two to three orders of magnitude below the flat baseline, while the one-time table-build cost (orange) remains negligible at all tested M values. Even at M=4, ADC reduces total operations by roughly 200x, confirming that the lookup-table strategy makes PQ practical at billion-vector scale.