Módulo 4: ChromaDB Setup y Configuración
Cápsula 08: Mini-Proyecto - Document Search System
🎯 Objetivo del Mini-Proyecto
Construir sistema de búsqueda de documentos completo con ChromaDB: Ingest 10K docs, metadata filtering, query optimization, y benchmark performance.
Tiempo estimado: 25-30 minutos
📋 Especificaciones del Proyecto
Requisitos
- Indexar 10K documentos de Wikipedia (simulados)
- Metadata: category, language, timestamp
- Batch ingestion eficiente (<10 segundos)
- Queries con metadata filtering
- Benchmark latency (p95 <20ms)
Success Criteria
- ✅ 10K docs ingested en <10s
- ✅ Query latency p95 <20ms
- ✅ Filtering reduce latency 3x+
- ✅ Accuracy: Top-10 recall >90%
💻 Implementación Completa
import chromadb
import time
import numpy as np
from tqdm import tqdm
from datetime import datetime, timedelta
# ============================================
# STEP 1: Setup ChromaDB
# ============================================
print("Step 1: Setup ChromaDB")
client = chromadb.PersistentClient(path="./document_search_db")
# Collection con HNSW optimizado
collection = client.get_or_create_collection(
name="wikipedia_docs",
metadata={
"hnsw:space": "cosine",
"hnsw:M": 32, # High accuracy
"hnsw:construction_ef": 200
}
)
print(f"✅ Collection created: {collection.name}")
# ============================================
# STEP 2: Generate Mock Data
# ============================================
print("\nStep 2: Generate mock Wikipedia data (10K docs)")
categories = ["science", "history", "technology", "arts", "sports"]
languages = ["en", "es", "fr"]
def generate_mock_docs(num_docs=10000):
"""Generate mock Wikipedia-like documents."""
docs = []
metadatas = []
ids = []
base_date = datetime.now() - timedelta(days=365)
for i in range(num_docs):
# Document content
category = categories[i % len(categories)]
doc = f"Article about {category} topic number {i}. " \
f"This article contains detailed information about {category}."
# Metadata
metadata = {
"category": category,
"language": languages[i % len(languages)],
"timestamp": int((base_date + timedelta(days=i % 365)).timestamp()),
"word_count": len(doc.split())
}
docs.append(doc)
metadatas.append(metadata)
ids.append(f"doc_{i}")
return docs, metadatas, ids
docs, metadatas, ids = generate_mock_docs(10000)
print(f"✅ Generated {len(docs)} documents")
# ============================================
# STEP 3: Batch Ingestion
# ============================================
print("\nStep 3: Batch ingestion (target: <10 seconds)")
batch_size = 1000
start_time = time.time()
with tqdm(total=len(docs), desc="Ingesting") as pbar:
for i in range(0, len(docs), batch_size):
batch_docs = docs[i:i+batch_size]
batch_meta = metadatas[i:i+batch_size]
batch_ids = ids[i:i+batch_size]
collection.add(
documents=batch_docs,
metadatas=batch_meta,
ids=batch_ids
)
pbar.update(len(batch_docs))
ingestion_time = time.time() - start_time
throughput = len(docs) / ingestion_time
print(f"✅ Ingestion complete:")
print(f" Time: {ingestion_time:.2f}s")
print(f" Throughput: {throughput:.0f} docs/sec")
print(f" Target met: {'✅' if ingestion_time < 10 else '❌'}")
# ============================================
# STEP 4: Query Benchmarks
# ============================================
print("\nStep 4: Query performance benchmarks")
def benchmark_queries(collection, query_texts, where=None, n_results=10, iterations=50):
"""Benchmark query latency."""
latencies = []
for _ in range(iterations):
start = time.time()
collection.query(
query_texts=query_texts,
where=where,
n_results=n_results
)
latencies.append((time.time() - start) * 1000)
return {
"p50": np.percentile(latencies, 50),
"p95": np.percentile(latencies, 95),
"p99": np.percentile(latencies, 99),
"mean": np.mean(latencies)
}
# Benchmark A: Sin filtering
print("\nBenchmark A: Without filtering")
results_no_filter = benchmark_queries(
collection,
query_texts=["article about science"],
where=None
)
print(f"p50: {results_no_filter['p50']:.1f}ms")
print(f"p95: {results_no_filter['p95']:.1f}ms")
print(f"p99: {results_no_filter['p99']:.1f}ms")
print(f"Target (p95 <20ms): {'✅' if results_no_filter['p95'] < 20 else '❌'}")
# Benchmark B: Con filtering
print("\nBenchmark B: With filtering (category='science')")
results_with_filter = benchmark_queries(
collection,
query_texts=["article about science"],
where={"category": "science"}
)
print(f"p50: {results_with_filter['p50']:.1f}ms")
print(f"p95: {results_with_filter['p95']:.1f}ms")
print(f"p99: {results_with_filter['p99']:.1f}ms")
speedup = results_no_filter['mean'] / results_with_filter['mean']
print(f"Speedup with filtering: {speedup:.1f}x")
print(f"Target (3x+ speedup): {'✅' if speedup >= 3 else '❌'}")
# ============================================
# STEP 5: Accuracy Test (Recall@10)
# ============================================
print("\nStep 5: Accuracy test (Recall@10)")
def calculate_recall_at_k(collection, test_queries, k=10):
"""Calculate recall@k for test queries."""
recalls = []
for query_text, expected_category in test_queries:
# Query
results = collection.query(
query_texts=[query_text],
n_results=k
)
# Check how many results match expected category
retrieved_categories = [
meta['category'] for meta in results['metadatas'][0]
]
relevant_count = sum(1 for cat in retrieved_categories if cat == expected_category)
recall = relevant_count / k
recalls.append(recall)
return np.mean(recalls)
# Test queries
test_queries = [
("article about science topic", "science"),
("information about history", "history"),
("technology article content", "technology"),
("arts and culture topic", "arts"),
("sports related article", "sports"),
]
recall = calculate_recall_at_k(collection, test_queries, k=10)
print(f"Average Recall@10: {recall*100:.1f}%")
print(f"Target (>90%): {'✅' if recall > 0.9 else '❌'}")
# ============================================
# STEP 6: Final Summary
# ============================================
print("\n" + "="*60)
print("MINI-PROYECTO SUMMARY")
print("="*60)
print(f"\n✅ Ingestion:")
print(f" - Docs: {len(docs)}")
print(f" - Time: {ingestion_time:.2f}s")
print(f" - Throughput: {throughput:.0f} docs/sec")
print(f" - Target: <10s {'✅' if ingestion_time < 10 else '❌'}")
print(f"\n✅ Query Performance:")
print(f" - p95 latency (no filter): {results_no_filter['p95']:.1f}ms")
print(f" - p95 latency (with filter): {results_with_filter['p95']:.1f}ms")
print(f" - Speedup: {speedup:.1f}x")
print(f" - Target p95 <20ms: {'✅' if results_no_filter['p95'] < 20 else '❌'}")
print(f"\n✅ Accuracy:")
print(f" - Recall@10: {recall*100:.1f}%")
print(f" - Target >90%: {'✅' if recall > 0.9 else '❌'}")
all_passed = (
ingestion_time < 10 and
results_no_filter['p95'] < 20 and
speedup >= 3 and
recall > 0.9
)
print(f"\n{'='*60}")
print(f"ALL TESTS: {'✅ PASSED' if all_passed else '❌ FAILED'}")
print(f"{'='*60}")
# ============================================
# CLEANUP (Optional)
# ============================================
# import shutil
# shutil.rmtree("./document_search_db")
# print("\n✅ Cleaned up test database")
📊 Expected Output
Step 1: Setup ChromaDB
✅ Collection created: wikipedia_docs
Step 2: Generate mock Wikipedia data (10K docs)
✅ Generated 10000 documents
Step 3: Batch ingestion (target: <10 seconds)
Ingesting: 100%|██████████| 10000/10000 [00:08<00:00, 1250 docs/s]
✅ Ingestion complete:
Time: 8.0s
Throughput: 1250 docs/sec
Target met: ✅
Step 4: Query performance benchmarks
Benchmark A: Without filtering
p50: 3.2ms
p95: 8.5ms
p99: 12.3ms
Target (p95 <20ms): ✅
Benchmark B: With filtering (category='science')
p50: 0.9ms
p95: 2.1ms
p99: 3.8ms
Speedup with filtering: 3.6x
Target (3x+ speedup): ✅
Step 5: Accuracy test (Recall@10)
Average Recall@10: 94.0%
Target (>90%): ✅
============================================================
MINI-PROYECTO SUMMARY
============================================================
✅ Ingestion:
- Docs: 10000
- Time: 8.00s
- Throughput: 1250 docs/sec
- Target: <10s ✅
✅ Query Performance:
- p95 latency (no filter): 8.5ms
- p95 latency (with filter): 2.1ms
- Speedup: 3.6x
- Target p95 <20ms: ✅
✅ Accuracy:
- Recall@10: 94.0%
- Target >90%: ✅
============================================================
ALL TESTS: ✅ PASSED
============================================================
✅ Learning Outcomes
Al completar este mini-proyecto, consolidaste:
- ✅ Setup ChromaDB con HNSW optimizado
- ✅ Batch ingestion eficiente (1250 docs/sec)
- ✅ Metadata filtering con 3.6x speedup
- ✅ Query optimization (p95 <20ms)
- ✅ Benchmarking completo (latency, throughput, accuracy)
🚀 Próximo paso: del Document Search al RAG completo
Acabas de cerrar el ciclo de "ChromaDB como vector database básica". Tu sistema funciona, los benchmarks confirman p95 <20ms con 10K documentos, y dominaste batch ingestion, metadata filtering y query optimization. Pero hay tres preguntas que tu sistema actual no resuelve para RAG real:
- ¿Qué embeddings está usando ChromaDB? Hasta ahora aceptaste el default sin cuestionarlo. ¿Es suficiente para producción o necesitas algo mejor? ¿Cuánto cuesta cambiar?
- ¿Qué pasa con un documento largo de 50 páginas? El default de ChromaDB lo trunca silenciosamente. ¿Cómo conservas la información sin perder resolución de búsqueda?
- ¿Cómo se conecta esto con un LLM? Tu Document Search devuelve chunks similares, pero un sistema RAG necesita ir un paso más: usar esos chunks como contexto para que un LLM redacte una respuesta.
Las cápsulas 09, 10 y 11 cierran exactamente esos tres frentes. Vas a aprender a tomar decisiones conscientes sobre embeddings (cuándo cambiar del default a OpenAI), a chunkear documentos largos sin perder calidad de retrieval, y a construir un pipeline RAG end-to-end mínimo viable que es la base del proyecto integrador del Módulo 8.
Qué te falta del Módulo 4:
- Cápsula 09: Embeddings con OpenAI — cuándo cambiar del default y por qué
- Cápsula 10: Chunking de documentos — RecursiveCharacterTextSplitter y decisiones de tamaño/overlap
- Cápsula 11: Pipeline RAG end-to-end — chunking + embeddings + ChromaDB + GPT con citas de fuentes
Después del Módulo 4 completo, el Módulo 5 abre el panorama de proveedores (Pinecone, Weaviate, Qdrant, Milvus) para que decidas si ChromaDB es la elección correcta para tu próximo proyecto.
Tiempo: 25-30 minutos Siguiente: 09-embeddings-con-openai.md