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:

  1. Load time: 30-60s para cargar 100K vectores en memoria
  2. Reinicio frecuente: Cada deploy/crash = 30-60s downtime
  3. 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:

  1. Metadata filtering: No puedes filtrar por fecha, autor, categoría antes de search
  2. Hybrid search: No combina keyword + semantic search
  3. Batch operations: Add/update de 1000 docs = 1000 operaciones separadas
  4. Backup/restore: Solo manual (copy .npy files)
  5. Monitoring: Sin métricas de performance, usage, latency
  6. 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:

  1. numpy es brute force O(n): 1M vectores = 1.5s (vs 12ms con ChromaDB = 126x más lento)
  2. numpy limitado por RAM: 1M vectores = 6GB RAM (vs ChromaDB: 2GB RAM + disk)
  3. numpy sin persistencia robusta: Recarga 30-60s después de reinicio (vs <1ms ChromaDB)
  4. numpy sin concurrencia: Requiere locks manuales (vs ChromaDB thread-safe)
  5. 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

  1. NumPy Performance Tips - Optimización de numpy
  2. Why Vector Databases - numpy vs vector DB
  3. ChromaDB Quickstart - Alternativa a numpy
  4. Semantic Search with NumPy - Tutorial completo
  5. Production RAG Considerations - Por qué numpy no escala
  6. FAISS vs NumPy Benchmark - Comparación detallada

Tiempo de lectura: 8-10 minutos
Siguiente: 05-cuando-si-necesitas-vector-database.md