---
title: "Product Quantization Integration Guide"
---

# Product Quantization Integration Guide

## Overview

Product Quantization is in Nano's default build: add `WITH (quantization='product')` to an HNSW index. Indexes are unquantized unless you ask, and durable restart-surviving PQ-HNSW indexes need the vector-persist build. In internal tests (Nano 3.32.1 regression fixture, dims 128/384, vs unquantized HNSW; see [benchmarks](https://www.heliosdb.com/benchmarks.html)) PQ used about 16x less resident RAM at recall-safe defaults (recall@10 0.987 vs 0.989).

## Features

- **SQL Support**: Create PQ indexes using standard SQL syntax
- **Automatic Configuration**: Smart defaults based on vector dimensions
- **Persistence**: restart-surviving PQ-HNSW indexes (`WITH (persistent = true)`) need a Nano build with the `vector-persist` feature; the default build rejects `persistent = true`
- **Transparency**: Works with the existing vector operations

## SQL Syntax

### Basic PQ Index Creation

```sql
-- Create table with vector column
CREATE TABLE documents (
    id INTEGER PRIMARY KEY,
    title TEXT,
    embedding VECTOR(768)
);

-- Create PQ-compressed vector index
CREATE INDEX doc_embedding_idx ON documents(embedding)
USING hnsw
WITH (quantization='product');
```

### Advanced Configuration

```sql
-- Customize PQ parameters
CREATE INDEX custom_pq_idx ON documents(embedding)
USING hnsw
WITH (
    quantization='product',
    pq_subquantizers=8,      -- Number of sub-quantizers (M)
    pq_centroids=256          -- Centroids per sub-quantizer (K)
);
```

### Parameter Guidelines

**pq_subquantizers (M)**:
- Default: Auto-calculated based on dimension
  - 768+ dims: M=8
  - 256-767 dims: M=4
  - <256 dims: M=2
- Must evenly divide vector dimension
- Higher M = better accuracy, larger codes
- Typical range: 2-16

**pq_centroids (K)**:
- Default: 256 (recommended)
- Range: 1-256
- 256 allows 1-byte codes (optimal)
- Higher K = better accuracy, larger codebook

## Usage Examples

### 1. Create and Populate Index

```sql
-- Create table
CREATE TABLE embeddings (
    id INTEGER PRIMARY KEY,
    text TEXT,
    vector VECTOR(384)
);

-- Create PQ index
CREATE INDEX emb_idx ON embeddings(vector)
USING hnsw
WITH (quantization='product');

-- Insert vectors (automatically quantized)
INSERT INTO embeddings (id, text, vector) VALUES
(1, 'hello world', VECTOR([0.1, 0.2, ...])),
(2, 'foo bar', VECTOR([0.3, 0.4, ...])),
(3, 'test data', VECTOR([0.5, 0.6, ...]));
```

### 2. Search with PQ Index

```sql
-- Vector similarity search (uses PQ automatically)
SELECT id, text, vector_distance(vector, VECTOR([0.1, 0.2, ...])) as distance
FROM embeddings
ORDER BY distance
LIMIT 10;
```

### 3. Check Index Statistics

```sql
-- View index details
SELECT
    index_name,
    num_vectors,
    dimensions,
    quantization,
    memory_bytes,
    memory_bytes / 1024.0 / 1024.0 as memory_mb
FROM helios_vector_indexes
WHERE quantization = 'Product';
```

### 4. Compare Memory Usage

```sql
-- Create standard and PQ indexes for comparison
CREATE TABLE comparison (
    id INTEGER PRIMARY KEY,
    vec VECTOR(768)
);

CREATE INDEX std_idx ON comparison(vec) USING hnsw;
CREATE INDEX pq_idx ON comparison(vec) USING hnsw
WITH (quantization='product');

-- Insert test data
INSERT INTO comparison (id, vec)
SELECT
    generate_series(1, 10000),
    random_vector(768);

-- Compare memory usage
SELECT
    index_name,
    quantization,
    memory_bytes,
    memory_bytes / 1024.0 / 1024.0 as memory_mb
FROM helios_vector_indexes
WHERE table_name = 'comparison';
```

Expected output:
```
index_name | quantization | memory_bytes | memory_mb
-----------+--------------+--------------+----------
std_idx    | None         | 30720000     | 29.30
pq_idx     | Product      | 2048000      | 1.95
```

## Configuration Examples by Dimension

### Common Embedding Dimensions

**384 dimensions (MiniLM, etc.)**:
```sql
CREATE INDEX idx_384 ON table(vec) USING hnsw
WITH (
    quantization='product',
    pq_subquantizers=4,    -- 384/4 = 96 per subquantizer
    pq_centroids=256
);
```

**768 dimensions (BERT, RoBERTa)**:
```sql
CREATE INDEX idx_768 ON table(vec) USING hnsw
WITH (
    quantization='product',
    pq_subquantizers=8,    -- 768/8 = 96 per subquantizer
    pq_centroids=256
);
```

