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:
| Aspecto | Descripción | Ejemplo |
|---|---|---|
| Match type | Exact 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:
| Aspecto | Descripción | Ejemplo |
|---|---|---|
| Match type | Semantic 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:
| Approach | Precision@10 | Recall@50 | Latency |
|---|---|---|---|
| BM25 solo | 75% | 60% | 50ms |
| Semantic solo | 78% | 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
| Aspecto | Keywords (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
- BM25 Explained - Algoritmo BM25 en detalle
- Keyword vs Semantic Search - Comparación técnica
- Hybrid Search Best Practices - Elasticsearch hybrid search
- Reciprocal Rank Fusion - Paper original de RRF
- When to Use SQL vs Vector DB - Decision guide
- RAG vs Traditional Search - LlamaIndex comparison
Creado: Febrero 6, 2026
Versión: 1.0