Módulo 1: RAG Pipeline Completo (Architecture Overview)

RAG vs Búsqueda Tradicional (Keywords)

Descripción de la cápsula

RAG no siempre es la solución correcta. A veces, búsqueda tradicional (keyword search, SQL queries, grep) es más simple, rápida, y barata. Esta cápsula te enseña cuándo usar RAG y cuándo no, con ejemplos concretos de trade-offs.

Entender cuándo NO usar RAG es tan importante como saber cómo usarlo bien. Si tu dataset tiene 50 documentos estructurados con IDs exactos, SQL search es 10x más rápido y 100x más barato que RAG. Si tu query es "find transaction_id=12345", keyword search responde en 5ms vs RAG que toma 500ms.

Esta cápsula te da: (1) Comparación técnica RAG vs Keywords, (2) Criterios de decisión basados en dataset y query type, (3) Casos donde RAG gana, (4) Casos donde keywords ganan, (5) Casos híbridos donde ambos se combinan.


🔍 Búsqueda Tradicional (Keywords)

¿Cómo funciona keyword search?

Busca matches exactos o parciales de palabras en texto.

# Ejemplo 1: SQL search (structured data)
query = "SELECT * FROM docs WHERE title LIKE '%FastAPI%'"

# Ejemplo 2: grep (unstructured text)
query = "grep -i 'fastapi' docs/*.txt"

# Ejemplo 3: BM25 (term frequency ranking)
from rank_bm25 import BM25Okapi

corpus = ["FastAPI is a web framework", "Django is a web framework", "Flask is lightweight"]
tokenized_corpus = [doc.split() for doc in corpus]

bm25 = BM25Okapi(tokenized_corpus)
query = "fastapi framework"
scores = bm25.get_scores(query.split())

# Output: [high_score, low_score, low_score]

Características de keyword search:

AspectoDescripciónEjemplo
Match typeExact o partial match de términos"FastAPI" → match literal
Context-aware❌ No entiende sinónimos ni context"framework" no match "library"
Latency✅ Muy rápido (5-50ms)Indexed search
Costo✅ Casi cero (local compute)No API calls
Setup✅ Simple (SQL, grep, BM25)pip install rank-bm25

🤖 Búsqueda Semántica (RAG)

¿Cómo funciona semantic search?

Convierte texto a embeddings (vectores) y busca por similarity.

from openai import OpenAI
import chromadb

# 1. Indexing: Convertir docs a embeddings
client = OpenAI()
docs = ["FastAPI is a web framework", "Django is a web framework"]

embeddings = [
    client.embeddings.create(model="text-embedding-ada-002", input=doc).data[0].embedding
    for doc in docs
]

# 2. Storage: Almacenar en vector DB
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("docs")
collection.add(documents=docs, embeddings=embeddings, ids=["1", "2"])

# 3. Retrieval: Query semántica
query = "python web framework"
query_embedding = client.embeddings.create(model="text-embedding-ada-002", input=query).data[0].embedding

results = collection.query(query_embeddings=[query_embedding], n_results=2)

# Output: Match "FastAPI" y "Django" aunque query dice "python" (no está en docs)

Características de semantic search:

AspectoDescripciónEjemplo
Match typeSemantic similarity (context-aware)"framework" match "library"
Context-aware✅ Entiende sinónimos y conceptos"python web" match "FastAPI"
Latency⚠️ Más lento (200-500ms)Embedding + vector search
Costo⚠️ API calls ($0.0001/1K tokens)OpenAI embeddings
Setup⚠️ Más complejo (embeddings + vector DB)ChromaDB + OpenAI

⚖️ Comparación Directa

Ejemplo: Buscar "FastAPI tutorial"

Keyword Search (BM25):

from rank_bm25 import BM25Okapi

corpus = [
    "FastAPI is a modern web framework",
    "Django tutorial for beginners",
    "Building APIs with FastAPI",
    "Python web development guide"
]

tokenized_corpus = [doc.split() for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)

query = "FastAPI tutorial"
scores = bm25.get_scores(query.split())

# Ranking:
# 1. "Building APIs with FastAPI" (0.85) ← Tiene "FastAPI"
# 2. "FastAPI is a modern web framework" (0.72) ← Tiene "FastAPI"
# 3. "Django tutorial for beginners" (0.42) ← Tiene "tutorial"
# 4. "Python web development guide" (0.10) ← No match directo

Resultado: Top-2 son relevantes porque tienen "FastAPI". Pero "Django tutorial" rankea tercero solo por "tutorial", aunque no es sobre FastAPI.


Semantic Search (Embeddings):

from openai import OpenAI
import chromadb

