Módulo 1: ¿Qué son Embeddings?

Mini-Proyecto: Primer Embedding - Similarity Calculator

Descripción del proyecto

En este mini-proyecto construirás tu primer sistema funcional usando embeddings: un Similarity Calculator que permite buscar documentos similares a un query usando OpenAI embeddings.

Este proyecto consolida TODOS los conceptos del Módulo 1: definición de embeddings, propiedades vectoriales, espacios vectoriales, casos de uso, comparación con keywords, y arquitectura. Al final tendrás una herramienta CLI production-ready que podrás expandir en módulos futuros.

Duración estimada: 30-40 minutos


Objetivos del proyecto

Al completar este proyecto, habrás:

  • ✅ Configurado OpenAI API correctamente
  • ✅ Generado embeddings de múltiples documentos
  • ✅ Calculado cosine similarity con numpy
  • ✅ Implementado búsqueda de top-K más similares
  • ✅ Creado una CLI funcional con manejo de errores
  • ✅ Guardado embeddings en caché (optimización)

Especificaciones técnicas

Funcionalidades obligatorias:

  1. Indexación: Cargar 10+ documentos y generar embeddings
  2. Query: Usuario ingresa query, sistema retorna top-3 más similares
  3. Scoring: Mostrar cosine similarity score para cada resultado
  4. Caché: Guardar embeddings en archivo para reutilizar
  5. Error handling: Manejar API errors, archivos faltantes, etc.

Stack tecnológico:

  • Python 3.10+
  • OpenAI API (text-embedding-3-small)
  • NumPy (cálculos vectoriales)
  • python-dotenv (environment variables)
  • JSON (persistencia simple)

Paso 1: Setup del proyecto

1.1: Estructura de archivos

embeddings-similarity-calculator/
├── .env                    # API key (NO commitear)
├── .gitignore             # Ignorar .env
├── requirements.txt       # Dependencias
├── documents.txt          # Corpus (10 documentos)
├── embeddings_cache.json  # Embeddings guardados
└── similarity_calculator.py  # Main script

1.2: Crear carpeta del proyecto

mkdir embeddings-similarity-calculator
cd embeddings-similarity-calculator

1.3: Crear virtualenv (recomendado)

# Crear virtualenv
python -m venv venv

# Activar
# macOS/Linux:
source venv/bin/activate

# Windows:
venv\Scripts\activate

1.4: Instalar dependencias

Crear requirements.txt:

openai==1.12.0
numpy==1.26.3
python-dotenv==1.0.0

Instalar:

pip install -r requirements.txt

1.5: Configurar OpenAI API key

Crear .env:

# .env
OPENAI_API_KEY=sk-proj-tu-api-key-aqui

Crear .gitignore:

# .gitignore
.env
venv/
__pycache__/
*.pyc
embeddings_cache.json

⚠️ IMPORTANTE: NUNCA commitees .env con tu API key.


Paso 2: Crear corpus de documentos

Crear documents.txt:

Python es un lenguaje de programación de alto nivel
JavaScript es el lenguaje principal para desarrollo web
TypeScript agrega tipos estáticos a JavaScript
React es una biblioteca para construir interfaces de usuario
Vue.js es un framework progresivo para construir UIs
Django es un framework web de Python para backend
FastAPI es un framework moderno y rápido para APIs en Python
Node.js permite ejecutar JavaScript en el servidor
Docker es una plataforma para containerizar aplicaciones
Kubernetes orquesta contenedores en producción

Cada línea = 1 documento.


Paso 3: Implementar Similarity Calculator

Crear similarity_calculator.py:

"""
Similarity Calculator - Mini-Proyecto Módulo 1
Sistema de búsqueda semántica usando OpenAI embeddings
"""

import os
import json
import numpy as np
from openai import OpenAI
from dotenv import load_dotenv
from typing import List, Dict, Tuple

# Cargar environment variables
load_dotenv()

# Configuración
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
EMBEDDING_MODEL = "text-embedding-3-small"
DOCUMENTS_FILE = "documents.txt"
CACHE_FILE = "embeddings_cache.json"


