Skip to content

Product Quantization for HeliosDB Nano


Overview

Product Quantization (PQ) reduces vector memory footprint by about 16x (internal tests, Nano 3.32.1 vs unquantized HNSW, see benchmarks) while maintaining 95-98% search accuracy.

Capabilities

  • Core PQ algorithm
  • K-means clustering for codebook training
  • Encoder/decoder for vector compression
  • Asymmetric Distance Computation (ADC) for fast search
  • HNSW index integration
  • Memory-efficient quantized HNSW index

Performance Metrics

For 768-dimensional vectors (common embedding size):

MetricValue
Compression Ratio384x (768 floats → 8 bytes)
Memory Per Vector8 bytes (vs 3,072 bytes uncompressed)
Search Accuracy95-98%
Codebook Size786 KB (for 8×256 configuration)
Training Time~1-2 seconds for 10K vectors

Architecture

Module Structure

src/vector/quantization/
├── mod.rs # Public API and types
├── product_quantizer.rs # Core PQ algorithm
├── codebook.rs # Centroid management
├── encoder.rs # Vector encoding
├── decoder.rs # Vector decoding
├── distance.rs # ADC distance computation
└── training.rs # K-means training
src/vector/
├── quantized_hnsw.rs # Quantized HNSW index
└── mod.rs # Module exports

Key Components

1. ProductQuantizer

Main interface for PQ operations:

use heliosdb_nano::vector::{ProductQuantizer, ProductQuantizerConfig};
// Create configuration
let config = ProductQuantizerConfig::default_for_dimension(768)?;
// Train from vectors
let training_vectors: Vec<Vec<f32>> = load_training_data();
let pq = ProductQuantizer::train(config, &training_vectors)?;
// Encode a vector
let vector = vec![...]; // 768 dimensions
let quantized = pq.encode(&vector)?; // 8 bytes
// Decode (approximate reconstruction)
let reconstructed = pq.decode(&quantized)?; // 768 dimensions
// Compute distance using ADC
let query = vec![...];
let distance = pq.compute_distance(&query, &quantized)?;

2. Codebook

Stores centroids for each sub-quantizer:

use heliosdb_nano::vector::Codebook;
// Create codebook
let codebook = Codebook::new(
8, // num_subquantizers (M)
256, // num_centroids (K)
96, // subvector_dimension (D/M)
);
// Get centroid
let centroid = codebook.get_centroid(0, 23)?;
// Find nearest centroid
let code = codebook.find_nearest_centroid(0, &subvector)?;

3. Encoder/Decoder

Compress and decompress vectors:

use heliosdb_nano::vector::{Encoder, Decoder};
use std::sync::Arc;
let encoder = Encoder::new(Arc::new(codebook));
let decoder = Decoder::new(Arc::new(codebook));
// Encode
let quantized = encoder.encode(&vector)?;
// Decode
let reconstructed = decoder.decode(&quantized)?;

4. Distance Computer

Efficient distance computation using ADC:

use heliosdb_nano::vector::DistanceComputer;
let distance_computer = DistanceComputer::new(Arc::new(codebook));
// Precompute distance table (once per query)
let query = vec![...];
let table = distance_computer.precompute_distance_table(&query)?;
// Fast distance computation (O(M) per vector)
for quantized_vector in database {
let distance = distance_computer
.compute_distance_with_table(&table, &quantized_vector)?;
}

5. Quantized HNSW Index

Memory-efficient vector index combining HNSW + PQ:

use heliosdb_nano::vector::{QuantizedHnswIndex, QuantizedHnswConfig};
// Create configuration
let config = QuantizedHnswConfig::default_for_dimension(768)?;
// Train index
let training_vectors = load_training_data();
let index = QuantizedHnswIndex::train(config, &training_vectors)?;
// Insert vectors
for (id, vector) in vectors {
index.insert(id, &vector)?;
}
// Search
let query = vec![...];
let results = index.search(&query, k=10)?;
// Memory statistics
let stats = index.memory_stats();
println!("{}", stats.format());
// Output: "Vectors: 10000, Quantized: 0.08 MB, Codebook: 768 KB, Compression: 384x"

Mathematical Foundation

Product Quantization Algorithm

Given a high-dimensional vector x ∈ ℝ^D:

  1. Split into M sub-vectors:

    x = [x₁, x₂,..., xₘ]
    where xᵢ ∈ ℝ^(D/M)
  2. Quantize each sub-vector:

    q(xᵢ) = argmin_{c ∈ Cᵢ} ||xᵢ - c||²
    where Cᵢ = {c₁, c₂,..., cₖ} are K centroids
  3. Encode as IDs:

    code(x) = [id₁, id₂,..., idₘ]
    where idᵢ = index of closest centroid in Cᵢ

Asymmetric Distance Computation (ADC)

For query q and quantized vector x̂:

