Módulo 4: ChromaDB Setup y Configuración

Cápsula 09: Embeddings con OpenAI — Cuándo cambiar del default

Descripción de la cápsula

Hasta ahora has usado ChromaDB sin pensar en cómo se generan los embeddings. Cuando llamaste a collection.add(documents=[...]), ChromaDB convirtió silenciosamente cada texto en un vector usando un modelo que nunca nombraste. Eso te permitió aprender ChromaDB sin distracciones — y fue la decisión pedagógica correcta hasta ahora.

Pero ese default tiene nombre y tiene límites. Y cuando construyas RAG para producción, vas a tener que decidir conscientemente si te quedas con él o si pagas por algo mejor. Esta cápsula te enseña a tomar esa decisión con datos, no con intuición. Vas a comparar el default contra OpenAI text-embedding-3-small sobre el mismo dataset, medir el impacto en accuracy y costo, y aprender cuándo el cambio se justifica.

Al terminar, cuando un Tech Lead te pregunte "¿por qué pagamos OpenAI si ChromaDB ya genera embeddings gratis?", tendrás una respuesta numérica defendible — y sabrás también cuándo la respuesta correcta es "no necesitamos pagar".

Al finalizar esta cápsula serás capaz de:

  • ✅ Identificar qué modelo usa ChromaDB por default y cuáles son sus límites técnicos reales
  • ✅ Comparar accuracy entre el default y text-embedding-3-small con el mismo dataset
  • ✅ Calcular el costo mensual de OpenAI embeddings para un volumen dado
  • ✅ Aplicar un framework de decisión para elegir entre los dos modelos
  • ✅ Implementar la integración OpenAI + ChromaDB con OpenAIEmbeddingFunction
  • ✅ Anticipar el error más caro: cambiar de modelo sin re-embebir datos existentes

Tiempo estimado: 35-45 minutos


Lo que has estado usando sin saberlo

Cuando ejecutaste esto en cápsulas anteriores:

import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.create_collection("docs")
collection.add(
    documents=["password reset instructions", "billing FAQ"],
    ids=["1", "2"]
)

ChromaDB hizo dos cosas que no viste:

  1. Descargó un modelo de embeddings la primera vez que ejecutaste add() (~80 MB).
  2. Generó embeddings de 384 dimensiones para cada documento usando ese modelo, localmente en tu máquina, sin llamadas a APIs externas.

El modelo que usó es all-MiniLM-L6-v2, parte de la librería Sentence Transformers. Es gratis, corre offline, y es razonablemente bueno para tareas generales de similaridad semántica. Por eso ChromaDB lo eligió como default: cero fricción para empezar.

Las características reales del default

Aspectoall-MiniLM-L6-v2 (default ChromaDB)
Dimensiones384
Tamaño del modelo80 MB (descarga local)
Latencia por documento5-15ms en CPU, 1-3ms en GPU
Costo monetario$0 (open-source)
IdiomasPrincipalmente inglés, multilingüe limitado
Calidad relativaBuena para textos cortos (<256 tokens), mediocre para textos largos o técnicos
Score MTEB (benchmark estándar)~56 (de 100)

Ese score de 56 en MTEB es el dato clave que la mayoría de tutoriales no menciona. MTEB (Massive Text Embedding Benchmark) es el benchmark estándar de la industria para comparar modelos de embeddings — mide accuracy en tareas de retrieval, classification, clustering y semantic similarity.

La pregunta que importa: ¿qué tan lejos está 56 del techo? ¿Y cuánto cuesta llegar más arriba?


OpenAI text-embedding-3-small: qué cambia

El modelo OpenAI más usado para RAG en 2026 es text-embedding-3-small. Comparado con el default de ChromaDB:

Aspectoall-MiniLM-L6-v2text-embedding-3-small
Dimensiones3841536 (ajustable a 256-1536)
Latencia por batch de 100 docs~50ms (local CPU)~150-300ms (API)
Costo por 1M tokens$0$0.02
IdiomasInglés bien, otros mediocreMultilingüe robusto (incluyendo español)
Tokens máximos por input256 (truncado más allá)8191
Score MTEB~56~62
SetupCero (descarga automática)API key + manejo de rate limits

Tres cambios importantes:

1. Calidad mejor, pero no el doble. Pasar de 56 a 62 en MTEB suena modesto, pero en queries de RAG se traduce en 5-15% mejor recall@10 — la diferencia entre "el bot da la respuesta correcta el 78% de las veces" y "el 91%". Esa diferencia es enorme en producción.

2. Multilingüe real. Si tu RAG va a recibir queries en español, la diferencia se amplifica. all-MiniLM-L6-v2 fue entrenado mayormente en inglés; OpenAI text-embedding-3-small fue entrenado en datos multilingües y tiene paridad razonable entre idiomas principales.

3. Tokens largos. El default trunca silenciosamente cualquier texto más allá de 256 tokens (~1000 caracteres). Si tienes documentos de 2000 caracteres, estás perdiendo más de la mitad del contenido sin saberlo. OpenAI maneja hasta 8191 tokens por input.

Pero todo esto tiene precio: $0.02 por cada millón de tokens embebidos. Si suena barato, calculemos.

Calculando el costo real

Un documento técnico promedio tiene ~500 tokens. Vamos a estimar el costo para tres escenarios:

# Calculadora de costo OpenAI text-embedding-3-small
PRICE_PER_1M_TOKENS = 0.02  # USD

scenarios = {
    "MVP / proyecto personal": {
        "docs": 1_000,
        "avg_tokens_per_doc": 500,
        "queries_per_month": 1_000,
        "avg_tokens_per_query": 30,
    },
    "Startup en producción": {
        "docs": 50_000,
        "avg_tokens_per_doc": 500,
        "queries_per_month": 100_000,
        "avg_tokens_per_query": 30,
    },
    "Enterprise mid-size": {
        "docs": 1_000_000,
        "avg_tokens_per_doc": 500,
        "queries_per_month": 10_000_000,
        "avg_tokens_per_query": 30,
    },
}

for name, s in scenarios.items():
    ingestion_tokens = s["docs"] * s["avg_tokens_per_doc"]
    monthly_query_tokens = s["queries_per_month"] * s["avg_tokens_per_query"]

    ingestion_cost = (ingestion_tokens / 1_000_000) * PRICE_PER_1M_TOKENS
    monthly_query_cost = (monthly_query_tokens / 1_000_000) * PRICE_PER_1M_TOKENS

    print(f"\n=== {name} ===")
    print(f"  Costo único de ingestion: ${ingestion_cost:.2f}")
    print(f"  Costo mensual de queries: ${monthly_query_cost:.2f}")
    print(f"  Total primer mes: ${ingestion_cost + monthly_query_cost:.2f}")

Output esperado:

=== MVP / proyecto personal ===
  Costo único de ingestion: $0.01
  Costo mensual de queries: $0.00
  Total primer mes: $0.01

=== Startup en producción ===
  Costo único de ingestion: $0.50
  Costo mensual de queries: $0.06
  Total primer mes: $0.56

=== Enterprise mid-size ===
  Costo único de ingestion: $10.00
  Costo mensual de queries: $6.00
  Total primer mes: $16.00

Lectura clave: para volúmenes razonables, OpenAI embeddings es asombrosamente barato. El caso "startup en producción" cuesta menos que un café por mes. Lo caro de OpenAI no son los embeddings — es la generation con GPT-4. Pero no te la enseñaré como gratis: el costo escala lineal con el volumen, y si haces re-ingestion frecuente o tienes documentos enormes, la cuenta crece.


Comparación práctica: el mismo dataset con dos modelos

Hasta aquí has visto tablas. Ahora vamos al ejemplo trabajado. Vas a cargar el mismo conjunto de 100 documentos técnicos en dos collections — una con el default, otra con OpenAI — y comparar el resultado de la misma query en ambas.

Setup del experimento

# experimento_embeddings.py
import os
import chromadb
from chromadb.utils import embedding_functions
from openai import OpenAI

# Verificar API key (deberías tenerla en .env)
assert os.getenv("OPENAI_API_KEY"), "Configura OPENAI_API_KEY en tu entorno"