class SimilarityCalculator:
    """
    Sistema de búsqueda semántica con embeddings
    """
    
    def __init__(self):
        """Inicializar OpenAI client y estructuras de datos"""
        if not OPENAI_API_KEY:
            raise ValueError("OPENAI_API_KEY no encontrado en .env")
        
        self.client = OpenAI(api_key=OPENAI_API_KEY)
        self.documents: List[str] = []
        self.embeddings: Dict[int, List[float]] = {}
    
    def load_documents(self, filepath: str) -> None:
        """
        Cargar documentos desde archivo
        
        Args:
            filepath: Path al archivo de documentos
        """
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                self.documents = [line.strip() for line in f if line.strip()]
            
            print(f"✅ Cargados {len(self.documents)} documentos")
        
        except FileNotFoundError:
            raise FileNotFoundError(f"Archivo {filepath} no encontrado")
        except Exception as e:
            raise Exception(f"Error al cargar documentos: {e}")
    
    def get_embedding(self, text: str) -> List[float]:
        """
        Generar embedding para un texto
        
        Args:
            text: Texto a convertir en embedding
        
        Returns:
            Lista de floats (embedding vector)
        """
        try:
            response = self.client.embeddings.create(
                model=EMBEDDING_MODEL,
                input=text
            )
            return response.data[0].embedding
        
        except Exception as e:
            print(f"❌ Error al generar embedding: {e}")
            raise
    
    def generate_embeddings(self, use_cache: bool = True) -> None:
        """
        Generar embeddings para todos los documentos
        
        Args:
            use_cache: Si True, intenta cargar de caché primero
        """
        # Intentar cargar de caché
        if use_cache and self.load_cache():
            print("✅ Embeddings cargados de caché")
            return
        
        print(f"🔄 Generando embeddings para {len(self.documents)} documentos...")
        
        for idx, doc in enumerate(self.documents):
            try:
                embedding = self.get_embedding(doc)
                self.embeddings[idx] = embedding
                print(f"  [{idx+1}/{len(self.documents)}] Generado")
            
            except Exception as e:
                print(f"  ❌ Error en documento {idx}: {e}")
        
        # Guardar en caché
        self.save_cache()
        print("✅ Embeddings generados y guardados en caché")
    
    def save_cache(self) -> None:
        """Guardar embeddings en archivo JSON"""
        try:
            cache_data = {
                "documents": self.documents,
                "embeddings": {str(k): v for k, v in self.embeddings.items()}
            }
            
            with open(CACHE_FILE, 'w', encoding='utf-8') as f:
                json.dump(cache_data, f)
        
        except Exception as e:
            print(f"⚠️ Error al guardar caché: {e}")
    
    def load_cache(self) -> bool:
        """
        Cargar embeddings desde caché
        
        Returns:
            True si se cargó exitosamente, False si no
        """
        try:
            if not os.path.exists(CACHE_FILE):
                return False
            
            with open(CACHE_FILE, 'r', encoding='utf-8') as f:
                cache_data = json.load(f)
            
            # Verificar que documentos coincidan
            if cache_data["documents"] != self.documents:
                print("⚠️ Documentos cambiaron, regenerando embeddings...")
                return False
            
            # Cargar embeddings
            self.embeddings = {
                int(k): v for k, v in cache_data["embeddings"].items()
            }
            
            return True
        
        except Exception as e:
            print(f"⚠️ Error al cargar caché: {e}")
            return False
    
    @staticmethod
    def cosine_similarity(vec_a: List[float], vec_b: List[float]) -> float:
        """
        Calcular cosine similarity entre dos vectores
        
        Args:
            vec_a: Primer vector
            vec_b: Segundo vector
        
        Returns:
            Similaridad (0.0 a 1.0)
        """
        vec_a = np.array(vec_a)
        vec_b = np.array(vec_b)
        
        dot_product = np.dot(vec_a, vec_b)
        norm_a = np.linalg.norm(vec_a)
        norm_b = np.linalg.norm(vec_b)
        
        return dot_product / (norm_a * norm_b)
    
    def search(self, query: str, top_k: int = 3) -> List[Tuple[int, str, float]]:
        """
        Buscar documentos más similares al query
        
        Args:
            query: Query del usuario
            top_k: Cantidad de resultados a retornar
        
        Returns:
            Lista de tuplas (doc_id, documento, similarity)
        """
        # Generar embedding del query
        print(f"\n🔍 Buscando: '{query}'")
        query_embedding = self.get_embedding(query)
        
        # Calcular similaridades
        similarities = []
        for doc_id, doc_embedding in self.embeddings.items():
            sim = self.cosine_similarity(query_embedding, doc_embedding)
            similarities.append((doc_id, self.documents[doc_id], sim))
        
        # Ordenar por similaridad (descendente)
        similarities.sort(key=lambda x: x[2], reverse=True)
        
        # Retornar top-K
        return similarities[:top_k]
    
    def print_results(self, results: List[Tuple[int, str, float]]) -> None:
        """
        Imprimir resultados formateados
        
        Args:
            results: Lista de resultados [(doc_id, doc, similarity)]
        """
        print("\n📊 Resultados:")
        print("=" * 80)
        
        for rank, (doc_id, doc, similarity) in enumerate(results, 1):
            # Barra de progreso visual
            bar_length = int(similarity * 50)  # 50 chars max
            bar = "█" * bar_length + "░" * (50 - bar_length)
            
            print(f"\n{rank}. [Doc {doc_id}] Similarity: {similarity:.4f}")
            print(f"   {bar} {similarity:.1%}")
            print(f"   \"{doc}\"")
        
        print("\n" + "=" * 80)


