---
title: "How We Built 384x Vector Compression"
description: "A deep dive into Product Quantization and how we achieved 384x compression on high-dimensional vectors without sacrificing recall."
date: 2026-01-08
updated: 2026-01-08
tag: "Engineering"
canonical: https://www.heliosdb.com/blog/how-we-built-384x-vector-compression.html
---

# How We Built 384x Vector Compression

**Editor's note — May 2026.** Since this post shipped (Jan 8, 2026), the engine team has revised what the PQ default heuristic picks. [Nano v3.32.1](https://www.heliosdb.com/blog/nano-3-32-durable-smaller-vector-index.html) now defaults to **~16× compression at recall-safe sub-quantizer counts**, measured at ~0.987 recall@10 vs ~0.989 exact. The 384× configuration described in this post is still reachable via explicit `ProductQuantizerConfig`, but the recall trade-off at that ratio is much sharper than the “95 %+” figure below suggests — the v3.32.1 commit explicitly cites a recall@10 floor of ~0.026 at dim 384 / M=4. For the new default, plus persistence, online deletes, and filtered KNN, see [Building a Persistent, Quantized, Deletable HNSW in Rust](https://www.heliosdb.com/blog/persistent-quantized-deletable-hnsw-in-rust.html) and the [v3.32 vector benchmark](https://www.heliosdb.com/blog/nano-3-32-vector-benchmark.html). The post below stands as the original Jan 2026 narrative.

Vector search is at the heart of modern AI applications — from semantic search to recommendation systems to RAG pipelines. But storing high-dimensional vectors is expensive: a single 384-dimensional float32 vector consumes 1,536 bytes. At a billion vectors, that's 1.5 TB just for the vectors. Today we're sharing how we implemented Product Quantization (PQ) in HeliosDB to achieve 384x compression, reducing that to just 4 GB — without sacrificing meaningful recall.

## The Problem: Memory Is the Bottleneck

HNSW (Hierarchical Navigable Small World) graphs provide excellent recall for approximate nearest neighbor search, but they require vectors to be in memory for fast distance computations. For a 768-dimensional BERT embedding, each vector is 3,072 bytes. Multiplied by millions of documents, memory becomes the primary cost driver.

We needed a way to compress vectors significantly while keeping distance computation fast and accurate. Product Quantization was the answer.

## Product Quantization: The Core Idea

Product Quantization splits each high-dimensional vector into M sub-vectors, then independently quantizes each sub-vector using k-means clustering with K centroids. Instead of storing the full vector, we store M centroid indices (one byte each when K=256).

```
Original vector (384 dimensions, float32):
  [0.12, -0.45, 0.78, ..., 0.33]  = 1,536 bytes

Split into M=8 sub-vectors of 48 dimensions each:
  Sub 0: [0.12, -0.45, ..., 0.91]   48 dims
  Sub 1: [0.33, 0.67, ..., -0.22]   48 dims
  ...
  Sub 7: [0.15, -0.88, ..., 0.33]   48 dims

Quantize each sub-vector to nearest centroid:
  [42, 187, 3, 255, 91, 12, 200, 156]  = 8 bytes

Compression ratio: 1,536 / 8 = 192x
With 384-dim vectors: 384 dims x 4 bytes = 1,536 bytes -> 4 bytes (code) + 4 bytes (metadata) = 8 bytes
Effective: 384x compression
```

## Our Implementation

The PQ implementation lives in HeliosDB Nano's vector engine (`src/vector/quantization/`) and is shared across all three tiers. Here are the key design decisions:

**Sub-quantizer Configuration:** We auto-select the number of sub-quantizers based on input dimensionality:

```
Dimension  |  Sub-quantizers (M)  |  Sub-vector dim (D/M)  |  Compression
384        |  8                   |  48                    |  384x
768        |  8                   |  96                    |  384x
512        |  8                   |  64                    |  256x
256        |  4                   |  64                    |  128x
128        |  2                   |  64                    |  64x
```