# Dataset: 100 documentos técnicos sobre vector databases
docs = [
    "HNSW (Hierarchical Navigable Small World) is a graph-based algorithm for approximate nearest neighbor search.",
    "ChromaDB uses HNSW by default with M=16 and construction_ef=100 for new collections.",
    "Cosine similarity measures the angle between two vectors, ignoring magnitude.",
    "Metadata filtering reduces search space before similarity computation, improving latency.",
    "Pinecone is a managed vector database service with serverless and pod-based deployment options.",
    "Embedding dimensionality affects both retrieval quality and storage cost.",
    "RAG (Retrieval Augmented Generation) combines vector search with LLM generation.",
    "Re-ranking models like cross-encoders improve top-K results at the cost of latency.",
    # ... (en práctica, 100 documentos sobre el tema)
]
ids = [f"doc_{i:03d}" for i in range(len(docs))]

client = chromadb.PersistentClient(path="./chroma_experiment")

# Collection 1: usando el default de ChromaDB
collection_default = client.get_or_create_collection(
    name="vectordb_docs_default"
    # Sin embedding_function → usa all-MiniLM-L6-v2
)
collection_default.add(documents=docs, ids=ids)

# Collection 2: usando OpenAI text-embedding-3-small
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.getenv("OPENAI_API_KEY"),
    model_name="text-embedding-3-small"
)
collection_openai = client.get_or_create_collection(
    name="vectordb_docs_openai",
    embedding_function=openai_ef
)
collection_openai.add(documents=docs, ids=ids)

print(f"Default collection: {collection_default.count()} docs")
print(f"OpenAI collection:  {collection_openai.count()} docs")

Punto clave: la única diferencia entre las dos collections es la embedding_function. Mismo dataset, mismo ChromaDB, mismo HNSW. Lo que cambia es cómo se convirtieron los textos en vectores.

Query 1: pregunta directa en inglés

query_en = "How does HNSW work for approximate nearest neighbor search?"

print("\n=== DEFAULT (all-MiniLM-L6-v2) ===")
results_default = collection_default.query(query_texts=[query_en], n_results=3)
for i, (doc, dist) in enumerate(zip(results_default['documents'][0], results_default['distances'][0])):
    print(f"  #{i+1} (dist={dist:.3f}): {doc[:80]}...")

print("\n=== OPENAI (text-embedding-3-small) ===")
results_openai = collection_openai.query(query_texts=[query_en], n_results=3)
for i, (doc, dist) in enumerate(zip(results_openai['documents'][0], results_openai['distances'][0])):
    print(f"  #{i+1} (dist={dist:.3f}): {doc[:80]}...")

Output típico (ambos aciertan, distancias diferentes):

=== DEFAULT (all-MiniLM-L6-v2) ===
  #1 (dist=0.412): HNSW (Hierarchical Navigable Small World) is a graph-based algorithm...
  #2 (dist=0.689): ChromaDB uses HNSW by default with M=16 and construction_ef=100...
  #3 (dist=0.852): Embedding dimensionality affects both retrieval quality and storage cost.

=== OPENAI (text-embedding-3-small) ===
  #1 (dist=0.187): HNSW (Hierarchical Navigable Small World) is a graph-based algorithm...
  #2 (dist=0.341): ChromaDB uses HNSW by default with M=16 and construction_ef=100...
  #3 (dist=0.498): Re-ranking models like cross-encoders improve top-K results...

Ambos modelos identifican correctamente las dos cápsulas más relevantes. La diferencia: OpenAI las separa más claramente (distancias 0.187 y 0.341) mientras el default las agrupa más cerca (0.412 y 0.689). Esto importa cuando aplicas un score_threshold para descartar resultados de baja calidad — el default es más ruidoso.

Query 2: la misma pregunta en español

Aquí es donde la diferencia se vuelve dramática.

query_es = "¿Cómo funciona HNSW para búsqueda aproximada de vecinos cercanos?"

print("\n=== DEFAULT con query en español ===")
results_default_es = collection_default.query(query_texts=[query_es], n_results=3)
for i, (doc, dist) in enumerate(zip(results_default_es['documents'][0], results_default_es['distances'][0])):
    print(f"  #{i+1} (dist={dist:.3f}): {doc[:80]}...")

print("\n=== OPENAI con query en español ===")
results_openai_es = collection_openai.query(query_texts=[query_es], n_results=3)
for i, (doc, dist) in enumerate(zip(results_openai_es['documents'][0], results_openai_es['distances'][0])):
    print(f"  #{i+1} (dist={dist:.3f}): {doc[:80]}...")

Output típico:

