ChromaDB: Cómo usar una base de datos vectorial para no meter la pata enseñando

| Última modificación: 4 de mayo de 2026 | Tiempo de Lectura: 7 minutos
Premios Blog KeepCoding 2025

Co-Fundador de KeepCoding

El problema: enseñar lo que aún no has enseñado

Tengo un curso de programación con 47 clases. Cada clase tiene notes (donde explico cosas) y labs (donde el alumno practica). Y tengo un problema: a veces uso conceptos en los labs que todavía no he explicado en las notes.

«Vale, en este ejercicio usa map para transformar la lista.»

¿El problema? No he explicado qué coño es map hasta tres clases después.

Esto pasa más de lo que crees. Tienes el material en la cabeza, saltas de un sitio a otro, y sin darte cuenta asumes que el alumno sabe cosas que todavía no le has contado. El resultado: frustración, confusión, y alumnos que piensan que son tontos cuando el tonto eres tú.

La solución manual sería revisar cada lab, anotar qué conceptos usa, y verificar que se hayan explicado antes. Pero tengo 47 clases con varios notebooks cada una. Va a ser que no.

La solución: búsqueda semántica con ChromaDB

La idea es simple:

  1. Extraer los conceptos de cada notebook (qué se enseña, qué se usa)
  2. Guardarlos en una base de datos que entienda significado, no solo texto
  3. Para cada concepto usado en un lab, verificar que exista en notes anteriores

Eso de «entender significado» es clave. Si en las notes digo «función de orden superior» y en el lab uso «higher-order function», un grep no encontrará nada. Pero semánticamente son lo mismo.

Aquí es donde entra ChromaDB: una base de datos de vectores que convierte texto en embeddings y permite buscar por similitud. Dicho en cristiano: guardas texto, y luego puedes preguntar «¿hay algo parecido a esto?» y te devuelve los más similares.

ChromaDB en 5 minutos

ChromaDB es como SQLite pero para embeddings. Un solo archivo (o carpeta), sin servidor, sin configuración. Instalas, usas, y a correr.

pip install chromadb
# O si usas uv:
uv add chromadb

El concepto básico

En una base de datos normal guardas filas con columnas. En ChromaDB guardas documentos con embeddings:

import chromadb

# Crear cliente (persistente en disco)
client = chromadb.PersistentClient(path="./mi_db")

# Crear una "colección" (como una tabla)
collection = client.get_or_create_collection(
    name="conceptos",
    metadata={"hnsw:space": "cosine"}  # Distancia coseno
)

# Guardar documentos
collection.add(
    ids=["c1", "c2", "c3"],
    documents=["función pura", "bucle for", "recursión"],
    metadatas=[
        {"clase": "class_010", "tipo": "notes"},
        {"clase": "class_015", "tipo": "notes"},
        {"clase": "class_020", "tipo": "notes"},
    ]
)

Eso es todo. ChromaDB automáticamente: 1. Genera embeddings de los documentos (por defecto usa all-MiniLM-L6-v2) 2. Los indexa para búsqueda rápida 3. Los persiste en disco

Buscar por similitud

results = collection.query(
    query_texts=["higher-order function"],
    n_results=3
)

print(results["documents"])
# [['función de orden superior', 'función pura', 'recursión']]

print(results["distances"])
# [[0.23, 0.45, 0.67]]  # Menor = más similar

¿Ves? Busqué «higher-order function» y encontró «función de orden superior» aunque el texto es completamente diferente. Eso es la magia de los embeddings.

El sistema completo: validación curricular

Ahora vamos a construir el sistema que valida que no meta la pata. El código real está en mi proyecto, pero aquí te doy la versión simplificada para que entiendas el concepto.

Paso 1: Extraer conceptos de notebooks

Primero necesitamos sacar los conceptos de cada notebook. Esto lo hago con un LLM (Gemini Flash via OpenRouter), pero podrías hacerlo con regexes si eres valiente:

def extract_concepts_from_notebook(notebook_path: Path) -> list[dict]:
    """
    Extrae conceptos de un notebook Jupyter.

    Returns:
        Lista de {"name": "concepto", "category": "introduces|uses"}
    """
    content = get_notebook_content(notebook_path)

    # Llamar al LLM para extraer conceptos
    response = llm.chat(
        messages=[
            {"role": "system", "content": EXTRACTION_PROMPT},
            {"role": "user", "content": content}
        ]
    )

    return json.loads(response)

El LLM clasifica cada concepto como: – introduces: Se enseña con explicación – uses: Se usa asumiendo conocimiento previo

Paso 2: Guardar en ChromaDB

Ahora guardamos los conceptos con sus metadatos:

from chromadb.utils import embedding_functions

# Usar modelo multilingüe (español + inglés)
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="paraphrase-multilingual-MiniLM-L12-v2"
)

collection = client.get_or_create_collection(
    name="course_concepts",
    embedding_function=embedding_fn,
    metadata={"hnsw:space": "cosine"}
)

# Guardar conceptos
for class_id, concepts in course_concepts.items():
    for concept in concepts:
        collection.add(
            ids=[f"{class_id}:{concept['name']}"],
            documents=[concept["name"]],
            metadatas=[{
                "class_id": class_id,
                "category": concept["category"],
                "source_type": concept["source_type"],  # notes o labs
            }]
        )

Paso 3: Validar la progresión

Aquí viene lo interesante. Para cada concepto «usado» en un lab, verificamos que exista algo similar en notes anteriores:

def validate_curriculum(course_concepts: dict) -> list[str]:
    """
    Valida que los labs no usen conceptos no enseñados.

    Returns:
        Lista de errores encontrados
    """
    errors = []
    known_concepts = set()

    # Procesar clases en orden
    for class_id in sorted(course_concepts.keys()):
        class_data = course_concepts[class_id]

        # Añadir conceptos introducidos en notes a los conocidos
        for c in class_data:
            if c["source_type"] == "notes" and c["category"] == "introduces":
                known_concepts.add(c["name"].lower())

        # Validar conceptos usados en labs
        for c in class_data:
            if c["source_type"] == "labs" and c["category"] == "uses":
                if not is_concept_known(c["name"], known_concepts):
                    errors.append(
                        f"{class_id}: '{c['name']}' usado sin enseñar"
                    )

    return errors

La función is_concept_known es donde entra ChromaDB. No hacemos match exacto, hacemos búsqueda semántica:

def is_concept_known(concept: str, known_concepts: set) -> bool:
    """Verifica si un concepto es conocido (exacto o semántico)."""

    # 1. Match exacto
    if concept.lower() in known_concepts:
        return True

    # 2. Búsqueda semántica
    results = collection.query(
        query_texts=[concept],
        n_results=3,
        where={"category": "introduces"}  # Solo buscar en "introduces"
    )

    # Si hay algo muy similar (distancia < 0.3), lo consideramos conocido
    if results["distances"][0] and results["distances"][0][0] < 0.3:
        return True

    return False

Paso 4: El informe

Ejecutando la validación sobre mi curso, obtengo un bonito informe:

# Informe de Validación Curricular

Se encontraron 17 problemas:

## class_006_abstraciones_abstraccion_funcion

- **refactorizar**: Concepto 'refactorizar' usado en lab pero no enseñado
  - Archivo: `labs/0.funciones_basicas.ipynb`

## class_020_secuencias_while

- **memoización**: Concepto 'memoización' usado en lab pero no enseñado
  - Archivo: `labs/2.generar_secuencias.ipynb`

## class_026_funciones_orden_superior

- **map**: Concepto 'map' usado en lab pero no enseñado
  - Archivo: `labs/0.ejercicios_aplicadores_listas.ipynb`

Ahora sé exactamente qué tengo que arreglar.

Detalles que importan

Modelo de embeddings multilingüe

Si tu contenido está en español, usa un modelo multilingüe:

embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="paraphrase-multilingual-MiniLM-L12-v2"
)

El modelo por defecto (all-MiniLM-L6-v2) está entrenado principalmente en inglés y puede dar resultados raros con español.

Distancia coseno vs euclidiana

Para texto, usa distancia coseno:

collection = client.get_or_create_collection(
    name="concepts",
    metadata={"hnsw:space": "cosine"}  # ← Esto
)

La distancia coseno mide el ángulo entre vectores, ignorando la magnitud. Esto es lo que quieres para similitud semántica.

Convertir distancia a similitud

ChromaDB devuelve distancia (menor = más similar). Si quieres similitud (mayor = más similar):

similarity = 1 - distance

Con distancia coseno, el rango es [0, 2], así que la similitud queda en [-1, 1]. En la práctica, para textos similares suele estar en [0.5, 1].

Persistencia

ChromaDB tiene dos modos:

# En memoria (se pierde al cerrar)
client = chromadb.Client()

# Persistente (se guarda en disco)
client = chromadb.PersistentClient(path="./data/chroma")

Para un sistema de validación que vas a ejecutar repetidamente, usa persistente. Así no recalculas embeddings cada vez.

Filtros con where

Puedes filtrar resultados por metadatos:

# Solo buscar en notes
results = collection.query(
    query_texts=["función"],
    where={"source_type": "notes"}
)

# Solo buscar en clases anteriores a la 020
results = collection.query(
    query_texts=["función"],
    where={"class_num": {"$lt": 20}}
)

Esto es crucial para la validación: solo queremos buscar en conceptos que ya se han enseñado.

Alternativas a ChromaDB

ChromaDB no es la única opción. Aquí tienes otras:

Herramienta Pros Contras
ChromaDB Simple, sin servidor, buena documentación Limitado a millones de vectores
Pinecone Escalable, managed De pago, vendor lock-in
Weaviate Potente, GraphQL API Más complejo de configurar
Qdrant Rápido, Rust Menos conocido
pgvector Si ya usas PostgreSQL Requiere PostgreSQL

Para un proyecto como este (miles de conceptos, no millones), ChromaDB es perfecto. Si necesitas escalar a billones de vectores o alta disponibilidad, mira las alternativas.

El hook de pre-commit

Para que esto sea útil de verdad, lo integré en el workflow de git:

#!/bin/bash
# .git/hooks/pre-commit

echo "🔍 Validando progresión curricular..."

if python bin/concept_index.py validate; then
    echo "✓ Validación exitosa"
    exit 0
else
    echo "❌ Hay conceptos usados sin enseñar"
    echo "   Ejecuta: make concept-validate-report"
    exit 1
fi

Ahora, cada vez que intento hacer commit, el sistema verifica que no estoy metiendo la pata. Si hay violaciones, el commit se bloquea y me dice qué arreglar.

Conclusión

ChromaDB es una de esas herramientas que cuando la descubres piensas «¿cómo he vivido sin esto?». Es SQLite para embeddings: simple, local, y funciona.

El caso de uso que te he mostrado (validación curricular) es solo un ejemplo. Las bases de datos vectoriales sirven para:

  • Búsqueda semántica en documentos
  • RAG (Retrieval-Augmented Generation) para LLMs
  • Detección de duplicados semánticos
  • Recomendaciones basadas en similitud
  • Clustering de contenido

Y lo mejor: la barrera de entrada es mínima. Instalas, guardas documentos, buscas. Nada de configurar servidores, schemas, o índices complicados.

Si tienes un problema donde necesitas encontrar «cosas parecidas a esto», prueba ChromaDB. Lo peor que puede pasar es que funcione demasiado bien y te preguntes por qué no lo usaste antes.


TL;DR: ChromaDB es una base de datos vectorial local y simple. La usamos para verificar que un curso de programación no use conceptos antes de enseñarlos, usando búsqueda semántica para detectar conceptos similares aunque el texto sea diferente.


Read this article in English.

Noticias recientes del mundo tech


¡CONVOCATORIA ABIERTA!

Desarrollo de apps móviles ios & Android

Full Stack Bootcamp

Clases en Directo | Profesores en Activo | Temario 100% actualizado

Descárgate también el informe de tendencias en el mercado laboral 2026.

Fórmate con planes adaptados a tus objetivos y logra resultados en tiempo récord.
KeepCoding Bootcamps
Resumen de privacidad

Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.