Módulo 2: Cómo funcionan Vector Databases (Conceptual)

Cápsula 06: PQ - Product Quantization

🎯 Objetivo de la cápsula

Entender CÓMO funciona PQ (Product Quantization) para comprimir vectores 4-8x, reduciendo memoria dramáticamente con trade-off de accuracy.

Al finalizar esta cápsula:

  • ✅ Explicarás qué es Product Quantization (codebook compression)
  • ✅ Entenderás trade-off: 4-8x menos memoria vs 85-90% accuracy
  • ✅ Conocerás parámetros clave (m, nbits)
  • ✅ Decidirás cuándo usar PQ (>5M vectores, RAM crítico)

Tiempo estimado: 8-10 minutos


📦 ¿Qué es Product Quantization?

Definición

PQ = Product Quantization

Es técnica de compresión de vectores que divide vector en sub-vectores, luego reemplaza cada sub-vector con código (index) de codebook.

Resultado: Vector comprimido 4-8x más pequeño con accuracy ligeramente menor.

Analogía: Compresión de imágenes (JPEG)

Imagen sin comprimir (PNG):

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

Imagen comprimida (JPEG):

  • Divide imagen en bloques 8x8
  • Reemplaza bloques similares con códigos
  • Resultado: 300 KB (10x compresión)
  • Trade-off: Calidad 90% (artifacts visibles al zoom)

Aplicado a vectores:

  • Vector original: 1536 dims × 4 bytes = 6 KB
  • Vector comprimido (PQ): 1536 dims → 192 códigos × 1 byte = 192 bytes
  • Resultado: 32x compresión
  • Trade-off: Accuracy 85-90% (vs 100% original)

🏗️ Arquitectura de PQ

Step 1: División en sub-vectores

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

# Dividir en m sub-vectores
m = 8  # Número de sub-vectores
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)
]

Clave: Vector de 1536 dims se divide en 8 sub-vectores de 192 dims cada uno.

Step 2: Construcción de codebook (Training)

Codebook es diccionario que mapea sub-vectores a códigos.

# Para cada sub-vector, ejecutar k-means
def build_codebook(all_vectors, m=8, k=256):
    codebooks = []
    
    for i in range(m):  # Para cada sub-vector position
        # Extraer sub-vectors de todos los vectores
        sub_vectors = [v[i] for v in all_vectors]
        
        # k-means para encontrar k centroids
        centroids = kmeans(sub_vectors, k=256)
        
        # Almacenar codebook
        codebooks[i] = centroids  # 256 centroids para sub-vector i
    
    return codebooks

Resultado:

  • 8 codebooks (uno por sub-vector)
  • Cada codebook: 256 centroids (192 dims cada uno)

Visualización:

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 (Comprimir vectores)

def encode_vector(vector, codebooks, m=8):
    codes = []
    
    for i in range(m):  # Para cada sub-vector
        sub_vector = vector[i]
        
        # Encontrar centroid más cercano en 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]

Resultado:

  • Vector original: 1536 floats × 4 bytes = 6144 bytes
  • Vector comprimido: 8 codes × 1 byte = 8 bytes
  • Compresión: 768x

Pero en práctica: También almacenas codebooks (overhead), compresión efectiva es ~4-8x.

Step 4: Búsqueda con vectores comprimidos

def search_pq(query, compressed_vectors, codebooks, k=10):
    # 1. Encode query usando codebooks
    query_codes = encode_vector(query, codebooks)
    
    # 2. Calcular asymmetric distance
    # (query original vs vectores comprimidos)
    similarities = []
    for compressed_vec in compressed_vectors:
        # Calcular distancia usando codebooks
        dist = asymmetric_distance(query, compressed_vec, codebooks)
        similarities.append(dist)
    
    # 3. Devolver top-k
    return top_k(similarities, k)

Clave: Query NO se comprime (usa vector original). Solo database vectors están comprimidos.


📊 Trade-off: Accuracy vs Memoria

Accuracy loss

Por qué accuracy baja:

Cuando reemplazas sub-vector con centroid más cercano, pierdes precisión.

Ejemplo:

Sub-vector original:     [0.123, 0.456, 0.789]
Centroid más cercano:    [0.120, 0.450, 0.800]
Error:                   [0.003, 0.006, 0.011]

Multiplicado por 8 sub-vectores → Error acumulado.

Accuracy típica:

ConfiguraciónAccuracyMemoria (1M vecs)
Sin PQ (original)100%6 GB
PQ m=8, nbits=888-92%1 GB (6x compresión)
PQ m=16, nbits=885-90%2 GB (3x compresión)
PQ m=8, nbits=480-85%0.5 GB (12x compresión)