=== DEFAULT con query en español ===
  #1 (dist=0.751): Cosine similarity measures the angle between two vectors...   ❌ Irrelevante
  #2 (dist=0.792): RAG (Retrieval Augmented Generation) combines vector search... ❌ Irrelevante
  #3 (dist=0.812): HNSW (Hierarchical Navigable Small World) is a graph-based... ⚠️ Tercer lugar

=== OPENAI con query en español ===
  #1 (dist=0.243): HNSW (Hierarchical Navigable Small World) is a graph-based... ✅ Correcto
  #2 (dist=0.401): ChromaDB uses HNSW by default with M=16 and construction_ef... ✅ Correcto
  #3 (dist=0.589): Embedding dimensionality affects both retrieval quality...    ⚠️ Tangencial

Lo que pasó: el default fue entrenado mayormente en inglés. Cuando le das una query en español, sus embeddings de la query no caen cerca de los embeddings de documentos en inglés sobre el mismo tema. El resultado: respuestas irrelevantes en posiciones 1-2, la respuesta correcta en posición 3.

OpenAI maneja la barrera de idioma sin esfuerzo: query en español → matchea documento en inglés sobre el mismo concepto.

Conclusión del experimento: si tu audiencia es 100% angloparlante y tus documentos son cortos, el default puede ser suficiente. En cualquier otro caso (multilingüe, documentos largos, RAG production), OpenAI gana de forma medible.


Decisión: framework de criterios

No hay respuesta universal. Pero sí hay un framework reproducible. Para cada proyecto, evalúa estos cinco criterios:

CriterioDefault ChromaDBOpenAI text-embedding-3-small
Tu dataset es <10K docs y solo en inglés✅ SuficienteSobreingeniería
Tienes queries multilingües (incluye español)❌ Calidad pobre✅ Necesario
Documentos largos (>1000 caracteres)❌ Truncamiento silencioso✅ Maneja hasta 8K tokens
Producción con SLA de calidad (>90% recall)⚠️ Difícil de alcanzar✅ Más alcanzable
Cero presupuesto y prototipo rápido✅ IdealInnecesario
Necesitas latencia <50ms y no quieres dependencia API✅ Local, predecible❌ Latencia variable + dependencia
Datos sensibles que no pueden salir de tu infra✅ Procesamiento local❌ Datos van a OpenAI

Regla práctica de tres pasos

  1. ¿Estás en prototipo o aprendiendo? → Default. No pagues lo que no necesitas validar.
  2. ¿Tu RAG va a producción y la calidad importa? → OpenAI, con altísima probabilidad.
  3. ¿Datos sensibles o regulación que prohíbe enviarlos a API externa? → Default, o un modelo open-source más fuerte (bge-large, e5-large, etc. — fuera del scope de esta guía).

Implementación: integrar OpenAI con ChromaDB

ChromaDB acepta embedding_function en create_collection() y usa esa función automáticamente para todas las operaciones (add, query, update).

Setup completo

# embeddings_openai_setup.py
import os
import chromadb
from chromadb.utils import embedding_functions
from dotenv import load_dotenv

load_dotenv()  # Lee OPENAI_API_KEY de .env

# Embedding function de OpenAI
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.getenv("OPENAI_API_KEY"),
    model_name="text-embedding-3-small"
)

# Persistent client (datos sobreviven reinicio)
client = chromadb.PersistentClient(path="./chroma_openai_db")

# Collection con OpenAI embeddings
collection = client.get_or_create_collection(
    name="rag_production",
    embedding_function=openai_ef,
    metadata={"hnsw:space": "cosine"}  # Métrica recomendada para OpenAI embeddings
)

# Inserción: ChromaDB llama a OpenAI internamente
collection.add(
    documents=[
        "ChromaDB integrates with OpenAI embeddings via the embedding_function parameter.",
        "Cosine similarity is the recommended distance metric for OpenAI text embeddings.",
        "Always set OPENAI_API_KEY in environment variables, never in source code."
    ],
    metadatas=[
        {"category": "integration", "source": "docs"},
        {"category": "best_practice", "source": "guide"},
        {"category": "security", "source": "guide"}
    ],
    ids=["doc_1", "doc_2", "doc_3"]
)

# Query: ChromaDB embebe la query con OpenAI antes de buscar
results = collection.query(
    query_texts=["What's the best distance metric for OpenAI embeddings?"],
    n_results=2
)

print(f"Top result: {results['documents'][0][0]}")
print(f"Distance: {results['distances'][0][0]:.3f}")