def main():
    """Función principal - CLI interactivo"""
    print("=" * 80)
    print("🚀 SIMILARITY CALCULATOR - Embeddings Deep Dive Guide")
    print("=" * 80)
    
    try:
        # Inicializar
        calculator = SimilarityCalculator()
        
        # Cargar documentos
        calculator.load_documents(DOCUMENTS_FILE)
        
        # Generar embeddings (usa caché si existe)
        calculator.generate_embeddings(use_cache=True)
        
        # Loop interactivo
        print("\n" + "=" * 80)
        print("💬 Ingresa tu query (o 'exit' para salir)")
        print("=" * 80)
        
        while True:
            query = input("\nQuery: ").strip()
            
            if query.lower() in ['exit', 'quit', 'salir']:
                print("\n👋 ¡Hasta luego!")
                break
            
            if not query:
                print("⚠️ Query vacío. Intenta de nuevo.")
                continue
            
            # Buscar
            results = calculator.search(query, top_k=3)
            
            # Mostrar resultados
            calculator.print_results(results)
    
    except KeyboardInterrupt:
        print("\n\n👋 Interrupted. ¡Hasta luego!")
    
    except Exception as e:
        print(f"\n❌ Error: {e}")
        import traceback
        traceback.print_exc()


if __name__ == "__main__":
    main()

Paso 4: Ejecutar el proyecto

Primera ejecución (sin caché):

python similarity_calculator.py

Output esperado:

================================================================================
🚀 SIMILARITY CALCULATOR - Embeddings Deep Dive Guide
================================================================================
✅ Cargados 10 documentos
🔄 Generando embeddings para 10 documentos...
  [1/10] Generado
  [2/10] Generado
  [3/10] Generado
  [4/10] Generado
  [5/10] Generado
  [6/10] Generado
  [7/10] Generado
  [8/10] Generado
  [9/10] Generado
  [10/10] Generado
✅ Embeddings generados y guardados en caché

================================================================================
💬 Ingresa tu query (o 'exit' para salir)
================================================================================

Query: python para backend

🔍 Buscando: 'python para backend'

📊 Resultados:
================================================================================

1. [Doc 5] Similarity: 0.8923
   ████████████████████████████████████████████░░░░░░ 89.2%
   "Django es un framework web de Python para backend"

2. [Doc 6] Similarity: 0.8756
   ███████████████████████████████████████████░░░░░░░ 87.6%
   "FastAPI es un framework moderno y rápido para APIs en Python"

3. [Doc 0] Similarity: 0.8234
   █████████████████████████████████████████░░░░░░░░░ 82.3%
   "Python es un lenguaje de programación de alto nivel"

================================================================================

Query: contenedores

🔍 Buscando: 'contenedores'

📊 Resultados:
================================================================================

1. [Doc 8] Similarity: 0.9012
   █████████████████████████████████████████████░░░░░ 90.1%
   "Docker es una plataforma para containerizar aplicaciones"

2. [Doc 9] Similarity: 0.8678
   ███████████████████████████████████████████░░░░░░░ 86.8%
   "Kubernetes orquesta contenedores en producción"

3. [Doc 7] Similarity: 0.6823
   ██████████████████████████████████░░░░░░░░░░░░░░░░ 68.2%
   "Node.js permite ejecutar JavaScript en el servidor"

================================================================================

Query: exit
👋 ¡Hasta luego!

