Module 2: How Vector Databases Work (Conceptual)

Capsule 06: PQ - Product Quantization

🎯 Capsule objective

Understand HOW PQ works (Product Quantization) to compress vectors 4-8x, reducing memory dramatically with an accuracy trade-off.

By the end of this capsule:

  • ✅ You'll explain what Product Quantization is (codebook compression)
  • ✅ You'll understand the trade-off: 4-8x less memory vs 85-90% accuracy
  • ✅ You'll know the key parameters (m, nbits)
  • ✅ You'll decide when to use PQ (>5M vectors, RAM critical)

Estimated time: 8-10 minutes


📦 What is Product Quantization?

Definition

PQ = Product Quantization

It's a vector compression technique that divides a vector into sub-vectors, then replaces each sub-vector with a code (an index) from a codebook.

Result: A compressed vector 4-8x smaller with slightly lower accuracy.

Analogy: Image compression (JPEG)

Uncompressed image (PNG):

  • Each pixel: 24 bits (RGB: 8+8+8)
  • 1000x1000 image: 24,000,000 bits = 3 MB

Compressed image (JPEG):

  • Divides the image into 8x8 blocks
  • Replaces similar blocks with codes
  • Result: 300 KB (10x compression)
  • Trade-off: 90% quality (visible artifacts when you zoom in)

Applied to vectors:

  • Original vector: 1536 dims × 4 bytes = 6 KB
  • Compressed vector (PQ): 1536 dims → 192 codes × 1 byte = 192 bytes
  • Result: 32x compression
  • Trade-off: 85-90% accuracy (vs 100% original)

🏗️ PQ architecture

Step 1: Splitting into sub-vectors

# Original vector (1536 dims)
vector = [0.1, 0.5, 0.8, ..., 0.3]  # 1536 dims

# Split into m sub-vectors
m = 8  # Number of sub-vectors
sub_vector_size = 1536 / 8 = 192 dims

sub_vectors = [
    [0.1, 0.5, ..., 0.2],  # Sub-vector 1 (192 dims)
    [0.8, 0.3, ..., 0.7],  # Sub-vector 2 (192 dims)
    ...
    [0.4, 0.9, ..., 0.3],  # Sub-vector 8 (192 dims)
]

Key: A 1536-dim vector is split into 8 sub-vectors of 192 dims each.

Step 2: Building the codebook (Training)

A codebook is a dictionary that maps sub-vectors to codes.

# For each sub-vector, run k-means
def build_codebook(all_vectors, m=8, k=256):
    codebooks = []
    
    for i in range(m):  # For each sub-vector position
        # Extract the sub-vectors from all vectors
        sub_vectors = [v[i] for v in all_vectors]
        
        # k-means to find k centroids
        centroids = kmeans(sub_vectors, k=256)
        
        # Store the codebook
        codebooks[i] = centroids  # 256 centroids for sub-vector i
    
    return codebooks

Result:

  • 8 codebooks (one per sub-vector)
  • Each codebook: 256 centroids (192 dims each)

Visualization:

Codebook 0 (sub-vector 0):
  0: [0.1, 0.2, ..., 0.3]  ← Centroid 0
  1: [0.5, 0.1, ..., 0.7]  ← Centroid 1
  ...
  255: [0.8, 0.9, ..., 0.2]  ← Centroid 255

Codebook 1 (sub-vector 1):
  0: [0.3, 0.4, ..., 0.1]
  ...
  255: [0.6, 0.5, ..., 0.9]

... (8 codebooks total)

Step 3: Encoding (Compressing vectors)

def encode_vector(vector, codebooks, m=8):
    codes = []
    
    for i in range(m):  # For each sub-vector
        sub_vector = vector[i]
        
        # Find the closest centroid in codebook i
        nearest_centroid_id = find_nearest(sub_vector, codebooks[i])
        
        codes.append(nearest_centroid_id)  # 0-255 (1 byte)
    
    return codes  # [123, 45, 200, 12, 89, 150, 3, 67]

Result:

  • Original vector: 1536 floats × 4 bytes = 6144 bytes
  • Compressed vector: 8 codes × 1 byte = 8 bytes
  • Compression: 768x

But in practice: You also store the codebooks (overhead), so the effective compression is ~4-8x.

Step 4: Search with compressed vectors

def search_pq(query, compressed_vectors, codebooks, k=10):
    # 1. Encode the query using the codebooks
    query_codes = encode_vector(query, codebooks)
    
    # 2. Compute the asymmetric distance
    # (original query vs compressed vectors)
    similarities = []
    for compressed_vec in compressed_vectors:
        # Compute the distance using the codebooks
        dist = asymmetric_distance(query, compressed_vec, codebooks)
        similarities.append(dist)
    
    # 3. Return top-k
    return top_k(similarities, k)

Key: The query is NOT compressed (it uses the original vector). Only the database vectors are compressed.


📊 Trade-off: Accuracy vs Memory

Accuracy loss

Why accuracy drops:

When you replace a sub-vector with its closest centroid, you lose precision.

Example:

Original sub-vector:     [0.123, 0.456, 0.789]
Closest centroid:        [0.120, 0.450, 0.800]
Error:                   [0.003, 0.006, 0.011]

Multiplied across 8 sub-vectors → Accumulated error.

Typical accuracy:

ConfigurationAccuracyMemory (1M vecs)
No PQ (original)100%6 GB
PQ m=8, nbits=888-92%1 GB (6x compression)
PQ m=16, nbits=885-90%2 GB (3x compression)
PQ m=8, nbits=480-85%0.5 GB (12x compression)

Memory saving

Calculation:

Original vector:
- Dims: 1536
- Type: float32 (4 bytes)
- Size: 1536 × 4 = 6144 bytes = 6 KB

Compressed vector (PQ):
- Sub-vectors: m = 8
- Bits per code: nbits = 8 (1 byte)
- Size: 8 × 1 = 8 bytes

Compression: 6144 / 8 = 768x

In practice with overhead:

  • Codebooks: 8 × 256 × 192 × 4 = 1.5 MB (shared)
  • Effective compression: ~6x for 1M vectors

⚙️ Key PQ parameters

1. m (Number of sub-vectors)

Definition: How many sub-vectors to split the original vector into.

Impact:

  • High m (16, 32):

    • ✅ Better accuracy (smaller sub-vectors = more precise centroids)
    • ❌ Less compression (more codes to store)
    • ❌ Slower query (more lookups in the codebook)
  • Low m (4, 8):

    • ✅ Higher compression
    • ✅ Fast query
    • ❌ Lower accuracy

Practical rule: m = 8 (accuracy/compression balance)

2. nbits (Bits per code)

Definition: How many bits to use for each code (it affects the codebook size).

Impact:

  • High nbits (8, 16):

    • ✅ Large codebook (256, 65536 centroids)
    • ✅ Better accuracy (more centroids = better representation)
    • ❌ More memory for the codebooks
  • Low nbits (4, 6):

    • ✅ Small codebook (16, 64 centroids)
    • ✅ Less memory
    • ❌ Lower accuracy

Practical rule: nbits = 8 (256 centroids, balance)

Trade-offs visualized

Accuracy vs Compression

High Accuracy   m=16, nbits=8
(90%)          ▲  ❌ 3x compression
               │
Medium         │  m=8, nbits=8
Accuracy       │  ⚖️ 6x compression
(88%)          │  ← Recommended
               │
Low Accuracy   │  m=8, nbits=4
(82%)          │  ✅ 12x compression
               └─────────────────
                 Low ← Memory → High

🆚 Comparison: HNSW vs IVF vs PQ

Complete comparison table

DimensionHNSWIVFPQIVF + PQ
ComplexityO(log n)O(sqrt(n))O(n) compressedO(sqrt(n)) compressed
Accuracy95-99%90-95%85-90%85-92%
Latency (1M)15-20 ms30-50 ms20-40 ms25-45 ms
Memory (1M)4-8 GB1-2 GB0.5-1 GB0.3-0.6 GB
Compression1x1x6x8x
Build time5-10 min2-3 min10-15 min15-20 min
Best for<1M, critical accuracy>1M, limited memory>5M, RAM critical>10M, extreme scale

When to use each one

HNSW:

  • ✅ <1M vectors
  • ✅ Accuracy >95% required
  • ✅ You have enough RAM

IVF:

  • ✅ 1-5M vectors
  • ✅ 90% accuracy is OK
  • ✅ Moderate memory

PQ:

  • ✅ >5M vectors
  • ✅ 85-90% accuracy is enough
  • ✅ RAM very limited

IVF + PQ:

  • ✅ >10M vectors
  • ✅ 85-90% accuracy is enough
  • ✅ Maximum scale (100M+ vectors)

🏭 PQ in practice: Faiss

PQ standalone

import faiss
import numpy as np

# 1. Create the PQ index
dimension = 1536
m = 8  # Sub-vectors
nbits = 8  # Bits per code (256 centroids)

index = faiss.IndexPQ(dimension, m, nbits)

# 2. Train (build the codebooks)
vectors = np.random.random((1_000_000, dimension)).astype('float32')
index.train(vectors)

# 3. Add vectors
index.add(vectors)

# 4. Query
query = np.random.random((1, dimension)).astype('float32')
distances, indices = index.search(query, k=10)

# Memory: ~1 GB (vs 6 GB without PQ)
# Accuracy: ~88%

IVF + PQ (Combined)

Best configuration for extreme scale:

# IVF + PQ combined
dimension = 1536
nlist = 1000  # IVF clusters
m = 8  # PQ sub-vectors
nbits = 8  # PQ bits

quantizer = faiss.IndexFlatL2(dimension)
index = faiss.IndexIVFPQ(quantizer, dimension, nlist, m, nbits)

# Train
index.train(vectors)

# Add
index.add(vectors)

# Query
index.nprobe = 10
distances, indices = index.search(query, k=10)

# Memory: ~600 MB for 1M vectors (10x compression)
# Accuracy: ~90%