**1536 dimensions (OpenAI ada-002)**:
```sql
CREATE INDEX idx_1536 ON table(vec) USING hnsw
WITH (
    quantization='product',
    pq_subquantizers=8,    -- 1536/8 = 192 per subquantizer
    pq_centroids=256
);
```

## Performance Characteristics

### Memory Compression

| Dimension | Standard | PQ (M=8) | Compression |
|-----------|----------|----------|-------------|
| 128 | 512 B | 40 B | 12.8x |
| 384 | 1.5 KB | 40 B | 38.4x |
| 768 | 3.0 KB | 40 B | 76.8x |
| 1536 | 6.0 KB | 40 B | 153.6x |

Note: Actual compression includes codebook overhead

### Search Accuracy

Typical recall@10 metrics:
- 95-98% with M=8, K=256
- 92-95% with M=4, K=256
- 88-92% with M=2, K=256

### Query Performance

- **PQ Search**: ~5-10% slower than standard HNSW
- **ADC Optimization**: Precomputed distance tables minimize overhead
- **Large Datasets**: Performance gap decreases as dataset grows

## Best Practices

### 1. Training Data

```sql
-- Let the index train on existing data
CREATE INDEX pq_idx ON large_table(vec) USING hnsw
WITH (quantization='product');

-- Index will use sample of existing vectors for training
-- Minimum 10,000 vectors recommended for optimal training
```

### 2. Dimension Selection

- Choose M such that D/M is between 64-128
- Common configurations:
  - 384 dims: M=4 (96 per subquantizer)
  - 768 dims: M=8 (96 per subquantizer)
  - 1536 dims: M=12 or M=16

### 3. When to Use PQ

**Use PQ when**:
- Dataset > 100K vectors
- Memory is constrained
- High-dimensional vectors (>256 dims)
- Can tolerate 2-5% accuracy loss

**Use standard HNSW when**:
- Dataset < 10K vectors
- Maximum accuracy required
- Low-dimensional vectors (<128 dims)
- Memory is not constrained

## Troubleshooting

### Error: Dimension not divisible by subquantizers

```sql
-- This will fail: 100 is not divisible by 8
CREATE INDEX bad_idx ON table(vec VECTOR(100)) USING hnsw
WITH (quantization='product', pq_subquantizers=8);

-- Solution: Use M=4 (100/4 = 25)
CREATE INDEX good_idx ON table(vec VECTOR(100)) USING hnsw
WITH (quantization='product', pq_subquantizers=4);
```

### Insufficient Training Data

```sql
-- Error if table has < 10,000 vectors
CREATE INDEX pq_idx ON small_table(vec) USING hnsw
WITH (quantization='product');

-- Solution: Use standard index for small datasets
CREATE INDEX std_idx ON small_table(vec) USING hnsw;
```

### Low Search Accuracy

```sql
-- If recall is too low, increase M
CREATE INDEX better_pq_idx ON table(vec VECTOR(768)) USING hnsw
WITH (
    quantization='product',
    pq_subquantizers=16  -- Increased from 8
);
```

## Migration Guide

### Migrating Existing Indexes to PQ

```sql
-- 1. Create new PQ index
CREATE INDEX new_pq_idx ON documents(embedding) USING hnsw
WITH (quantization='product');

-- 2. Test search quality
SELECT id FROM documents
USE INDEX (new_pq_idx)
ORDER BY vector_distance(embedding, VECTOR(?))
LIMIT 10;

-- 3. Drop old index if satisfied
DROP INDEX old_idx;
```

## Monitoring

### Index Health

```sql
SELECT
    index_name,
    num_vectors,
    quantization,
    memory_bytes,
    memory_bytes::float / num_vectors as bytes_per_vector,
    recall_at_10
FROM helios_vector_indexes
WHERE quantization = 'Product';
```

### Performance Metrics

```sql
-- Track query performance
EXPLAIN ANALYZE
SELECT id FROM documents
ORDER BY vector_distance(embedding, VECTOR(?))
LIMIT 10;
```

## Advanced Topics

### Custom Distance Metrics

```sql
-- PQ supports different distance metrics
CREATE INDEX cosine_pq_idx ON documents(embedding) USING hnsw
WITH (
    quantization='product',
    distance_metric='cosine'
);
```

### Hybrid Indexes

```sql
-- Use PQ for large-scale search, standard for refinement
CREATE INDEX pq_idx ON documents(embedding) USING hnsw
WITH (quantization='product');

CREATE INDEX std_idx ON top_results(embedding) USING hnsw;

-- Two-stage search
WITH candidates AS (
    SELECT id FROM documents
    USE INDEX (pq_idx)
    ORDER BY vector_distance(embedding, VECTOR(?))
    LIMIT 100
)
SELECT id FROM candidates
JOIN documents USING (id)
USE INDEX (std_idx)
ORDER BY vector_distance(embedding, VECTOR(?))
LIMIT 10;
```

## References

- [Product Quantization Paper](https://inria.hal.science/inria-00514462v2)
- [HNSW Paper](https://arxiv.org/abs/1603.09320)
- [Vector Search Documentation](/docs/nano/features/vector_search/)