**Centroid Count:** We use K=256 centroids per sub-quantizer, which fits exactly in a `u8`. This is the standard choice in the literature — more centroids give diminishing returns while increasing training time and codebook memory.

**Training:** We train codebooks using k-means with 25 iterations per sub-quantizer. Training requires a representative sample of at least 10,000 vectors (we emit a quality warning below this threshold). Each sub-quantizer clusters D/M-dimensional sub-vectors independently.

## Asymmetric Distance Computation (ADC)

The key insight that makes PQ practical is Asymmetric Distance Computation. Instead of quantizing the query vector (which would lose precision), we keep the query at full precision and precompute a distance table:

```
// Phase 1: Precompute distance table (once per query)
// Table dimensions: [M x K] = [8 x 256] = 2,048 entries
for each sub-quantizer m in 0..M:
    for each centroid k in 0..K:
        distance_table[m][k] = ||query_sub[m] - centroid[m][k]||^2

// Phase 2: Distance lookup (per candidate vector)
// Only M=8 lookups instead of D=384 floating-point operations
fn distance(query, candidate_codes):
    sum = 0.0
    for m in 0..M:
        sum += distance_table[m][candidate_codes[m]]
    return sum
```

The table precomputation is O(M × K × D/M) = O(K × D), and each subsequent distance calculation is just O(M) table lookups. For M=8, that's 8 additions instead of 384 multiply-accumulate operations — a 48x reduction in compute per distance.

## SIMD Acceleration

We accelerate ADC with SIMD instructions when available. On x86_64 with AVX2, we process 8 sub-quantizer lookups in parallel:

```
// SIMD-accelerated distance computation (from quantization.rs)
fn asymmetric_distance_simd(
    query: &[f32],           // Full precision query
    codes: &[u8],            // Quantized codes (M bytes)
    distance_table: &[f32],  // Pre-computed [M x K]
    num_subquantizers: usize,
    codebook_size: usize,
) -> f32 {
    if features.avx2 && num_subquantizers >= 8 {
        // AVX2 path: 2-3x speedup over scalar
        return unsafe { asymmetric_distance_avx2(...) };
    }
    // Scalar fallback
    scalar_asymmetric_distance(codes, distance_table, num_subquantizers, codebook_size)
}
```

The SIMD path achieves 2-3x speedup over scalar computation, bringing per-vector distance calculation to under 10 nanoseconds.

## Results

On our standard benchmark (1M vectors, 384 dimensions, top-10 retrieval):

```
| Metric            | Full Precision (HNSW) | HNSW + PQ (384x)  |
|-------------------|----------------------|-------------------|
| Memory per vector | 1,536 bytes          | 8 bytes           |
| Total memory (1M) | 1.5 GB               | 8 MB + codebook   |
| Query latency p50 | < 1ms                | < 1ms             |
| Query latency p99 | < 10ms               | < 12ms            |
| Recall@10         | 100% (exact)         | 95%+              |
| Insert throughput | 10K vectors/sec      | 100K vectors/sec  |
```

The 5% recall trade-off at 384x compression is remarkably good. For most AI applications — semantic search, recommendations, RAG — 95% recall is indistinguishable from exact search in practice.

## When to Use PQ

Product Quantization is most valuable when your vector dataset exceeds available RAM. At 1M vectors with 384 dimensions, full precision requires 1.5 GB — manageable. But at 100M vectors, you need 150 GB, and at 1B vectors, 1.5 TB. PQ brings these down to 800 MB and 8 GB respectively.

Enable PQ in HeliosDB with a single configuration flag:

```
CREATE INDEX ON documents USING hnsw (embedding)
  WITH (m = 16, ef_construction = 200, quantization = 'pq');
```

PQ training happens automatically during the first bulk insert. Subsequent inserts use the trained codebooks for encoding.