Segunda ejecución (con caché):

python similarity_calculator.py

Output:

================================================================================
🚀 SIMILARITY CALCULATOR - Embeddings Deep Dive Guide
================================================================================
✅ Cargados 10 documentos
✅ Embeddings cargados de caché

================================================================================
💬 Ingresa tu query (o 'exit' para salir)
================================================================================

Query: 

Nota: Esta vez NO generó embeddings (cargó de embeddings_cache.json). Ahorro de tiempo y costos API.


Paso 5: Testing manual

Queries de prueba:

Query: framework para interfaces
# Debería retornar: React, Vue.js, (relacionados)

Query: lenguaje tipado
# Debería retornar: TypeScript, (relacionados)

Query: ejecutar en servidor
# Debería retornar: Node.js, FastAPI, Django

Query: orquestación
# Debería retornar: Kubernetes, Docker

Query: desarrollo web moderno
# Debería retornar: React, Vue.js, JavaScript

Validaciones técnicas

Checklist de funcionalidad:

  • Sistema carga documentos correctamente
  • Genera embeddings (1536 dims cada uno)
  • Calcula cosine similarity correctamente
  • Retorna top-3 ordenados por similarity
  • Guarda caché en JSON
  • Carga caché en ejecuciones subsecuentes
  • Maneja errors (API key inválida, archivo faltante)
  • CLI interactivo funciona
  • Scores están en rango [0.0, 1.0]
  • Resultados son semánticamente correctos

Mejoras opcionales (Extra Credit)

Nivel 1: Funcionalidades adicionales

# 1. Normalizar embeddings
def normalize_embedding(self, embedding: List[float]) -> List[float]:
    """Normalizar embedding a magnitud 1.0"""
    embedding_array = np.array(embedding)
    return (embedding_array / np.linalg.norm(embedding_array)).tolist()

# 2. Mostrar estadísticas
def print_stats(self):
    """Imprimir estadísticas del corpus"""
    print(f"\n📈 Estadísticas:")
    print(f"  Documentos: {len(self.documents)}")
    print(f"  Embeddings: {len(self.embeddings)}")
    print(f"  Dimensiones: 1536")
    print(f"  Modelo: {EMBEDDING_MODEL}")

Nivel 2: Comparar con BM25

# Agregar búsqueda keyword simple
def keyword_search(self, query: str, top_k: int = 3):
    """Búsqueda BM25 simplificada (keyword matching)"""
    query_words = set(query.lower().split())
    
    scores = []
    for idx, doc in enumerate(self.documents):
        doc_words = set(doc.lower().split())
        matches = len(query_words.intersection(doc_words))
        scores.append((idx, doc, matches))
    
    scores.sort(key=lambda x: x[2], reverse=True)
    return scores[:top_k]

# Comparar ambos métodos:
semantic_results = calculator.search(query, top_k=3)
keyword_results = calculator.keyword_search(query, top_k=3)

print("\n🔹 Semantic Search:")
calculator.print_results(semantic_results)

print("\n🔹 Keyword Search:")
# print keyword results...

Nivel 3: Hybrid search

def hybrid_search(self, query: str, alpha: float = 0.5, top_k: int = 3):
    """
    Hybrid search: Semantic + Keyword
    
    Args:
        query: Query del usuario
        alpha: Peso de keyword (1-alpha = peso de semantic)
        top_k: Cantidad de resultados
    """
    # Semantic scores
    query_emb = self.get_embedding(query)
    semantic_scores = {}
    for doc_id, doc_emb in self.embeddings.items():
        sim = self.cosine_similarity(query_emb, doc_emb)
        semantic_scores[doc_id] = (sim + 1) / 2  # Normalizar a [0, 1]
    
    # Keyword scores
    query_words = set(query.lower().split())
    keyword_scores = {}
    for doc_id, doc in enumerate(self.documents):
        doc_words = set(doc.lower().split())
        matches = len(query_words.intersection(doc_words))
        keyword_scores[doc_id] = matches
    
    # Normalizar keyword scores
    max_keyword = max(keyword_scores.values()) if keyword_scores else 1
    keyword_normalized = {
        k: v / max_keyword for k, v in keyword_scores.items()
    }
    
    # Hybrid scores
    hybrid_scores = []
    for doc_id in range(len(self.documents)):
        semantic_score = semantic_scores.get(doc_id, 0)
        keyword_score = keyword_normalized.get(doc_id, 0)
        
        hybrid_score = alpha * keyword_score + (1 - alpha) * semantic_score
        
        hybrid_scores.append((doc_id, self.documents[doc_id], hybrid_score))
    
    # Ordenar y retornar top-K
    hybrid_scores.sort(key=lambda x: x[2], reverse=True)
    return hybrid_scores[:top_k]