Trade-off:

  • IVF reduces the search space (clusters)
  • PQ reduces memory (compression)
  • Combined = extreme scale with acceptable accuracy

📊 Benchmark: PQ in production

Scenario A: 5M vectors

ConfigurationAccuracyLatencyMemoryBuild Time
HNSW98%30 ms24 GB ❌50 min
IVF93%50 ms10 GB15 min
PQ (m=8)88%35 ms5 GB ✅60 min
IVF+PQ90%45 ms3 GB ✅70 min

Conclusion: PQ or IVF+PQ are viable when HNSW requires too much RAM.

Scenario B: 50M vectors

ConfigurationAccuracyLatencyMemoryViability
HNSW98%80 ms240 GB ❌Impractical
IVF92%90 ms100 GB ❌Very expensive
IVF+PQ89%85 ms25 GB ✅Viable

Conclusion: IVF+PQ is the only viable option for 50M+ vectors on reasonable hardware.

Scenario C: 100M vectors

ConfigurationMemoryCost (AWS)Accuracy
HNSW480 GB$4000/month ❌98%
IVF+PQ (m=8, nbits=8)50 GB$400/month ✅88%

Trade-off: 10% accuracy loss → 10x cost reduction.

Decision: If 88% accuracy is enough → Use IVF+PQ (massive savings).


✅ Advantages and disadvantages of PQ

Advantages

  1. 4-8x less memory

    • 1M vectors: 6 GB → 1 GB (HNSW vs PQ)
    • Lets you handle 10M-100M vectors in reasonable RAM
  2. Economical

    • Reduces hardware/cloud cost dramatically
    • AWS: $4000/month → $400/month (10x)
  3. Acceptable latency

    • 20-40ms (vs 15ms HNSW)
    • Enough for many use cases
  4. Combinable with IVF

    • IVF+PQ = extreme scale
    • Best of both worlds

Disadvantages

  1. Lower accuracy (85-90%)

    • vs 98% HNSW
    • Can be a deal-breaker if accuracy is critical
  2. Slow build time

    • Computing the codebooks with k-means takes time
    • 10-15 min for 1M vectors
  3. Requires training

    • You need a representative dataset
    • If the distribution changes → rebuild the codebooks
  4. No incremental updates

    • Adding vectors requires rebuilding the codebooks
    • Better for batch updates

🔗 Connection with RAG

When to use PQ in RAG

Signs you need PQ:

  1. Dataset >5M vectors

    • HNSW requires >40 GB RAM (impractical)
    • PQ reduces it to 8 GB
  2. 85-90% accuracy is enough

    • Not critically customer-facing
    • Internal search, analytics, recommendations
  3. Limited budget

    • Cloud cost is critical
    • Self-hosted with limited RAM
  4. Batch processing

    • You don't need latency <100ms
    • Rebuilding codebooks nightly is OK

RAG example: Massive internal knowledge base (50M Confluence + Slack + Jira).

Tiered architecture (HNSW + IVF + PQ)

An advanced system can use multiple indexes:

Tier 1: Hot data (last 7 days, 50K vectors)

  • HNSW for high accuracy (98%)
  • Latency: 15ms

Tier 2: Warm data (last 30 days, 500K vectors)

  • IVF for balance (93% accuracy)
  • Latency: 30ms

Tier 3: Cold data (historical, 10M vectors)

  • IVF+PQ for memory efficiency (88% accuracy)
  • Latency: 60ms

Query strategy:

  1. Search Tier 1 (HNSW)
  2. If not enough results → Search Tier 2 (IVF)
  3. If still not enough → Search Tier 3 (IVF+PQ)

✅ Comprehension checklist

Verify that you understood this capsule:

  • What is Product Quantization?

    • Answer: A compression technique that divides vectors into sub-vectors, then replaces each sub-vector with a code from a codebook.
  • Why does PQ reduce memory 4-8x?

    • Answer: A 1536-float vector (6 KB) is compressed to 8 codes (8 bytes). The codebook overhead reduces effective compression to 4-8x.
  • What does the m parameter do?

    • Answer: The number of sub-vectors. High m = better accuracy + less compression. Low m = more compression + lower accuracy.
  • When to use PQ vs HNSW?

    • Answer: PQ when >5M vectors, RAM critical, 85-90% accuracy is enough. HNSW when accuracy is critical (>95%), <1M vectors.
  • Why combine IVF + PQ?

    • Answer: IVF reduces the search space (clusters), PQ reduces memory (compression). Combined = extreme scale (100M+ vectors).

If you answered 4-5/5 correctly → ✅ Ready for Capsule 07 (Comparison)


🚀 Next step

Now that you know HNSW, IVF, and PQ individually, we'll compare them directly.

Next capsule: 07 - HNSW vs IVF vs PQ comparison

You'll learn:

  • Decision tree: Which algorithm to choose based on requirements
  • Side-by-side benchmarks in real scenarios
  • Recommended configuration per RAG use case
  • Migration path (HNSW → IVF → IVF+PQ)

Key: A decision framework for choosing the right algorithm in your RAG system.


Reading time: 8-10 minutes
Next: 07-algorithm-comparison.md