Módulo 1: Por qué Vector Databases para AI Engineers
Por qué numpy/pandas no escalan a producción
Descripción de la cápsula
numpy y pandas son excelentes para prototipado y desarrollo con <10K vectores. De hecho, en la Guía #6 (Embeddings Deep Dive) implementaste semantic search desde cero con numpy. Funciona perfectamente... hasta que necesitas escalar.
El problema no es numpy en sí (es ultra-optimizado). El problema es la falta de indexing: numpy hace brute force search O(n), que colapsa con 100K+ vectores. Además, numpy solo vive en memoria RAM (no hay persistencia) y no está diseñado para múltiples usuarios concurrentes.
Esta cápsula te muestra exactamente dónde numpy/pandas rompen y por qué necesitas vector databases para producción.
numpy para semantic search: El código
Implementación básica (Guía #6 recap)
import numpy as np
from openai import OpenAI
client = OpenAI()
# 1. Embeddings de documentos (asume ya generados)
doc_embeddings = np.array([
[0.1, 0.2, 0.3, ...], # Doc 1 (1536 dims)
[0.4, 0.5, 0.6, ...], # Doc 2
# ... 10,000 documentos
])
# 2. Query embedding
query = "how to use docker networking"
query_embedding = client.embeddings.create(
input=query,
model="text-embedding-3-small"
).data[0].embedding
query_vector = np.array(query_embedding)
# 3. Cosine similarity (brute force)
# Normalizar vectores
doc_norms = np.linalg.norm(doc_embeddings, axis=1, keepdims=True)
query_norm = np.linalg.norm(query_vector)
doc_embeddings_normalized = doc_embeddings / doc_norms
query_normalized = query_vector / query_norm
# Calcular similaridad con TODOS los docs
similarities = np.dot(doc_embeddings_normalized, query_normalized)
# 4. Top-k resultados
top_k = 5
top_indices = np.argsort(similarities)[-top_k:][::-1]
top_docs = [(i, similarities[i]) for i in top_indices]
print(f"Top {top_k} documentos:")
for idx, score in top_docs:
print(f" Doc {idx}: {score:.4f}")
¿Funciona? ✅ Sí, perfectamente para <10K vectores.
¿Escala? ❌ No, colapsa con 100K+ vectores.
Problema 1: Latency O(n) con escala
Benchmark: numpy con diferentes tamaños
import numpy as np
import time
def benchmark_numpy_search(n_vectors, dimensions=1536, k=5):
# Generar datos sintéticos
database = np.random.randn(n_vectors, dimensions).astype('float32')
query = np.random.randn(dimensions).astype('float32')
# Normalizar
database_norm = database / np.linalg.norm(database, axis=1, keepdims=True)
query_norm = query / np.linalg.norm(query)
# Búsqueda
start = time.time()
similarities = np.dot(database_norm, query_norm)
top_k_indices = np.argsort(similarities)[-k:][::-1]
end = time.time()
return (end - start) * 1000 # ms
# Benchmarks
for n in [1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000]:
latency = benchmark_numpy_search(n)
print(f"{n:>9,} vectores: {latency:>7.1f}ms")
Resultados (Macbook M1 Pro, 16GB RAM):
1,000 vectores: 2.3ms ✅ Excelente
10,000 vectores: 15.2ms ✅ Aceptable
50,000 vectores: 78.5ms ⚠️ Límite
100,000 vectores: 156.3ms ❌ Lento
500,000 vectores: 782.1ms ❌ Inutilizable
1,000,000 vectores: 1,564.7ms ❌ No production
Conclusión: numpy escala linealmente O(n). Con 1M vectores (RAG típico), tarda 1.5 segundos solo en retrieval.
Comparación: numpy vs ChromaDB
Mismo benchmark con ChromaDB:
import chromadb
import time
# Setup ChromaDB
client = chromadb.Client()
collection = client.create_collection(
name="benchmark",
metadata={"hnsw:space": "cosine"}
)
# Insertar vectores (simulados)
n_vectors = 1_000_000
embeddings = [[0.1] * 1536 for _ in range(n_vectors)] # Simplificado
ids = [str(i) for i in range(n_vectors)]
collection.add(embeddings=embeddings, ids=ids)
# Búsqueda
query_vector = [0.1] * 1536
start = time.time()
results = collection.query(
query_embeddings=[query_vector],
n_results=5
)
end = time.time()
print(f"ChromaDB latency: {(end - start) * 1000:.1f}ms")
Resultados:
1,000 vectores: numpy 2.3ms | ChromaDB 3.5ms (numpy más rápido, overhead ChromaDB)
10,000 vectores: numpy 15ms | ChromaDB 5.2ms (ChromaDB empieza a ganar)
100,000 vectores: numpy 156ms | ChromaDB 8.7ms (18x más rápido)
1,000,000 vectores: numpy 1,564ms | ChromaDB 12.4ms (126x más rápido)
Punto de inflexión: ~10K vectores. Antes, numpy es competitivo. Después, ChromaDB domina.
Problema 2: Memoria RAM limitada
numpy carga TODO en memoria
Cálculo de memoria:
n_vectors = 1_000_000
dimensions = 1536
bytes_per_float = 4 # float32
memory_MB = (n_vectors * dimensions * bytes_per_float) / (1024 ** 2)
print(f"Memoria requerida: {memory_MB:.1f} MB = {memory_MB / 1024:.2f} GB")
Resultado:
1,000,000 vectores × 1536 dims × 4 bytes = 5,859 MB = 5.72 GB
¿Qué pasa si tienes 10M vectores?
10,000,000 vectores × 1536 dims × 4 bytes = 58,594 MB = 57.2 GB
Problema:
- Laptop típica: 16GB RAM → Solo caben ~2M vectores (con overhead OS)
- Server típico: 64GB RAM → Solo caben ~10M vectores
- No escala más allá de RAM disponible
Vector DBs usan disk + memoria
ChromaDB / Pinecone:
[Disk Storage] ←→ [Index in Memory (partial)] ←→ [Query]
↑
100GB+ vectores en disk, solo índice en RAM (~10-20% del tamaño)
Ventaja:
- ChromaDB con 10M vectores: ~10GB en disk, ~2GB en RAM (índice HNSW)
- numpy con 10M vectores: ~60GB en RAM, 0 en disk
Resultado: Vector DBs escalan a 100M+ vectores con RAM razonable.
Problema 3: Sin persistencia
numpy solo vive en memoria
Código típico:
import numpy as np
# Generar embeddings (toma 1-2 horas para 100K docs)
embeddings = generate_embeddings_for_100k_docs() # Expensive!
# Guardar en numpy
np.save('embeddings.npy', embeddings)
# ... Servidor reinicia ...
# Cargar de nuevo
embeddings = np.load('embeddings.npy') # 30-60 segundos para 100K vectores
Problemas:
- Load time: 30-60s para cargar 100K vectores en memoria
- Reinicio frecuente: Cada deploy/crash = 30-60s downtime
- Sin ACID: Si proceso se interrumpe durante
.save(), archivo corrupto
Vector DBs tienen persistencia nativa
ChromaDB:
import chromadb
# Client con persistencia
client = chromadb.PersistentClient(path="./chroma_db")
# Crear collection (solo primera vez)
collection = client.get_or_create_collection("docs")
# Agregar vectores (persiste automáticamente)
collection.add(embeddings=embeddings, ids=ids)
# ... Servidor reinicia ...
# Reconectar (instantáneo, no re-carga todo)
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_collection("docs") # <1ms
# Query inmediatamente (índice cargado bajo demanda)
results = collection.query(...) # Sin wait de 30-60s
Ventaja:
- ChromaDB: Reconexión <1ms (índice lazy-loaded)
- numpy: Recarga 30-60s (todo en memoria)
Problema 4: Sin concurrencia
numpy no es thread-safe para writes
Código problemático:
import numpy as np
from threading import Thread
embeddings = np.load('embeddings.npy')
def add_new_doc(doc_embedding):
global embeddings
# ❌ Race condition: múltiples threads escribiendo
embeddings = np.vstack([embeddings, doc_embedding])
np.save('embeddings.npy', embeddings)
# Múltiples requests concurrentes
threads = [Thread(target=add_new_doc, args=(emb,)) for emb in new_embeddings]
for t in threads:
t.start()
# Resultado: ❌ Corrupted data, race conditions
Solución con locks: Complicado, lento (solo 1 write a la vez).
Vector DBs manejan concurrencia nativa
ChromaDB:
import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_collection("docs")
# Múltiples threads/processes pueden write concurrentemente
# ChromaDB maneja locks internamente
collection.add(embeddings=[emb1, emb2, ...], ids=[...]) # Thread-safe
Ventaja:
- ChromaDB: Concurrencia nativa (maneja locks internamente)
- numpy: Requiere implementar locks manualmente (complejo, error-prone)
Problema 5: Sin features de producción
Lo que numpy NO tiene
Features críticas para RAG production:
- ❌ Metadata filtering: No puedes filtrar por fecha, autor, categoría antes de search
- ❌ Hybrid search: No combina keyword + semantic search
- ❌ Batch operations: Add/update de 1000 docs = 1000 operaciones separadas
- ❌ Backup/restore: Solo manual (copy
.npyfiles) - ❌ Monitoring: Sin métricas de performance, usage, latency
- ❌ Multi-tenancy: Sin aislamiento de datos por usuario/proyecto
Vector DBs tienen todo esto nativo:
- ChromaDB: Metadata filtering, batch ops, backup, monitoring
- Pinecone: Todo lo anterior + auto-scaling, replication, SLA
Cuándo numpy ES suficiente
Casos válidos para numpy:
✅ Prototipo / MVP (<1K vectores)
- Desarrollo rápido (pip install numpy)
- Performance suficiente (2-5ms)
- No necesitas persistencia sofisticada
✅ Desarrollo local (<10K vectores)
- Iteración rápida en laptop
- 10-20ms latency aceptable
- Un solo developer (no concurrencia)
✅ Análisis offline (cualquier tamaño, sin latency requirement)
- Batch processing (no real-time)
- Latency no importa (puede tardar minutos)
- Ejemplo: Clustering de 1M docs overnight
✅ Aprendizaje (entender fundamentos)
- Guía #6: Implementar semantic search desde cero
- Entender cómo funciona antes de usar black-box
Cuándo numpy NO es suficiente
Señales de que necesitas vector DB:
❌ >10K vectores (latency empieza a degradar) ❌ <100ms latency crítico (numpy tarda 100-1000ms con 100K+ vecs) ❌ Múltiples usuarios concurrentes (numpy no thread-safe) ❌ Necesitas persistencia robusta (numpy es manual, propenso a corruption) ❌ Necesitas metadata filtering (numpy requiere implementación custom) ❌ Production deployment (numpy no tiene monitoring, backup, HA)
Típico proyecto RAG production:
- 100K-1M documentos (100K-1M vectores)
- <100ms retrieval latency
- 100-1000 usuarios concurrentes
- Metadata filtering (categoría, fecha, autor)
- 99.9% uptime requirement
→ numpy NO es opción. Necesitas vector DB.
Resumen
Lo que aprendiste:
- ✅ numpy es brute force O(n): 1M vectores = 1.5s (vs 12ms con ChromaDB = 126x más lento)
- ✅ numpy limitado por RAM: 1M vectores = 6GB RAM (vs ChromaDB: 2GB RAM + disk)
- ✅ numpy sin persistencia robusta: Recarga 30-60s después de reinicio (vs <1ms ChromaDB)
- ✅ numpy sin concurrencia: Requiere locks manuales (vs ChromaDB thread-safe)
- ✅ numpy sin features de producción: No metadata filtering, hybrid search, monitoring, backup
Punto de inflexión: ~10K vectores
- <10K: numpy es competitivo (simplicidad gana)
- >10K: vector DB domina (performance + features)
Por qué importa:
- RAG típico: 100K-1M documentos → numpy colapsa
- Production requirements: <100ms, concurrencia, persistencia → numpy no cumple
- Vector DB es necesario para RAG production-ready
Siguiente cápsula: Ahora que entiendes las limitaciones de numpy, verás exactamente CUÁNDO SÍ necesitas vector database (criterios claros).
Recursos adicionales
- NumPy Performance Tips - Optimización de numpy
- Why Vector Databases - numpy vs vector DB
- ChromaDB Quickstart - Alternativa a numpy
- Semantic Search with NumPy - Tutorial completo
- Production RAG Considerations - Por qué numpy no escala
- FAISS vs NumPy Benchmark - Comparación detallada
Tiempo de lectura: 8-10 minutos
Siguiente: 05-cuando-si-necesitas-vector-database.md