Troubleshooting

Problema 1: "OPENAI_API_KEY no encontrado"

Causa: .env no existe o no tiene la key.

Solución:

# Crear .env:
echo "OPENAI_API_KEY=sk-proj-tu-key-aqui" > .env

# Verificar que está cargando:
python -c "from dotenv import load_dotenv; import os; load_dotenv(); print(os.getenv('OPENAI_API_KEY'))"

Problema 2: "FileNotFoundError: documents.txt"

Causa: Archivo no existe.

Solución:

# Crear documents.txt con el contenido del Paso 2

Problema 3: API rate limit error

Causa: Demasiadas llamadas a OpenAI API.

Solución:

# Agregar delay entre llamadas:
import time

for idx, doc in enumerate(self.documents):
    embedding = self.get_embedding(doc)
    self.embeddings[idx] = embedding
    time.sleep(0.1)  # 100ms delay

Problema 4: Similarity scores todos ~0.60-0.70

Causa: Corpus muy heterogéneo o query muy genérico.

Solución: Normal. Scores >0.80 indican alta similaridad. Scores 0.60-0.70 son "algo relacionados".


Criterios de éxito

Funcionalidad (70 puntos):

  • (15 pts) Sistema carga documentos
  • (20 pts) Genera embeddings correctamente
  • (15 pts) Calcula cosine similarity
  • (10 pts) Retorna top-3 ordenados
  • (10 pts) Caché funciona

Código (20 puntos):

  • (5 pts) Código limpio y organizado
  • (5 pts) Type hints consistentes
  • (5 pts) Docstrings en funciones
  • (5 pts) Error handling robusto

Usabilidad (10 puntos):

  • (5 pts) CLI intuitivo
  • (5 pts) Output legible y formateado

Resumen del proyecto

Has construido:

  • ✅ Sistema de búsqueda semántica funcional
  • ✅ Integración con OpenAI API
  • ✅ Cálculo de cosine similarity con numpy
  • ✅ Caché de embeddings (optimización)
  • ✅ CLI interactivo

Skills adquiridos:

  • Consumir APIs de embeddings
  • Manipulación de vectores con numpy
  • Persistencia de datos (JSON)
  • Error handling robusto
  • Arquitectura de búsqueda semántica

Este proyecto es la base para:

  • Módulo 2: Arquitectura técnica profunda
  • Módulo 4: Semantic search desde cero con numpy
  • Módulo 6: Embeddings en producción (batch, cache)
  • Módulo 8: Proyecto integrador completo

Recursos adicionales

  1. OpenAI Embeddings API Docs - Documentación oficial
  2. NumPy Docs - NumPy para cálculos vectoriales
  3. Python dotenv - Environment variables
  4. JSON in Python - Serialización
  5. Cosine Similarity Explained - Matemáticas

Próximos pasos

Módulo 2: ¿Cómo funcionan Embeddings?

Profundizarás en:

  • Transformer encoders (arquitectura detallada)
  • Tokenización (BPE, WordPiece)
  • Self-attention mechanisms
  • Pooling strategies (código real)
  • OpenAI API avanzado (batching, rate limiting)

Este proyecto sienta las bases. Los siguientes módulos construyen sobre esto.


Felicitaciones! 🎉

Has completado el Módulo 1: ¿Qué son Embeddings?

Lo que lograste:

  • ✅ Entendiste qué son embeddings y por qué son críticos
  • ✅ Aprendiste propiedades vectoriales y cosine similarity
  • ✅ Comprendiste espacios vectoriales de alta dimensionalidad
  • ✅ Identificaste casos de uso reales (RAG, search, recommendations)
  • ✅ Comparaste embeddings vs keyword search
  • ✅ Conociste arquitectura high-level de Transformers
  • ✅ Construiste tu primer sistema funcional

Estás listo para Módulo 2: Arquitectura profunda de embeddings. 🚀


Módulo 1 completado - Embeddings Deep Dive Guide De conceptos fundamentales a implementación práctica