Módulo 4: ChromaDB Setup y Configuración
Cápsula 03: Collection Configuration — los tres parámetros que casi nadie justifica
Descripción de la cápsula
Cuando creás una collection en ChromaDB con client.create_collection("docs"), estás aceptando silenciosamente cuatro decisiones que el sistema toma por vos: la métrica de distancia (cosine), el algoritmo de indexing (HNSW), la cantidad de conexiones por nodo del grafo (M=16) y la calidad del build del índice (construction_ef=100). Esos defaults son razonables — para datasets pequeños y casos de uso típicos. Para producción real, los defaults son el equivalente a comprar un auto sin elegir el motor: vas a llegar a destino, pero con la performance de fábrica.
Esta cápsula te explica los tres parámetros HNSW configurables (hnsw:space, hnsw:M, hnsw:construction_ef) que definen el rendimiento de tu vector database antes de insertar el primer documento. La distance metric ya la cubrimos en M03/06 — acá nos enfocamos en M y construction_ef, que son las dos perillas que cambian el balance accuracy/memoria/latencia. Vas a aprender qué controla cada uno, qué valores elegir según tu caso, y por qué no podés cambiarlos después de insertar datos sin re-construir el índice completo.
Al finalizar esta cápsula serás capaz de:
- ✅ Explicar qué controla cada parámetro HNSW:
M,construction_ef,search_ef - ✅ Elegir valores justificados según tres escenarios: prototipo, producción típica, sistemas críticos
- ✅ Calcular el impacto de cambiar
Men consumo de RAM (16 → 32 ≈ +50%) - ✅ Diferenciar entre parámetros build-time (no se pueden cambiar después) y runtime (sí se pueden)
- ✅ Benchmarkear configuraciones distintas para validar el trade-off antes de comprometerse
- ✅ Anticipar el error más caro: cambiar
Men una collection con datos y descubrir que requiere rebuild completo
Tiempo estimado: 30-35 minutos
El modelo mental: ¿qué controla cada parámetro?
HNSW construye un grafo en capas. Cada vector es un nodo. Los nodos están conectados a otros nodos cercanos en el espacio vectorial. Para buscar, vas saltando de nodo en nodo siguiendo las conexiones, hasta encontrar el cluster correcto.
Los tres parámetros controlan distintos aspectos de ese grafo:
┌─────────────────────────────────────────────────────┐
│ │
│ M (build-time): cuántas conexiones tiene cada nodo │
│ ┌─────┐ Más conexiones = │
│ │ ●───┤ - Mejor recall │
│ │ ●───┤ - Más RAM │
│ │ ●───┤ - Build más lento │
│ └─────┘ │
│ │
│ construction_ef (build-time): qué tan exhaustivo │
│ busca conexiones cuando construye el grafo │
│ Más alto = grafo de mejor calidad │
│ + build más lento │
│ │
│ search_ef (runtime): qué tan exhaustivo busca │
│ cuando se hace una query │
│ Más alto = mejor recall + más latencia │
│ │
└─────────────────────────────────────────────────────┘
Distinción crítica:
Myconstruction_efse aplican al construir el grafo (cuando insertás vectores). Una vez insertado, no podés cambiarlos sin reconstruir el índice completo.search_efse aplica al hacer queries. Lo podés cambiar dinámicamente sin tocar los datos.
Si elegís mal M y descubrís el problema con 1M vectores ya insertados, te toca re-embebir y re-insertar todo. Si elegís mal search_ef, lo cambiás en una línea de configuración. Por eso M merece más atención al inicio.
Parámetro 1: hnsw:space (distance metric)
Ya cubierto en detalle en M03/06. Resumen:
| Valor | Cuándo usarlo |
|---|---|
"cosine" | Default y recomendado para text embeddings (OpenAI, Cohere, Sentence Transformers) |
"l2" | Image embeddings tradicionales (ResNet), face recognition |
"ip" | Vectores ya normalizados; optimización de velocidad |
Si estás haciendo RAG con OpenAI o cualquier text embedding moderno, usá cosine y olvidate. Para profundización, releé M03/06.
import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.create_collection(
name="docs",
metadata={"hnsw:space": "cosine"} # explícito > default implícito
)
Parámetro 2: hnsw:M (conexiones por nodo)
M es el parámetro más impactante. Controla cuántas conexiones tiene cada nodo del grafo HNSW.
Qué cambia con M
M = 8 (bajo):
- Cada nodo se conecta a ~8 vecinos
- Grafo "delgado": pocas opciones para navegar
- Recall típico: 88-92%
- RAM: baseline
- Build time: rápido
M = 16 (default ChromaDB):
- ~16 vecinos por nodo
- Grafo "balanceado"
- Recall típico: 93-96%
- RAM: +30% vs M=8
- Build time: medio
M = 32 (recomendado producción):
- ~32 vecinos por nodo
- Grafo "denso"
- Recall típico: 97-98%
- RAM: +60% vs M=8
- Build time: ~2x M=16
M = 64+ (sistemas críticos):
- Grafo muy denso, retornos decrecientes
- Recall típico: 98-99%
- RAM: +120% vs M=8
- Build time: ~4x M=16
El cálculo de RAM
ChromaDB con HNSW guarda en RAM:
RAM ≈ N × (D × 4 bytes + M × 8 bytes × levels)
Donde:
N = número de vectores
D = dimensiones del vector
4 bytes = float32
8 bytes = puntero por conexión
levels ≈ 4-6 (capas del grafo)
Para 1M vectores de 1536 dimensiones:
M | RAM estimada |
|---|---|
| 8 | ~6.5 GB |
| 16 | ~7.0 GB |
| 32 | ~8.5 GB |
| 64 | ~12 GB |
Para datasets de 100K-1M vectores, la diferencia entre M=16 y M=32 es ~1.5 GB extra. Razonable para producción.
Para datasets de 10M+ vectores, la diferencia se vuelve significativa (15 GB vs 30 GB) y empieza a importar el costo de hosting.
Cómo configurarlo
collection = client.create_collection(
name="prod_docs",
metadata={
"hnsw:space": "cosine",
"hnsw:M": 32, # producción típica
"hnsw:construction_ef": 200, # ver siguiente sección
}
)
Importante: este metadata se fija al crear la collection y no se puede cambiar. Si querés cambiar M, tenés que crear collection nueva y re-insertar.
Parámetro 3: hnsw:construction_ef (calidad del build)
construction_ef controla qué tan exhaustivamente HNSW busca conexiones óptimas al construir el grafo.
Cuando insertás un vector nuevo, HNSW tiene que decidir a qué M vecinos lo conecta. Buscar el "mejor M vecinos" exhaustivamente sería O(n) por inserción — demasiado caro. En su lugar, HNSW hace búsqueda aproximada controlada por construction_ef:
construction_efbajo (50-100): búsqueda rápida pero subóptima — el grafo queda con conexiones "buenas pero no las mejores"construction_efalto (200-400): búsqueda más exhaustiva — el grafo queda con conexiones más cercanas a óptimas
Trade-off:
construction_ef | Build speed | Recall del índice resultante |
|---|---|---|
| 100 (default) | rápido | -3 a -5% vs óptimo teórico |
| 200 (recomendado) | 2x más lento | -1 a -2% |
| 400 | 4x más lento | <1% (cercano al óptimo) |
Recomendación:
- Prototipo / dataset pequeño:
construction_ef=100(default). Suficiente. - Producción típica:
construction_ef=200. La diferencia de calidad vale el tiempo extra de build (que solo pagás una vez). - Sistemas críticos / datasets enormes:
construction_ef=400. Vas a hacer build muy poco frecuentemente, así que la calidad importa más que la velocidad.
Parámetro 4: hnsw:search_ef (calidad del query)
A diferencia de los anteriores, search_ef se aplica en runtime — cada query usa el valor configurado. Y se puede cambiar sin tocar los datos.
# Configurar search_ef en la collection
collection = client.get_or_create_collection(
name="prod_docs",
metadata={
"hnsw:space": "cosine",
"hnsw:M": 32,
"hnsw:construction_ef": 200,
"hnsw:search_ef": 50, # más alto que default 10
}
)
Trade-off:
search_ef | p95 latency típica | Recall@10 |
|---|---|---|
| 10 (default) | ~8 ms | 88-92% |
| 50 | ~16 ms | 94-96% |
| 100 | ~25 ms | 97-98% |
| 200 | ~42 ms | 98-99% |
Recomendación: producción search_ef=50-100. Sweet spot accuracy/latencia. El default de 10 es demasiado bajo para casi cualquier caso real.
Cubrimos esto en detalle en M04/06 (query optimization).
Las tres configuraciones canónicas
Configuración A: prototipo / desarrollo
collection = client.create_collection(
name="dev_docs",
metadata={
"hnsw:space": "cosine",
"hnsw:M": 16, # default
"hnsw:construction_ef": 100, # default
"hnsw:search_ef": 10, # default
}
)
Cuándo usar: estás aprendiendo, dataset <50K vectores, no importa la calidad fina del recall.
Performance esperada (50K vectores):
- Build time: ~30 segundos
- Query p95: ~5ms
- Recall@10: ~93%
- RAM: ~400 MB
Configuración B: producción típica (recomendada para la mayoría)
collection = client.create_collection(
name="prod_docs",
metadata={
"hnsw:space": "cosine",
"hnsw:M": 32,
"hnsw:construction_ef": 200,
"hnsw:search_ef": 50,
}
)
Cuándo usar: RAG en producción con 100K-5M vectores, SLA razonable, accuracy importa.
Performance esperada (1M vectores):
- Build time: ~30 minutos (una sola vez)
- Query p95: ~15ms
- Recall@10: ~97%
- RAM: ~8.5 GB
Esta es la config que vas a usar en 80% de los casos. Empezá acá.
Configuración C: sistemas críticos (legal, médico, financiero)
collection = client.create_collection(
name="critical_docs",
metadata={
"hnsw:space": "cosine",
"hnsw:M": 64,
"hnsw:construction_ef": 400,
"hnsw:search_ef": 200,
}
)
Cuándo usar: un retrieval malo tiene consecuencias serias (información médica, asesoría legal, compliance). Tolerás más latencia y RAM por mejor accuracy.
Performance esperada (1M vectores):
- Build time: ~2 horas
- Query p95: ~40ms
- Recall@10: ~99%
- RAM: ~12 GB
No empieces acá si no lo necesitás. Es 4x más caro en RAM y 4x más lento en build vs config B, ganando solo 2% recall extra.
Validación con benchmark antes de deployar
No confíes en las tablas. Mide en tu dataset real.
# benchmark_configs.py
import chromadb
from chromadb.utils import embedding_functions
import os
import time
import statistics
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.getenv("OPENAI_API_KEY"),
model_name="text-embedding-3-small"
)
def benchmark_config(name, M, construction_ef, search_ef, docs, queries, eval_set):
"""Benchmark una configuración HNSW: build time + query latency + recall."""
client = chromadb.PersistentClient(path=f"./chroma_bench_{name}")
# Limpiar previo si existe
try:
client.delete_collection(name)
except Exception:
pass
collection = client.create_collection(
name=name,
embedding_function=openai_ef,
metadata={
"hnsw:space": "cosine",
"hnsw:M": M,
"hnsw:construction_ef": construction_ef,
"hnsw:search_ef": search_ef,
}
)
# Build time
build_start = time.perf_counter()
for batch_start in range(0, len(docs), 200):
end = min(batch_start + 200, len(docs))
collection.add(
documents=docs[batch_start:end],
ids=[f"doc_{i}" for i in range(batch_start, end)],
)
build_time = time.perf_counter() - build_start
# Query latency (warm-up + measure)
for q in queries[:5]:
collection.query(query_texts=[q], n_results=10)
latencies = []
for q in queries:
start = time.perf_counter()
collection.query(query_texts=[q], n_results=10)
latencies.append((time.perf_counter() - start) * 1000)
latencies.sort()
p50 = latencies[len(latencies) // 2]
p95 = latencies[int(len(latencies) * 0.95)]
# Recall@10 sobre eval set
hits = 0
total = 0
for item in eval_set:
result = collection.query(query_texts=[item["query"]], n_results=10)
retrieved_ids = set(result['ids'][0])
relevant_ids = set(item["expected_doc_ids"])
hits += len(retrieved_ids & relevant_ids)
total += len(relevant_ids)
recall = hits / total if total else 0
return {
"config": name,
"build_time_seconds": build_time,
"p50_ms": p50,
"p95_ms": p95,
"recall@10": recall,
}
# Configs a comparar
configs = [
("A_dev", 16, 100, 10),
("B_prod", 32, 200, 50),
("C_critical", 64, 400, 200),
]
# Asume docs, queries, eval_set definidos en otro lado
# docs = [...] # 10K-100K documentos reales
# queries = [...] # 50-100 queries representativas
# eval_set = [{"query": ..., "expected_doc_ids": [...]}] # 30+ entries
results = []
for name, M, ef_c, ef_s in configs:
print(f"\nBenchmarking {name} (M={M}, construction_ef={ef_c}, search_ef={ef_s})...")
result = benchmark_config(name, M, ef_c, ef_s, docs, queries, eval_set)
results.append(result)
print(f" Build time: {result['build_time_seconds']:.1f}s")
print(f" Query p50: {result['p50_ms']:.1f}ms")
print(f" Query p95: {result['p95_ms']:.1f}ms")
print(f" Recall@10: {result['recall@10']:.2%}")
Output típico (dataset de 100K vectores reales):
Benchmarking A_dev (M=16, construction_ef=100, search_ef=10)...
Build time: 188.4s
Query p50: 3.2ms
Query p95: 6.8ms
Recall@10: 91.5%
Benchmarking B_prod (M=32, construction_ef=200, search_ef=50)...
Build time: 412.7s
Query p50: 7.4ms
Query p95: 14.1ms
Recall@10: 96.8%
Benchmarking C_critical (M=64, construction_ef=400, search_ef=200)...
Build time: 1247.3s
Query p50: 19.8ms
Query p95: 38.5ms
Recall@10: 98.9%
Lecturas:
- B_prod gana 5% de recall sobre A_dev a costa de 2x build time y 2x query latency. Vale.
- C_critical gana 2% recall extra sobre B_prod a costa de 3x build time y 2.5x query latency. Solo vale en casos críticos.
Tu eval set determina si las diferencias de recall son significativas para tu caso. Si tu producto tolera 91% recall (dev), no pagues el costo de prod. Si necesitás 98%+ (legal/médico), aceptá el costo extra.
Trampas y errores comunes
Trampa 1: cambiar M en una collection con datos
El error: insertaste 500K docs con M=16. Después de leer recomendaciones, decidís pasar a M=32. Modificás el metadata de la collection.
Síntoma: o ChromaDB lanza error, o (peor) acepta el cambio pero el grafo HNSW interno sigue construido con M=16. La nueva config se aplica solo a docs nuevos. Resultado: collection con grafo inconsistente.
Cómo prevenir: M y construction_ef se fijan al crear y no se pueden cambiar. Si necesitás cambiar, crear collection nueva con la config deseada y re-insertar todo (cubre M04/05 batch ingestion).
Trampa 2: usar config C ("critical") por default
El error: "queremos lo mejor", entonces usás M=64, construction_ef=400 desde el inicio.
Síntoma: build time 4x más lento, RAM 50% más alta, latencia 2.5x peor — todo para ganar 2% de recall que no pasa por tu eval set.
Cómo prevenir: empezar con config B (producción), medir recall sobre tu eval set. Solo subir a C si la diferencia se justifica.
Trampa 3: aceptar search_ef=10 (default) en producción
El error: dejás todo default, no setás hnsw:search_ef explícitamente. ChromaDB usa 10.
Síntoma: queries son rapidísimas (5ms p95), pero recall@10 es 88% en lugar del 96% que esperás del modelo de embeddings. Los usuarios reportan "no encuentra cosas que sé que están".
Cómo prevenir: explícitamente hnsw:search_ef=50 para producción. La latencia extra (10ms vs 5ms) es invisible para el usuario, la calidad de recall sí es notable.
Trampa 4: confundir construction_ef con search_ef
El error: pensás que ajustar construction_ef afecta queries. Subís construction_ef=400 esperando mejor recall en runtime.
Síntoma: las queries no cambian — construction_ef solo afectó el build (que ya pasó). Lo que querías ajustar era search_ef.
Cómo prevenir: mnemónico — "construction_ef = al construir, search_ef = al buscar". El primero solo importa en el build inicial; el segundo en cada query.
Trampa 5: olvidar que M afecta RAM linealmente
El error: dataset crece de 100K a 5M. Tu config era M=32 para 100K (RAM ~1 GB). Asumís que con 5M va a ser ~50 GB, instancia con 64 GB de RAM aguanta.
Realidad: además de los vectores en sí, HNSW guarda M × levels punteros por nodo. Para 5M con M=32, el overhead de HNSW solo (sin contar vectores) son ~6 GB extras. RAM total ~56 GB — dentro del límite pero sin margen.
Cómo prevenir: monitorear ram_usage_pct y planificar antes de hitting límites. Considerar bajar a M=16 o migrar a IVF+PQ si memoria es restricción.
Trampa 6: no benchmarkear, asumir las tablas
El error: copiás los valores de "config B" sin medir. Asumís que recall será 96-98%.
Realidad: recall depende del modelo de embeddings, distribución del dataset, y queries reales. Las tablas son orientativas. Tu sistema real puede tener recall 99% (dataset bien comportado) o 89% (queries fuera de distribución).
Cómo prevenir: construir eval set propio (30-50 queries con docs etiquetados) y medir antes de comprometerse.
Ejercicio aplicado
Escenario: vas a deployar un RAG para una empresa de servicios médicos. Características:
- Dataset: 800K guías clínicas en formato PDF (chunkeadas a ~3M chunks)
- Modelo de embeddings: OpenAI
text-embedding-3-small(1536 dim) - Restricciones:
- Hardware: instancia con 32 GB RAM disponible para ChromaDB
- SLA: p95 query <50ms total (incluyendo embedding ~150ms — entonces retrieval <20ms)
- Compliance: errores en respuestas tienen consecuencias clínicas. Recall@10 mínimo 97%.
- Build time tolerable: hasta 4 horas (ventana de mantenimiento mensual)
Tu trabajo: elegí los parámetros HNSW (M, construction_ef, search_ef) y justificá cada elección con números.
Solución
Análisis de restricciones:
- 3M chunks × 1536 dim × 4 bytes = 18 GB solo en vectores
- RAM disponible: 32 GB → margen para HNSW: 32 - 18 = 14 GB
- HNSW overhead aproximado:
N × M × 8 bytes × 5 levels
Cálculo de margen para distintos M:
M | HNSW overhead (3M vectores) | RAM total | ¿Cabe en 32 GB? |
|---|---|---|---|
| 16 | 1.9 GB | 19.9 GB | ✅ holgado |
| 32 | 3.7 GB | 21.7 GB | ✅ holgado |
| 64 | 7.4 GB | 25.4 GB | ✅ con margen |
| 128 | 14.7 GB | 32.7 GB | ❌ no cabe |
Análisis de SLA:
- Retrieval target: <20ms p95
- Con
M=32, search_ef=50: ~14ms p95 (según benchmarks típicos a 3M) - Con
M=64, search_ef=100: ~30ms p95 — fuera de SLA
Análisis de compliance (recall ≥97%):
M=16típicamente alcanza 93-96% — insuficienteM=32típicamente alcanza 96-98% — al límiteM=64típicamente alcanza 98-99% — cómodo
El conflicto: compliance pide M=64 (recall), SLA pide M=32 (latencia). Hay que decidir.
Decisión propuesta: M=32, construction_ef=400, search_ef=100
Justificación:
collection = client.create_collection(
name="medical_guidelines",
embedding_function=openai_ef,
metadata={
"hnsw:space": "cosine",
"hnsw:M": 32, # margen RAM, latencia OK
"hnsw:construction_ef": 400, # build de mayor calidad para compensar M moderado
"hnsw:search_ef": 100, # alto recall en runtime
}
)
Razonamiento:
M=32: balance entre RAM y latencia. Cabe en presupuesto, queries en ~14ms p95 (cumple SLA), recall esperado 96-98%.construction_ef=400: compensa elM=32con grafo de mayor calidad. Build time esperado: ~3 horas (dentro de la ventana de 4h). Mejora recall 1-2% sobreconstruction_ef=200.search_ef=100: bumps recall final ~98%. Latency esperada ~16-18ms p95 — dentro del SLA de 20ms con margen.
Total esperado:
- Build time: ~3h (cabe en ventana)
- RAM: ~22 GB (margen de 10 GB para crecimiento)
- Query p95: ~16ms (cumple SLA <20ms)
- Recall@10: ~97-98% (cumple compliance ≥97%)
Validación obligatoria antes de producción:
- Construir eval set médico con 50-100 queries reales anotadas por médicos.
- Benchmarkear las tres configs (B prod, custom, C critical) sobre el eval set.
- Si recall <97%, considerar pasar a
M=64y aceptar la latencia mayor (negociar SLA con stakeholders), o evaluar re-ranker con cross-encoder médico (post-retrieval). - Si latencia >20ms con
M=32, considerar reducirn_results(M04/06).
Plan B si la config no alcanza el SLA o el recall:
- Si latencia es el problema: bajar
search_efa 70, aceptar recall 96-97%, buscar acuerdo con stakeholders sobre el threshold. - Si recall es el problema: subir a
M=64, RAM sube a 25 GB (cabe), build time sube a 6h (necesita ventana de mantenimiento más larga), latencia sube a 30ms (renegociar SLA). - Si ambos son problema: arquitectura distinta — sharding por especialidad médica (cardiología, oncología, etc.), cada shard con dataset 5-10x menor, latencia recall mejorables.
Resumen y siguiente paso
Lo que aprendiste:
- ChromaDB usa HNSW por default, configurable con cuatro parámetros:
hnsw:space,hnsw:M,hnsw:construction_ef,hnsw:search_ef. Myconstruction_efse fijan al crear la collection y no se pueden cambiar sin reconstruir el índice.search_efse aplica en runtime y se puede cambiar sin tocar los datos.Mcontrola densidad del grafo: másM→ mejor recall, más RAM, build más lento. Default 16, recomendado producción 32.construction_efcontrola calidad del build: más alto → grafo de mejor calidad, build más lento. Default 100, recomendado producción 200-400.search_efcontrola calidad del query: default 10 (muy bajo para producción), recomendado 50-100.- Las tres configs canónicas (dev, producción, crítica) cubren la mayoría de casos. Empezá con producción (B), ajustá si tu eval set lo justifica.
Checkpoint: antes de avanzar, deberías poder:
- Diferenciar parámetros build-time (
M,construction_ef) de runtime (search_ef). - Calcular el impacto aproximado de cambiar
Men consumo de RAM para un dataset dado. - Justificar la elección de config (B prod vs C critical) basándote en SLA y compliance.
Siguiente cápsula: 04 — Metadata Filtering Implementation.
Configuraste el motor del índice. Ahora vas a aprender a aprovecharlo: filtrar resultados por metadata para reducir el espacio de búsqueda 10x cuando tu query lo permite. La cápsula 04 te enseña la sintaxis completa de where clauses y cómo diseñar el schema de metadata para que los filters que vas a necesitar sean fáciles de expresar.
Recursos
- ChromaDB — Configuring HNSW Parameters — Documentación oficial
- HNSW: Hierarchical Navigable Small World Graphs (paper) — El paper que define el algoritmo, sección 4 explica
Myef - HNSW Tutorial (Pinecone Learn) — Visualización del grafo y parámetros
- Comparing HNSW Configurations (Qdrant Blog) — Trade-offs ilustrados
- ANN Benchmarks — Benchmarks reproducibles para distintas configs
- Tuning HNSW for Production (Weaviate) — Caso práctico con valores recomendados
Tiempo estimado: 30-35 minutos Siguiente: 04-metadata-filtering.md