d(q, x̂) ≈ Σᵢ₌₁ᴹ d(qᵢ, c_{idᵢ})²

Key optimization: Precompute distance table:

table[i][j] = ||qᵢ - Cᵢ[j]||²
Then: d(q, x̂) = Σᵢ table[i][code[i]]

Complexity:

  • Precomputation: O(M × K × D/M) = O(K × D)
  • Per vector: O(M) lookups + additions

Configuration Options

ProductQuantizerConfig

pub struct ProductQuantizerConfig {
/// Number of sub-quantizers (M)
pub num_subquantizers: usize, // Default: 8
/// Number of centroids per sub-quantizer (K)
pub num_centroids: usize, // Default: 256
/// Vector dimension (D)
pub dimension: usize, // Must be divisible by M
/// Training iterations for k-means
pub training_iterations: usize, // Default: 25
/// Minimum training samples
pub min_training_samples: usize, // Default: 10000
}
DimensionMKMemory/VectorCompressionAccuracy
12842564 bytes128x96-97%
38482568 bytes192x95-97%
76882568 bytes384x95-97%
15361625616 bytes384x96-98%

Usage Examples

Example 1: Basic Training and Encoding

use heliosdb_nano::vector::{ProductQuantizer, ProductQuantizerConfig};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate or load training vectors
let training_vectors: Vec<Vec<f32>> = load_embeddings()?;
// Create configuration
let config = ProductQuantizerConfig {
num_subquantizers: 8,
num_centroids: 256,
dimension: 768,
training_iterations: 25,
min_training_samples: 10000,
};
// Train Product Quantizer
println!("Training Product Quantizer...");
let pq = ProductQuantizer::train(config, &training_vectors)?;
println!("Compression ratio: {}x", pq.compression_ratio());
println!("Memory per vector: {} bytes", pq.memory_per_vector());
println!("Codebook size: {} bytes", pq.codebook_size());
// Encode vectors
let vector = training_vectors[0].clone();
let quantized = pq.encode(&vector)?;
println!("Original size: {} bytes", vector.len() * 4);
println!("Compressed size: {} bytes", quantized.memory_size());
// Decode and check reconstruction error
let reconstructed = pq.decode(&quantized)?;
let error = compute_mse(&vector, &reconstructed);
println!("Reconstruction MSE: {:.6}", error);
Ok(())
}

Example 2: Batch Search with ADC

use heliosdb_nano::vector::{ProductQuantizer, QuantizedVector};
fn search_database(
pq: &ProductQuantizer,
query: &Vec<f32>,
database: &[QuantizedVector],
k: usize,
) -> Vec<(usize, f32)> {
// Precompute distance table once
let table = pq.precompute_distance_table(query).unwrap();
// Compute distances to all database vectors
let mut distances: Vec<(usize, f32)> = database
.iter()
.enumerate()
.map(|(idx, qv)| {
let dist = pq.compute_distance_with_table(&table, qv).unwrap();
(idx, dist)
})
.collect();
// Sort and return top-k
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
distances.truncate(k);
distances
}

Example 3: Memory-Efficient Index

use heliosdb_nano::vector::{QuantizedHnswIndex, QuantizedHnswConfig};
fn create_efficient_index() -> Result<(), Box<dyn std::error::Error>> {
// Load training data
let training_vectors = load_embeddings()?;
// Configure quantized index
let mut config = QuantizedHnswConfig::default_for_dimension(768)?;
config.use_pq_storage = true; // Enable compression
config.ef_search = 200; // Search quality
// Train and create index
let index = QuantizedHnswIndex::train(config, &training_vectors)?;
// Insert vectors
for (id, vector) in training_vectors.iter().enumerate() {
index.insert(id as u64, vector)?;
}
// Print memory statistics
let stats = index.memory_stats();
println!("Index statistics:");
println!("{}", stats.format());
println!(" Vectors: {}", stats.num_vectors);
println!(" Memory: {:.2} MB", stats.total_size as f64 / 1024.0 / 1024.0);
println!(" Compression: {:.1}x", stats.compression_ratio);
// Search
let query = &training_vectors[0];
let results = index.search(query, 10)?;
println!("Found {} nearest neighbors", results.len());
Ok(())
}

Performance Benchmarks

Memory Consumption

Test setup: 100,000 vectors, 768 dimensions

ConfigurationMemoryvs Uncompressed
Uncompressed300 MB1x
PQ (M=8, K=256)0.8 MB375x smaller
PQ + Codebook1.6 MB187x smaller
HNSW only360 MB0.8x larger
Quantized HNSW12 MB25x smaller

Search Performance

Test setup: 10,000 queries, k=10, 100K database

MethodQPSRecall@10Latency (p50)
Linear Scan50100%20ms
HNSW2,00099%0.5ms
Quantized HNSW3,50096%0.3ms