Memoria saving

Cálculo:

Vector original:
- Dims: 1536
- Type: float32 (4 bytes)
- Tamaño: 1536 × 4 = 6144 bytes = 6 KB

Vector comprimido (PQ):
- Sub-vectors: m = 8
- Bits per code: nbits = 8 (1 byte)
- Tamaño: 8 × 1 = 8 bytes

Compresión: 6144 / 8 = 768x

En práctica con overhead:

  • Codebooks: 8 × 256 × 192 × 4 = 1.5 MB (compartido)
  • Compresión efectiva: ~6x para 1M vectores

⚙️ Parámetros clave de PQ

1. m (Número de sub-vectores)

Definición: En cuántos sub-vectores dividir vector original.

Impacto:

  • m alto (16, 32):

    • ✅ Mejor accuracy (sub-vectores más pequeños = centroids más precisos)
    • ❌ Menor compresión (más códigos que almacenar)
    • ❌ Query más lento (más lookups en codebook)
  • m bajo (4, 8):

    • ✅ Mayor compresión
    • ✅ Query rápido
    • ❌ Accuracy menor

Regla práctica: m = 8 (balance accuracy/compresión)

2. nbits (Bits por código)

Definición: Cuántos bits usar para cada código (afecta tamaño de codebook).

Impacto:

  • nbits alto (8, 16):

    • ✅ Codebook grande (256, 65536 centroids)
    • ✅ Mejor accuracy (más centroids = mejor representación)
    • ❌ Más memoria para codebooks
  • nbits bajo (4, 6):

    • ✅ Codebook pequeño (16, 64 centroids)
    • ✅ Menos memoria
    • ❌ Accuracy menor

Regla práctica: nbits = 8 (256 centroids, balance)

Trade-offs visualizados

Accuracy vs Compression

High Accuracy   m=16, nbits=8
(90%)          ▲  ❌ Compresión 3x
               │
Medium         │  m=8, nbits=8
Accuracy       │  ⚖️ Compresión 6x
(88%)          │  ← Recomendado
               │
Low Accuracy   │  m=8, nbits=4
(82%)          │  ✅ Compresión 12x
               └─────────────────
                 Low ← Memoria → High

🆚 Comparación: HNSW vs IVF vs PQ

Tabla comparativa completa

DimensiónHNSWIVFPQIVF + PQ
ComplejidadO(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
Memoria (1M)4-8 GB1-2 GB0.5-1 GB0.3-0.6 GB
Compresión1x1x6x8x
Build time5-10 min2-3 min10-15 min15-20 min
Mejor para<1M, accuracy crítica>1M, memoria limitada>5M, RAM crítico>10M, scale extremo

Cuándo usar cada uno

HNSW:

  • ✅ <1M vectores
  • ✅ Accuracy >95% requerida
  • ✅ Tienes RAM suficiente

IVF:

  • ✅ 1-5M vectores
  • ✅ Accuracy 90% OK
  • ✅ Memoria moderada

PQ:

  • ✅ >5M vectores
  • ✅ Accuracy 85-90% suficiente
  • ✅ RAM muy limitado

IVF + PQ:

  • ✅ >10M vectores
  • ✅ Accuracy 85-90% suficiente
  • ✅ Maximum scale (100M+ vectores)

🏭 PQ en la práctica: Faiss

PQ standalone

import faiss
import numpy as np

# 1. Crear índice PQ
dimension = 1536
m = 8  # Sub-vectores
nbits = 8  # Bits per code (256 centroids)

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

# 2. Train (construir 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)

# Memoria: ~1 GB (vs 6 GB sin PQ)
# Accuracy: ~88%

IVF + PQ (Combinado)

Mejor configuración para scale extremo:

# IVF + PQ combinado
dimension = 1536
nlist = 1000  # IVF clusters
m = 8  # PQ sub-vectores
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)

# Memoria: ~600 MB para 1M vectores (10x compresión)
# Accuracy: ~90%

Trade-off:

  • IVF reduce search space (clusters)
  • PQ reduce memoria (compresión)
  • Combinados = scale extremo con accuracy aceptable

📊 Benchmark: PQ en producción

Escenario A: 5M vectores

ConfiguraciónAccuracyLatencyMemoriaBuild 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

Conclusión: PQ o IVF+PQ son viables cuando HNSW requiere demasiada RAM.

Escenario B: 50M vectores