client = OpenAI()
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("docs")

# Indexar corpus
embeddings = [
    client.embeddings.create(model="text-embedding-ada-002", input=doc).data[0].embedding
    for doc in corpus
]

collection.add(documents=corpus, embeddings=embeddings, ids=["1", "2", "3", "4"])

# Query
query = "FastAPI tutorial"
query_embedding = client.embeddings.create(model="text-embedding-ada-002", input=query).data[0].embedding

results = collection.query(query_embeddings=[query_embedding], n_results=4)

# Ranking (cosine similarity):
# 1. "Building APIs with FastAPI" (0.92) ← Tema + herramienta match
# 2. "FastAPI is a modern web framework" (0.89) ← Herramienta match
# 3. "Python web development guide" (0.78) ← Tema relacionado (web dev)
# 4. "Django tutorial for beginners" (0.75) ← Tema similar pero herramienta diferente

Resultado: Top-2 son relevantes (FastAPI). "Python web development" rankea tercero porque es conceptualmente relacionado, aunque no tenga "FastAPI" literal.


Diferencia clave:

Keywords: Match literal de "FastAPI" → Miss "Python web development" (que es relevante)
Semantic: Match conceptual → Encuentra "Python web development" (relevante por contexto)


🎯 Cuándo Usar Keywords

Caso 1: Exact Match Queries

Ejemplo: Buscar transaction ID, user ID, código de error.

# Query: "Find transaction_id=12345"

# Keywords (SQL): ✅ Perfecto
query = "SELECT * FROM transactions WHERE id = 12345"
# Latency: 5ms
# Costo: Cero

# Semantic (RAG): ❌ Overkill
query_embedding = create_embedding("transaction 12345")
results = collection.query(query_embeddings=[query_embedding])
# Latency: 500ms
# Costo: $0.0001
# Problema: Embedding puede confundir "12345" con "12346" (similar semánticamente)

Decisión: Keywords gana. Exact match es más rápido y preciso.


Caso 2: Structured Data

Ejemplo: Buscar en base de datos con schema definido.

# Dataset: Usuarios con campos (name, email, age, city)

# Query: "Find users in San Francisco older than 30"

# Keywords (SQL): ✅ Perfecto
query = "SELECT * FROM users WHERE city = 'San Francisco' AND age > 30"
# Latency: 10ms
# Precisión: 100%

# Semantic (RAG): ❌ No sirve bien
# Problema: Embeddings no entienden ranges (">30") ni structured filters

Decisión: Keywords gana. Structured data es dominio de SQL.


Caso 3: Small Dataset (<100 docs)

Ejemplo: Buscar en 20 documentos de políticas internas.

# Dataset: 20 documentos de 1-2 páginas cada uno