Output esperado:

Top result: Cosine similarity is the recommended distance metric for OpenAI text embeddings.
Distance: 0.187

Variables de entorno (manejo correcto de la API key)

# .env (NUNCA commitees esto a git)
OPENAI_API_KEY=sk-...tu-key-real...
# .gitignore (asegúrate de que .env esté incluido)
.env
.env.local
chroma_openai_db/
# Validación al inicio de tu app
from dotenv import load_dotenv
import os

load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")

if not api_key:
    raise RuntimeError(
        "OPENAI_API_KEY no está configurada. "
        "Crea un archivo .env con tu API key."
    )
if not api_key.startswith("sk-"):
    raise RuntimeError(
        f"OPENAI_API_KEY parece inválida (no empieza con 'sk-')."
    )

Manejo de rate limits

OpenAI impone rate limits (RPM y TPM). Para batch ingestion grande, conviene controlar la concurrencia:

import time
from openai import OpenAI

# El SDK de OpenAI ya hace retry automático en errores 429
# pero conviene controlar el ritmo en batches grandes

def ingest_with_backoff(collection, docs, ids, batch_size=100, sleep_between=0.5):
    """Inserta en batches con pausa entre llamadas para evitar rate limit."""
    for i in range(0, len(docs), batch_size):
        batch_docs = docs[i:i + batch_size]
        batch_ids = ids[i:i + batch_size]
        collection.add(documents=batch_docs, ids=batch_ids)
        print(f"  Insertado batch {i//batch_size + 1}: {len(batch_docs)} docs")
        if i + batch_size < len(docs):
            time.sleep(sleep_between)

# Para 10K documentos:
# 100 batches × 0.5s pausa = 50s overhead
# vs riesgo de hitting rate limit y perder progreso
ingest_with_backoff(collection, my_docs, my_ids, batch_size=100)

Trampas y errores comunes

Trampa 1: Cambiar de modelo sin re-embebir datos existentes

El error:

# Día 1: creaste la collection con default
collection = client.create_collection("docs")
collection.add(documents=mil_docs, ids=mil_ids)

# Día 30: decides cambiar a OpenAI
openai_ef = embedding_functions.OpenAIEmbeddingFunction(api_key=key, model_name="text-embedding-3-small")
collection_v2 = client.create_collection("docs_v2", embedding_function=openai_ef)
# ✅ Hasta aquí bien

# ❌ ERROR fatal: cargar las queries con OpenAI sobre la collection vieja
collection.query(query_texts=["...nueva query..."])  
# ChromaDB usa el modelo default para la query
# Pero los docs en "docs" fueron embebidos con default también
# Resultado: la query funciona, pero con calidad default, no OpenAI

Por qué pasa: los embeddings ya existentes en la collection se generaron con el modelo viejo. Si cambias embedding_function y haces queries, ChromaDB embebe la query con el modelo nuevo — pero busca en vectores generados con el modelo viejo. Eso devuelve resultados, pero técnicamente comparas peras con manzanas.

Cómo detectarlo: el sistema "funciona" pero los resultados son extraños o peores que antes. No hay error explícito.

Cómo corregir: crea una collection nueva con el modelo nuevo y re-inserta TODOS los documentos. No hay atajo. Migración:

def migrar_collection(client, old_name, new_name, new_embedding_function):
    """Migra todos los docs de una collection a otra con embedding nuevo."""
    old = client.get_collection(old_name)
    new = client.create_collection(new_name, embedding_function=new_embedding_function)

    # Extraer docs y metadata (en batches si es grande)
    batch_size = 500
    total = old.count()
    for offset in range(0, total, batch_size):
        batch = old.get(limit=batch_size, offset=offset, include=['documents', 'metadatas'])
        new.add(
            documents=batch['documents'],
            metadatas=batch['metadatas'],
            ids=batch['ids']
        )
        print(f"  Migrados {offset + len(batch['ids'])}/{total}")

    print(f"Migración completa. Verifica con new.count() == {total}")

Trampa 2: Mismatch de dimensiones entre collections

El error:

# Generar query con OpenAI (1536 dim)
query_embedding = openai_client.embeddings.create(
    input="my query",
    model="text-embedding-3-small"
).data[0].embedding  # 1536 dimensiones