ConfiguraciónAccuracyLatencyMemoriaViabilidad
HNSW98%80 ms240 GB ❌Impracticable
IVF92%90 ms100 GB ❌Muy costoso
IVF+PQ89%85 ms25 GB ✅Viable

Conclusión: IVF+PQ es única opción viable para 50M+ vectores en hardware razonable.

Escenario C: 100M vectores

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

Trade-off: 10% accuracy loss → 10x costo reducción.

Decisión: Si accuracy 88% es suficiente → Usar IVF+PQ (ahorro masivo).


✅ Ventajas y desventajas de PQ

Ventajas

  1. Memoria 4-8x menor

    • 1M vectores: 6 GB → 1 GB (HNSW vs PQ)
    • Permite manejar 10M-100M vectores en RAM razonable
  2. Económico

    • Reduce costo de hardware/cloud dramáticamente
    • AWS: $4000/month → $400/month (10x)
  3. Latencia aceptable

    • 20-40ms (vs 15ms HNSW)
    • Suficiente para muchos casos de uso
  4. Combinable con IVF

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

Desventajas

  1. Accuracy menor (85-90%)

    • vs 98% HNSW
    • Puede ser deal-breaker si accuracy crítica
  2. Build time lento

    • Calcular codebooks con k-means tarda tiempo
    • 10-15 min para 1M vectores
  3. Requiere training

    • Necesitas dataset representativo
    • Si distribution cambia → rebuild codebooks
  4. No incremental updates

    • Agregar vectores requiere rebuild codebooks
    • Mejor para batch updates

🔗 Conexión con RAG

Cuándo usar PQ en RAG

Señales que necesitas PQ:

  1. Dataset >5M vectores

    • HNSW requiere >40 GB RAM (impracticable)
    • PQ reduce a 8 GB
  2. Accuracy 85-90% es suficiente

    • No es customer-facing crítico
    • Internal search, analytics, recommendations
  3. Presupuesto limitado

    • Cloud cost es crítico
    • Self-hosted con RAM limitado
  4. Batch processing

    • No necesitas latencia <100ms
    • Rebuild codebooks nightly OK

Ejemplo RAG: Knowledge base interno masivo (50M Confluence + Slack + Jira).

Arquitectura tiered (HNSW + IVF + PQ)

Sistema avanzado puede usar múltiples índices:

Tier 1: Hot data (últimos 7 días, 50K vectores)

  • HNSW para accuracy alta (98%)
  • Latencia: 15ms

Tier 2: Warm data (últimos 30 días, 500K vectores)

  • IVF para balance (93% accuracy)
  • Latencia: 30ms

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

  • IVF+PQ para memoria eficiente (88% accuracy)
  • Latencia: 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)

✅ Checklist de comprensión

Verifica que entendiste esta cápsula:

  • ¿Qué es Product Quantization?

    • Respuesta: Técnica de compresión que divide vectores en sub-vectores, luego reemplaza cada sub-vector con código de codebook.
  • ¿Por qué PQ reduce memoria 4-8x?

    • Respuesta: Vector de 1536 floats (6 KB) se comprime a 8 códigos (8 bytes). Overhead de codebooks reduce compresión efectiva a 4-8x.
  • ¿Qué hace parámetro m?

    • Respuesta: Número de sub-vectores. m alto = mejor accuracy + menor compresión. m bajo = mayor compresión + accuracy menor.
  • ¿Cuándo usar PQ vs HNSW?

    • Respuesta: PQ cuando >5M vectores, RAM crítico, accuracy 85-90% suficiente. HNSW cuando accuracy crítica (>95%), <1M vectores.
  • ¿Por qué combinar IVF + PQ?

    • Respuesta: IVF reduce search space (clusters), PQ reduce memoria (compresión). Combinados = scale extremo (100M+ vectores).

Si respondiste 4-5/5 correctamente → ✅ Listo para Cápsula 07 (Comparación)


🚀 Siguiente paso

Ahora que conoces HNSW, IVF, y PQ individualmente, compararemos directamente.

Próxima cápsula: 07 - Comparación HNSW vs IVF vs PQ

Aprenderás:

  • Decision tree: Cuál algoritmo elegir según requisitos
  • Benchmarks side-by-side en escenarios reales
  • Configuración recomendada por caso de uso RAG
  • Migration path (HNSW → IVF → IVF+PQ)

Clave: Framework de decisión para elegir algoritmo correcto en tu RAG system.


Tiempo de lectura: 8-10 minutos
Siguiente: 07-comparacion-algoritmos.md