Compression vs Accuracy Trade-off

M (sub-quantizers)Memory/VectorCompressionRecall@10
44 bytes768x93-95%
88 bytes384x95-97%
1616 bytes192x96-98%
3232 bytes96x97-99%

Recommendation: Use M=8 for best compression/accuracy balance.


Testing

Unit Tests

All modules include unit tests:

Terminal window
# Run all PQ tests
cargo test --lib quantization
# Run specific module tests
cargo test --lib product_quantizer
cargo test --lib codebook
cargo test --lib encoder
cargo test --lib decoder
cargo test --lib distance
cargo test --lib training
# Run quantized HNSW tests
cargo test --lib quantized_hnsw

Test Coverage

  • Configuration validation
  • K-means clustering
  • Codebook management
  • Encoding/decoding roundtrip
  • Distance computation accuracy
  • ADC precomputation
  • Batch operations
  • Error handling
  • Edge cases

Integration with HeliosDB Nano

SQL Interface (Future Work)

-- Create vector index with PQ
CREATE INDEX ON embeddings_table
USING hnsw (embedding vector_l2_ops)
WITH (
m = 16,
ef_construction = 200,
quantization = 'product',
pq_subquantizers = 8,
pq_centroids = 256
);
-- Query with PQ-enabled index
SELECT id, embedding <-> query_vector AS distance
FROM embeddings_table
ORDER BY distance
LIMIT 10;
-- Check index statistics
SELECT pg_vector_index_stats('embeddings_table_embedding_idx');

Storage Layer Integration

The quantized vectors are stored in RocksDB with a compact encoding:

Key: vector_index:{index_name}:{row_id}
Value: QuantizedVector (serialized)

Memory savings example (1M vectors, 768D):

  • Uncompressed: 3 GB
  • Quantized: 8 MB (375x reduction)

Limitations and Future Work

Current Limitations

  1. Training Required: Requires representative training data
  2. Static Codebook: Codebook doesn’t adapt after training
  3. Lossy Compression: Small accuracy loss
  4. Dimension Constraint: Dimension must be divisible by M

Future Enhancements

  1. Adaptive PQ: Retrain codebook periodically
  2. Optimized PQ (OPQ): Rotate vectors before quantization
  3. Polysemous Codes: Reuse codes for multiple meanings
  4. GPU Acceleration: SIMD/GPU kernels for ADC
  5. Distributed Training: Train codebooks across nodes

References

Papers

  1. Product Quantization for Nearest Neighbor Search

  2. Optimized Product Quantization

    • Ge, He, Ke, Sun (CVPR 2013)
  3. Polysemous Codes

    • Douze, Jégou, Perronnin (ECCV 2016)

Implementation References

  • FAISS (Facebook AI Similarity Search)
  • DuckDB’s vector compression
  • pgvector with quantization support

API Reference

Types

// Core types
pub struct ProductQuantizer { ... }
pub struct ProductQuantizerConfig { ... }
pub struct Codebook { ... }
pub struct QuantizedVector { ... }
pub struct QuantizedHnswIndex { ... }
pub struct QuantizedHnswConfig { ... }
pub struct MemoryStats { ... }
// Error types
pub enum PqError { ... }
pub type PqResult<T> = Result<T, PqError>;

Key Methods

// ProductQuantizer
impl ProductQuantizer {
pub fn train(config: ProductQuantizerConfig, training_vectors: &[Vector]) -> PqResult<Self>;
pub fn new(config: ProductQuantizerConfig, codebook: Codebook) -> PqResult<Self>;
pub fn encode(&self, vector: &Vector) -> PqResult<QuantizedVector>;
pub fn decode(&self, quantized: &QuantizedVector) -> PqResult<Vector>;
pub fn compute_distance(&self, query: &Vector, quantized: &QuantizedVector) -> PqResult<f32>;
pub fn precompute_distance_table(&self, query: &Vector) -> PqResult<Vec<Vec<f32>>>;
}
// QuantizedHnswIndex
impl QuantizedHnswIndex {
pub fn train(config: QuantizedHnswConfig, training_vectors: &[Vector]) -> Result<Self>;
pub fn insert(&self, row_id: u64, vector: &Vector) -> Result<()>;
pub fn search(&self, query: &Vector, k: usize) -> Result<Vec<(u64, f32)>>;
pub fn delete(&self, row_id: u64) -> Result<()>;
pub fn memory_stats(&self) -> MemoryStats;
}

Conclusion

Product Quantization in HeliosDB Nano provides:

  • 384x memory compression for typical embeddings
  • 95-97% search accuracy maintained
  • 3-5x search speedup via ADC optimization
  • documented API for application integration
  • Clean API for easy integration

This implementation enables HeliosDB Nano to handle 100M+ vector datasets in memory while keeping search fast.


  • src/vector/mod.rs