# Keywords (grep): ✅ Suficiente
grep -i "vacation policy" docs/*.txt
# Latency: 50ms
# Costo: Cero

# Semantic (RAG): ❌ Overkill
# Setup: Crear embeddings (20 API calls), vector DB, etc.
# Latency: 500ms
# Costo: $0.002
# Beneficio: Minimal (dataset pequeño, keywords suficiente)

Decisión: Keywords gana. Setup de RAG no vale la pena para 20 docs.


Caso 4: Code Search (IDs, function names)

Ejemplo: Buscar function definition en codebase.

# Query: "Find function calculate_total"

# Keywords (grep): ✅ Perfecto
grep -r "def calculate_total" src/
# Latency: 100ms
# Precisión: 100%

# Semantic (RAG): ❌ Confunde
query_embedding = create_embedding("calculate total")
# Problema: Puede match "compute_sum", "get_total", etc. (similar semánticamente)
# Pero queremos EXACTAMENTE "calculate_total"

Decisión: Keywords gana. Function names necesitan exact match.


🤖 Cuándo Usar RAG

Caso 1: Conceptual Queries

Ejemplo: "¿Cómo funciona autenticación en FastAPI?"

# Dataset: Documentación de FastAPI (500 páginas)

# Keywords (grep): ❌ Miss context
grep -i "authentication" docs/*.txt
# Problema: Devuelve 100+ matches de "authentication" literal
# Pero no rankea por relevancia conceptual

# Semantic (RAG): ✅ Perfecto
query = "How does authentication work in FastAPI"
query_embedding = create_embedding(query)
results = collection.query(query_embeddings=[query_embedding], n_results=5)

# Output: Top-5 documentos sobre auth en FastAPI, rankeados por relevancia semántica
# Encuentra "OAuth2", "JWT", "security" aunque query no menciona esos términos

Decisión: RAG gana. Entiende conceptos relacionados.


Caso 2: Sinónimos y Parafraseo

Ejemplo: "python web library" debe match "FastAPI framework".

# Keywords: ❌ No match
corpus = ["FastAPI is a web framework"]
query = "python web library"
# BM25 score: 0.0 (no overlap de términos)

# Semantic: ✅ Match
query_embedding = create_embedding("python web library")
doc_embedding = create_embedding("FastAPI is a web framework")
similarity = cosine_similarity(query_embedding, doc_embedding)
# Similarity: 0.85 (alto match por contexto)

Decisión: RAG gana. Entiende que "library" ≈ "framework", "python web" ≈ "FastAPI".


Caso 3: Large Unstructured Dataset (>10,000 docs)

Ejemplo: Buscar en 100,000 documentos de Wikipedia.

# Keywords (grep): ❌ No escala bien
grep -i "machine learning" wikipedia/*.txt
# Latency: 30 segundos (busca en 100K files)
# Output: 10,000+ matches sin ranking

# Semantic (RAG): ✅ Escala + ranking
results = collection.query(query_embeddings=[query_embedding], n_results=10)
# Latency: 200ms (vector search es rápido)
# Output: Top-10 documentos más relevantes (ranked)

Decisión: RAG gana. Vector search escala mejor que grep para large datasets.


Caso 4: Question Answering

Ejemplo: "¿Cuál es la capital de Francia?"

# Keywords: ❌ No responde pregunta
grep -i "capital france" docs/*.txt
# Output: Fragmentos de texto con "capital" y "france"
# Usuario tiene que leer y encontrar respuesta

# Semantic RAG: ✅ Responde directamente
query = "What is the capital of France?"
docs = retrieve_relevant_docs(query)  # Retrieval
answer = llm.invoke(context=docs, query=query)  # Generation
# Output: "The capital of France is Paris."

Decisión: RAG gana. LLM genera respuesta directa vs solo retrieval.


🔀 Casos Híbridos (Keywords + Semantic)

Caso: Búsqueda en Documentación Técnica

Problema: Queries contienen nombres exactos (API endpoints, function names) pero también conceptos.

Ejemplo: "How to use FastAPI OAuth2PasswordBearer class?"


Approach 1: Keywords solo (BM25)

# BM25 busca términos exactos
query = "FastAPI OAuth2PasswordBearer class"

# Pros:
# - Match exacto de "OAuth2PasswordBearer" (nombre de clase)

# Cons:
# - Miss documentos que explican OAuth2 sin mencionar "OAuth2PasswordBearer"
# - No entiende que "how to use" implica buscar ejemplos/tutorials

Approach 2: Semantic solo (Embeddings)

# Embeddings buscan por contexto
query_embedding = create_embedding("How to use FastAPI OAuth2PasswordBearer class")

# Pros:
# - Encuentra ejemplos de OAuth2 aunque no mencionen "OAuth2PasswordBearer" exacto
# - Entiende "how to use" → prioriza tutorials

# Cons:
# - Puede confundir "OAuth2PasswordBearer" con "OAuth2AuthorizationCodeBearer" (similar)
# - Miss exact match cuando usuario busca clase específica

Approach 3: Hybrid (BM25 + Embeddings) ✅

from rank_bm25 import BM25Okapi

# 1. BM25 keyword search (top-50)
bm25_scores = bm25.get_scores(query.split())
bm25_top_50 = get_top_k(bm25_scores, k=50)

# 2. Semantic search (top-50)
semantic_results = collection.query(query_embeddings=[query_embedding], n_results=50)
semantic_top_50 = semantic_results['ids'][0]

# 3. Reciprocal Rank Fusion (merge)
def reciprocal_rank_fusion(bm25_results, semantic_results, k=60):
    """Merge BM25 + Semantic con RRF"""
    
    scores = {}
    
    # BM25 scores
    for rank, doc_id in enumerate(bm25_results, 1):
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
    
    # Semantic scores
    for rank, doc_id in enumerate(semantic_results, 1):
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
    
    # Sort by combined score
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

merged = reciprocal_rank_fusion(bm25_top_50, semantic_top_50)
top_10 = [doc_id for doc_id, score in merged[:10]]

Resultado híbrido:

✅ Match exacto de "OAuth2PasswordBearer" (de BM25)
✅ Encuentra ejemplos relacionados aunque no mencionen clase exacta (de Semantic)
✅ Mejor recall y precision que ambos solos

Performance:

ApproachPrecision@10Recall@50Latency
BM25 solo75%60%50ms
Semantic solo78%70%200ms
Hybrid (RRF)85%80%250ms

Decisión: Hybrid gana. +10% precision, +20% recall, solo +50ms latency.

Técnica: Módulo 5 enseña hybrid search en detalle.


📊 Decision Matrix: ¿Keywords o RAG?

Factores de decisión:

def decide_search_method(
    dataset_size: int,
    data_type: str,  # "structured" or "unstructured"
    query_type: str,  # "exact_match", "conceptual", "mixed"
    budget: float,
    latency_target: int  # ms
):
    """
    Decision tree para seleccionar search method.
    """
    
    # Factor 1: Data type
    if data_type == "structured":
        return "keywords_sql"  # SQL siempre gana en structured
    
    # Factor 2: Query type
    if query_type == "exact_match":
        return "keywords_bm25"  # Exact match → keywords
    
    # Factor 3: Dataset size
    if dataset_size < 100:
        return "keywords_grep"  # Small dataset → grep suficiente
    
    # Factor 4: Budget
    if budget == 0:
        if latency_target < 500:
            return "keywords_bm25"  # Gratis + rápido
        else:
            return "hybrid"  # Local embeddings + BM25
    
    # Factor 5: Default para conceptual queries + large dataset
    if query_type == "conceptual":
        if latency_target < 300:
            return "hybrid"  # Balance velocidad/calidad
        else:
            return "rag_semantic"  # Calidad máxima
    
    # Factor 6: Mixed queries (technical docs)
    if query_type == "mixed":
        return "hybrid"  # Siempre hybrid para technical
    
    return "rag_semantic"  # Default

# Ejemplos
print(decide_search_method(50, "unstructured", "conceptual", 0, 1000))
# Output: "keywords_grep"

print(decide_search_method(100_000, "unstructured", "mixed", 500, 500))
# Output: "hybrid"

print(decide_search_method(10_000, "unstructured", "conceptual", 500, 2000))
# Output: "rag_semantic"

📋 Tabla Comparativa Final

AspectoKeywords (BM25)Semantic (RAG)Hybrid (BM25 + Semantic)
Exact match✅ Excelente❌ Puede confundir✅ Excelente
Conceptual queries❌ Miss sinónimos✅ Excelente✅ Excelente
Latency✅ 50ms⚠️ 200-500ms⚠️ 250ms
Costo✅ Cero⚠️ $0.0001/query⚠️ $0.0001/query
Setup✅ Simple⚠️ Complejo⚠️ Complejo
Small dataset (<100)✅ Suficiente❌ Overkill❌ Overkill
Large dataset (>10K)⚠️ Lento sin index✅ Escala bien✅ Escala bien
Structured data✅ Perfecto (SQL)❌ No sirve❌ No sirve
Technical docs⚠️ Miss context⚠️ Miss exact match✅ Best of both

🎯 Resumen

Conceptos clave:

  • Keywords (BM25): Rápido, gratis, exact match → Perfecto para IDs, structured data, small datasets
  • Semantic (RAG): Context-aware, sinónimos, conceptual → Perfecto para Q&A, large datasets, unstructured
  • Hybrid (BM25 + Semantic): Best of both → Perfecto para technical docs, mixed queries
  • Decision factors: Dataset size, data type, query type, budget, latency target
  • Casos donde keywords gana: Exact match, structured, small dataset, code search
  • Casos donde RAG gana: Conceptual, sinónimos, large dataset, question answering
  • Casos donde hybrid gana: Technical docs, API reference, mixed queries

Decisión típica:

if query_has_exact_ids or data_is_structured:
    use_keywords()  # SQL, BM25, grep
elif dataset_size < 100:
    use_keywords()  # Grep suficiente
elif query_is_conceptual and budget_available:
    use_rag_semantic()  # Embeddings
elif query_is_mixed or technical_docs:
    use_hybrid()  # BM25 + Semantic (Módulo 5)
else:
    use_keywords()  # Default simple

Qué sigue:

Cápsula 07 te muestra la arquitectura completa de módulos 2-8: cómo chunking (M2), query optimization (M3), re-ranking (M4), hybrid search (M5), metadata filtering (M6), vector DBs (M7), y evaluation (M8) se conectan para construir RAG production-ready.


📚 Recursos Adicionales

  1. BM25 Explained - Algoritmo BM25 en detalle
  2. Keyword vs Semantic Search - Comparación técnica
  3. Hybrid Search Best Practices - Elasticsearch hybrid search
  4. Reciprocal Rank Fusion - Paper original de RRF
  5. When to Use SQL vs Vector DB - Decision guide
  6. RAG vs Traditional Search - LlamaIndex comparison

Creado: Febrero 6, 2026
Versión: 1.0