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ón | Accuracy | Memoria (1M vecs) |
|---|---|---|
| Sin PQ (original) | 100% | 6 GB |
| PQ m=8, nbits=8 | 88-92% | 1 GB (6x compresión) |
| PQ m=16, nbits=8 | 85-90% | 2 GB (3x compresión) |
| PQ m=8, nbits=4 | 80-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ón | HNSW | IVF | PQ | IVF + PQ |
|---|---|---|---|---|
| Complejidad | O(log n) | O(sqrt(n)) | O(n) compressed | O(sqrt(n)) compressed |
| Accuracy | 95-99% | 90-95% | 85-90% | 85-92% |
| Latency (1M) | 15-20 ms | 30-50 ms | 20-40 ms | 25-45 ms |
| Memoria (1M) | 4-8 GB | 1-2 GB | 0.5-1 GB | 0.3-0.6 GB |
| Compresión | 1x | 1x | 6x | 8x |
| Build time | 5-10 min | 2-3 min | 10-15 min | 15-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ón | Accuracy | Latency | Memoria | Build Time |
|---|---|---|---|---|
| HNSW | 98% | 30 ms | 24 GB ❌ | 50 min |
| IVF | 93% | 50 ms | 10 GB | 15 min |
| PQ (m=8) | 88% | 35 ms | 5 GB ✅ | 60 min |
| IVF+PQ | 90% | 45 ms | 3 GB ✅ | 70 min |
Conclusión: PQ o IVF+PQ son viables cuando HNSW requiere demasiada RAM.
Escenario B: 50M vectores
| Configuración | Accuracy | Latency | Memoria | Viabilidad |
|---|---|---|---|---|
| HNSW | 98% | 80 ms | 240 GB ❌ | Impracticable |
| IVF | 92% | 90 ms | 100 GB ❌ | Muy costoso |
| IVF+PQ | 89% | 85 ms | 25 GB ✅ | Viable |
Conclusión: IVF+PQ es única opción viable para 50M+ vectores en hardware razonable.
Escenario C: 100M vectores
| Configuración | Memoria | Costo (AWS) | Accuracy |
|---|---|---|---|
| HNSW | 480 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
-
Memoria 4-8x menor
- 1M vectores: 6 GB → 1 GB (HNSW vs PQ)
- Permite manejar 10M-100M vectores en RAM razonable
-
Económico
- Reduce costo de hardware/cloud dramáticamente
- AWS: $4000/month → $400/month (10x)
-
Latencia aceptable
- 20-40ms (vs 15ms HNSW)
- Suficiente para muchos casos de uso
-
Combinable con IVF
- IVF+PQ = scale extremo
- Best of both worlds
Desventajas
-
Accuracy menor (85-90%)
- vs 98% HNSW
- Puede ser deal-breaker si accuracy crítica
-
Build time lento
- Calcular codebooks con k-means tarda tiempo
- 10-15 min para 1M vectores
-
Requiere training
- Necesitas dataset representativo
- Si distribution cambia → rebuild codebooks
-
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:
-
Dataset >5M vectores
- HNSW requiere >40 GB RAM (impracticable)
- PQ reduce a 8 GB
-
Accuracy 85-90% es suficiente
- No es customer-facing crítico
- Internal search, analytics, recommendations
-
Presupuesto limitado
- Cloud cost es crítico
- Self-hosted con RAM limitado
-
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:
- Search Tier 1 (HNSW)
- If not enough results → Search Tier 2 (IVF)
- 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