# Buscar en collection con default (384 dim)
collection_default.query(query_embeddings=[query_embedding], n_results=5)
# ❌ ChromaError: Embedding dimension 1536 does not match collection dimensionality 384

Por qué pasa: cada modelo produce vectores de tamaño fijo. ChromaDB rechaza embeddings de tamaño distinto al de la collection.

Cómo prevenir: siempre que pases query_embeddings directamente, asegúrate de usar el mismo modelo que se usó para add. Si dejas que ChromaDB embeba la query con query_texts y la collection tiene embedding_function configurada, ChromaDB usa la misma función — no hay mismatch posible.

Trampa 3: Documentos truncados silenciosamente

El error: insertas un documento de 5000 caracteres usando el default de ChromaDB. ChromaDB no falla, pero internamente solo embebe los primeros ~256 tokens (~1000 caracteres). Los otros 4000 caracteres se almacenan como documents[i] pero no influyen en el embedding.

Síntoma: queries que deberían matchear con la parte final del documento no lo encuentran. El sistema "funciona" pero falla recall sin explicación.

Cómo prevenir:

  • Si vas a usar el default, divide documentos largos en chunks de ~800 caracteres antes de insertar (eso lo cubre M4/10).
  • Si usas OpenAI, tienes margen hasta 8191 tokens (~32K caracteres), pero igualmente conviene chunkear para retrieval más preciso.

Trampa 4: Pagar OpenAI cuando no aporta valor

El error: copy-paste de un tutorial de "RAG production-ready" que usa OpenAI desde el día uno, sin que tu caso lo justifique.

Cómo detectarlo: prototipo de portfolio con 200 documentos, todo en inglés, sin SLA de calidad, presupuesto $0 — y estás generando $50/mes en API calls.

Cómo corregir: usa el default. Cambia a OpenAI cuando midas que lo necesitas (recall@10 inadecuado en una eval set), no por defecto cultural.

Trampa 5: API key en el código fuente

# ❌ NUNCA hagas esto
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key="sk-proj-abc123...",  # commiteado a git → leak en GitHub
    model_name="text-embedding-3-small"
)

Por qué pasa: prisa, copy-paste, falta de hábito.

Cómo corregir: siempre desde variable de entorno. Configura un pre-commit hook (gitleaks, detect-secrets) que detecte el patrón sk- en commits.

Trampa 6: No medir antes de optar

El error: elegir entre default y OpenAI por intuición ("OpenAI es mejor, ¿no?") sin nunca medir sobre tu propio dataset.

Cómo corregir: crea un eval set pequeño (20-50 queries con documentos relevantes etiquetados) y mide recall@10 con ambos modelos. Si la diferencia es <5%, el default basta. Si es >10%, justificas OpenAI con datos. Eso te toma 30 minutos y te ahorra meses de discusión sin evidencia.


Ejercicio aplicado

Escenario: Te contrataron como AI Engineer en una startup de soporte técnico para empresas SaaS en LATAM. Especificaciones del proyecto:

  • 8,000 documentos de help articles (mezclados español + inglés)
  • Tamaño promedio de documento: 1,200 palabras (~1,800 tokens)
  • Volumen esperado: 200,000 queries por mes
  • SLA: respuesta correcta en top-3 ≥85% de las veces
  • Presupuesto mensual para infraestructura AI: $200
  • Restricción: documentos pueden enviarse a APIs externas (no son sensibles)

Pregunta: ¿Eliges el default de ChromaDB o OpenAI text-embedding-3-small? Justifica con números, no con intuición.

Solución

Análisis paso a paso:

1. Aplicar el framework de criterios:

CriterioVeredicto
Dataset >10K docsCerca del límite (8K), pero el siguiente criterio decide
Multilingüe (español + inglés)❌ Default falla aquí — necesitamos OpenAI
Documentos largos (1,800 tokens)❌ Default trunca a 256 tokens — perderíamos 86% del contenido
SLA de calidad (≥85% en top-3)Difícil con default por los dos puntos anteriores
Datos sensiblesNo es restricción aquí
Presupuesto$200/mes — hay que verificar que OpenAI cabe

Solo con criterios 2 y 3, OpenAI es la única opción defendible. Pero verifiquemos el presupuesto.

2. Cálculo de costo OpenAI:

# Ingestion (una sola vez)
docs = 8_000
tokens_per_doc = 1_800
ingestion_tokens = docs * tokens_per_doc  # 14.4M tokens
ingestion_cost = (ingestion_tokens / 1_000_000) * 0.02
# = $0.288 (un solo pago)

# Queries mensuales
queries_per_month = 200_000
tokens_per_query = 30  # queries cortas típicas
monthly_query_tokens = queries_per_month * tokens_per_query  # 6M tokens
monthly_query_cost = (monthly_query_tokens / 1_000_000) * 0.02
# = $0.12 / mes

Total: $0.288 ingestion + $0.12/mes queries = $0.41 el primer mes, $0.12/mes después.

Eso es 0.06% del presupuesto. Embeddings es un costo trivial — el grueso del $200/mes se irá a generation con GPT-4 (no embeddings).

3. Decisión final:

Eligió OpenAI text-embedding-3-small. Justificación: (a) la mezcla español+inglés del dataset hace que el default tenga calidad pobre en queries en español, demostrable con benchmark interno; (b) los documentos de 1,800 tokens serían truncados al 14% por el default (256/1800), perdiendo el 86% del contenido; (c) el SLA de 85% en top-3 es difícil de alcanzar con default truncado; (d) el costo es trivial ($0.41 setup + $0.12/mes), no compite con el presupuesto. Riesgo asumido: dependencia de API externa (mitigable con fallback a default si OpenAI tiene downtime, aunque con calidad degradada).

Bonus: antes de poner esto en producción, construir un eval set de 50 queries reales (mitad español, mitad inglés) y medir recall@3 con ambos modelos. Si el default sorprendentemente alcanza ≥85%, reconsiderar. Si OpenAI no llega a 85%, considerar text-embedding-3-large (más caro pero mejor).


Resumen y siguiente paso

Lo que aprendiste:

  • El default de ChromaDB es all-MiniLM-L6-v2: 384 dim, gratis, local, pero con límites en queries multilingües y documentos >256 tokens.
  • OpenAI text-embedding-3-small es el upgrade estándar para producción: 1536 dim, multilingüe robusto, hasta 8191 tokens por input, $0.02 por millón de tokens.
  • La decisión no es default vs OpenAI en abstracto — depende de cinco criterios: tamaño de dataset, idiomas, longitud de documentos, SLA de calidad y presupuesto.
  • Integrar OpenAI con ChromaDB es una línea: embedding_function=OpenAIEmbeddingFunction(...).
  • El error más caro es cambiar de modelo sin re-embebir todos los datos existentes — los embeddings viejos siguen ahí, mezclando peras con manzanas en silencio.

Checkpoint: antes de avanzar, deberías poder:

  • Explicar a un compañero qué hace ChromaDB por default cuando llamas collection.add(documents=...) sin configurar embedding_function.
  • Calcular el costo mensual de OpenAI embeddings para un escenario dado (ingestion + queries).
  • Justificar con un criterio cuál de los dos modelos elegirías para un proyecto que te describan.

Siguiente cápsula: 10 — Chunking de documentos.

Acabas de aprender a generar embeddings de calidad. Pero hay un problema que la cápsula no resolvió: ¿qué pasa con un documento de 5000 tokens? OpenAI lo acepta entero (cabe en 8191), pero embebir un documento entero como un solo vector destruye la precisión del retrieval. Si una pregunta del usuario apunta a una sección específica del documento, el vector "promedio" del documento entero no la encuentra.

La solución es chunking: dividir documentos largos en piezas de ~500 tokens y embebir cada chunk por separado. Suena simple, pero las decisiones (¿qué tamaño? ¿qué overlap? ¿dividir por párrafos o caracteres?) afectan dramáticamente la calidad de tu RAG. Eso es lo que cubre M4/10.


Recursos

  1. OpenAI Embeddings Documentation — Especificaciones oficiales del modelo
  2. MTEB Leaderboard — Benchmark estándar para comparar modelos de embeddings
  3. ChromaDB Embedding Functions — Lista oficial de funciones disponibles
  4. Sentence Transformers — all-MiniLM-L6-v2 — Documentación del modelo default
  5. OpenAI Pricing — Precios actualizados de embeddings y otros modelos
  6. MTEB: Massive Text Embedding Benchmark (paper) — Metodología detrás del benchmark MTEB

Tiempo estimado: 35-45 minutos Siguiente: 10-chunking-documentos.md