Phase 05 - Lesson 14
Recuperación de Información y Búsqueda
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
BM25 es preciso pero frágil. El denso lanza una red amplia pero pierde palabras clave. El híbrido es el estándar de 2026. Todo lo demás es ajuste fino.
Tipo: Build Lenguajes: Python Prerrequisitos: Fase 5 · 02 (BoW + TF-IDF), Fase 5 · 04 (GloVe, FastText, Subword) Tiempo: ~75 minutos
El Problema
El usuario escribe "qué pasa si alguien miente para conseguir dinero" y espera encontrar el artículo de ley que realmente lo cubre: "Sección 420 IPC". Una búsqueda por palabra clave falla por completo (no hay vocabulario en común). Una búsqueda semántica falla si los embeddings no fueron entrenados con texto jurídico. La búsqueda real tiene que manejar ambos casos.
La RI es el pipeline detrás de todo sistema RAG, toda barra de búsqueda, toda búsqueda aproximada de un sitio de documentación. La arquitectura de 2026 que funciona en producción no es un único método. Es una cadena de métodos complementarios, cada uno captando las fallas del anterior.
Esta lección construye cada pieza y nombra qué fallas capta cada una.
El Concepto
Cuatro capas. Elige las que necesites.
- Recuperación dispersa (BM25). Rápida, precisa en coincidencias exactas, pésima en semántica. Corre sobre un índice invertido. Menos de 10ms por consulta en millones de documentos. Acierta referencias de leyes, códigos de productos, mensajes de error y entidades nombradas.
- Recuperación densa. Codifica consulta y documentos en vectores. Búsqueda por vecino más cercano. Captura paráfrasis y similitud semántica. Pierde coincidencias exactas de palabra clave que difieren por un solo carácter. 50-200ms por consulta con FAISS o una base de datos vectorial.
- Fusión. Combina las listas ordenadas de la recuperación dispersa y densa. La Reciprocal Rank Fusion (RRF) es el estándar fácil porque ignora los puntajes crudos (que viven en escalas diferentes) y usa solo las posiciones de ranking. La fusión ponderada es una opción cuando sabes que una señal domina en tu dominio.
- Reordenamiento con cross-encoder. Toma el top-30 de la fusión. Corre un cross-encoder (consulta + documento juntos, puntuando cada par). Conserva el top-5. Los cross-encoders son más lentos por par que los bi-encoders, pero mucho más precisos. Amortizas ese costo corriéndolos solo sobre el top-30.
La recuperación en tres vías (BM25 + denso + disperso aprendido como SPLADE) supera a la de dos vías en los benchmarks de 2026, pero necesita infraestructura para índices dispersos aprendidos. Para la mayoría de los equipos, dos vías más reordenamiento con cross-encoder es el punto justo.
Constrúyelo
Paso 1: BM25 desde cero
import math
import re
from collections import Counter
TOKEN_RE = re.compile(r"[a-z0-9]+")
def tokenize(text):
return TOKEN_RE.findall(text.lower())
class BM25:
def __init__(self, corpus, k1=1.5, b=0.75):
if not corpus:
raise ValueError("corpus must not be empty")
self.corpus = [tokenize(d) for d in corpus]
self.k1 = k1
self.b = b
self.n_docs = len(self.corpus)
self.avg_dl = sum(len(d) for d in self.corpus) / self.n_docs
self.df = Counter()
for doc in self.corpus:
for term in set(doc):
self.df[term] += 1
def idf(self, term):
n = self.df.get(term, 0)
return math.log(1 + (self.n_docs - n + 0.5) / (n + 0.5))
def score(self, query, doc_idx):
q_tokens = tokenize(query)
doc = self.corpus[doc_idx]
dl = len(doc)
freq = Counter(doc)
score = 0.0
for term in q_tokens:
f = freq.get(term, 0)
if f == 0:
continue
numerator = f * (self.k1 + 1)
denominator = f + self.k1 * (1 - self.b + self.b * dl / self.avg_dl)
score += self.idf(term) * numerator / denominator
return score
def rank(self, query, top_k=10):
scored = [(self.score(query, i), i) for i in range(self.n_docs)]
scored.sort(reverse=True)
return scored[:top_k]
Dos parámetros que vale la pena conocer. k1=1.5 controla la saturación de la frecuencia de términos; mayor significa más peso en la repetición del término. b=0.75 controla la normalización por longitud; 0 ignora la longitud del documento, 1 normaliza por completo. Los valores por defecto son las recomendaciones de Robertson del artículo original y rara vez necesitan ajuste.
Paso 2: recuperación densa con un bi-encoder
from sentence_transformers import SentenceTransformer
import numpy as np
def build_dense_index(corpus, model_id="sentence-transformers/all-MiniLM-L6-v2"):
encoder = SentenceTransformer(model_id)
embeddings = encoder.encode(corpus, normalize_embeddings=True)
return encoder, embeddings
def dense_search(encoder, embeddings, query, top_k=10):
q_emb = encoder.encode([query], normalize_embeddings=True)
sims = (embeddings @ q_emb.T).flatten()
order = np.argsort(-sims)[:top_k]
return [(float(sims[i]), int(i)) for i in order]
Normaliza los embeddings con L2 para que el producto punto sea igual al coseno. all-MiniLM-L6-v2 tiene 384 dimensiones, es rápido y lo bastante fuerte para la mayoría de las recuperaciones en inglés. Para trabajo multilingüe, usa paraphrase-multilingual-MiniLM-L12-v2. Para máxima precisión, bge-large-en-v1.5 o e5-large-v2.
Paso 3: Reciprocal Rank Fusion
def reciprocal_rank_fusion(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, (_, doc_idx) in enumerate(ranking):
scores[doc_idx] = scores.get(doc_idx, 0.0) + 1.0 / (k + rank + 1)
fused = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [(score, doc_idx) for doc_idx, score in fused]
La constante k=60 proviene del artículo original de RRF. Un k mayor aplana la contribución de las diferencias de ranking; un k menor hace que los rankings de la cima dominen. 60 es el valor por defecto publicado y rara vez necesita ajuste.
Paso 4: búsqueda híbrida + reordenamiento
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
def hybrid_search(query, bm25, encoder, dense_embeddings, corpus, top_k=5, pool_size=30, reranker=reranker):
sparse_ranking = bm25.rank(query, top_k=pool_size)
dense_ranking = dense_search(encoder, dense_embeddings, query, top_k=pool_size)
fused = reciprocal_rank_fusion([sparse_ranking, dense_ranking])[:pool_size]
pairs = [(query, corpus[doc_idx]) for _, doc_idx in fused]
scores = reranker.predict(pairs)
reranked = sorted(zip(scores, [doc_idx for _, doc_idx in fused]), reverse=True)
return reranked[:top_k]
Tres etapas compuestas. BM25 encuentra coincidencias léxicas. El denso encuentra coincidencias semánticas. La RRF combina los dos rankings sin necesitar calibración de puntajes. El cross-encoder vuelve a puntuar el top-30 usando pares de consulta-documento juntos, lo que captura una relevancia de grano fino que el bi-encoder perdió. Conserva el top-5.
Paso 5: evaluación
| Métrica | Significado |
|---|---|
| Recall@k | De las consultas donde el documento correcto existe, ¿con qué frecuencia está en el top-k? |
| MRR (Mean Reciprocal Rank) | Promedio de 1/ranking del primer documento relevante. |
| nDCG@k | Considera gradaciones de relevancia, no solo relevante/no relevante binario. |
Para RAG específicamente, el Recall@k del recuperador es el número más importante. Tu lector no puede responder si el pasaje correcto no está en el conjunto recuperado.
Consejo de depuración: para las consultas que fallan, compara los rankings disperso y denso. Si uno encuentra el documento correcto y el otro no, tienes una incompatibilidad de vocabulario (solución: agrega la mitad que falta) o una ambigüedad semántica (solución: mejores embeddings o un reordenador).
Úsalo
El stack de 2026:
| Escala | Stack |
|---|---|
| 1k-100k docs | BM25 en memoria + embeddings all-MiniLM-L6-v2 + RRF. Sin base de datos aparte. |
| 100k-10M docs | FAISS o pgvector para el denso + Elasticsearch / OpenSearch para BM25. Corre en paralelo. |
| 10M+ docs | Qdrant / Weaviate / Vespa / Milvus con soporte híbrido. Reordenamiento con cross-encoder sobre el top-30. |
| Frontera de mejor calidad | Tres vías (BM25 + denso + SPLADE) + reordenamiento de interacción tardía con ColBERT |
Sea lo que sea que elijas, reserva presupuesto para evaluación. Haz benchmark del recall de la recuperación antes de hacer benchmark de la precisión de RAG de extremo a extremo. Un lector no puede arreglar lo que el recuperador perdió.
Las lecciones arduamente ganadas del RAG en producción en 2026
- El 80% de las fallas de RAG se remontan a la ingesta y al chunking, no al modelo. Los equipos pasan semanas cambiando LLMs y ajustando prompts mientras la recuperación silenciosamente devuelve el contexto equivocado cada tercera consulta. Arregla el chunking primero.
- La estrategia de chunking importa más que el tamaño del chunk. Las divisiones de tamaño fijo rompen tablas, código y encabezados anidados. La división por oración es el estándar; el chunking semántico o basado en LLM rinde para documentos técnicos y manuales de producto.
- Patrón parent-doc. Recupera chunks "hijos" pequeños para precisión. Cuando aparecen múltiples hijos de la misma sección padre, sustituye por el bloque padre para preservar el contexto. Esto eleva de forma consistente la calidad de la respuesta sin reentrenamiento.
- k_rerank=3 suele ser óptimo. Cada chunk extra más allá de eso agrega costo de tokens y latencia de generación sin elevar la calidad de la respuesta. Si k=8 sigue siendo mejor que k=3 para ti, el reordenador está rindiendo por debajo de lo esperado.
- HyDE / expansión de consulta. Genera una respuesta hipotética a partir de la consulta, haz el embedding de eso y recupera. Tiende un puente sobre la brecha de formulación entre preguntas cortas y documentos largos. Ganancia de precisión gratis, sin entrenamiento.
- Presupuesto de contexto por debajo de 8K tokens. Alcanzar ese límite de forma consistente significa que el umbral del reordenador está demasiado holgado.
- Versiona todo. Prompts, reglas de chunking, modelo de embedding, reordenador. Cualquier deriva rompe silenciosamente la calidad de la respuesta. Los gates de CI sobre fidelidad, precisión de contexto y tasa de preguntas sin responder bloquean regresiones antes de que los usuarios las vean.
- La recuperación en tres vías (BM25 + denso + disperso aprendido como SPLADE) supera a la de dos vías en los benchmarks de 2026, especialmente para consultas que mezclan nombres propios con semántica. Ponla en producción cuando la infraestructura dé soporte a índices SPLADE.
Un diseño de recuperación adecuado reduce las alucinaciones en un 70-90% según mediciones de la industria en 2026. La mayoría de las ganancias de rendimiento en RAG provienen de una mejor recuperación, no del fine-tuning del modelo.
Entrégalo
Guarda como outputs/skill-retrieval-picker.md:
---
name: retrieval-picker
description: Pick a retrieval stack for a given corpus and query pattern.
version: 1.0.0
phase: 5
lesson: 14
tags: [nlp, retrieval, rag, search]
---
Given requirements (corpus size, query pattern, latency budget, quality bar, infra constraints), output:
1. Stack. BM25 only, dense only, hybrid (BM25 + dense + RRF), hybrid + cross-encoder rerank, or three-way (BM25 + dense + learned-sparse).
2. Dense encoder. Name the specific model. Match to language(s), domain, and context length.
3. Reranker. Name the specific cross-encoder model if used. Flag that rerank adds 30-100ms latency on top-30.
4. Evaluation plan. Recall@10 is the primary retriever metric. MRR for multi-answer. Baseline first, incremental improvements measured against it.
Refuse to recommend dense-only for corpora with named entities, error codes, or product SKUs unless the user has evidence dense handles exact matches. Refuse to skip reranking for high-stakes retrieval (legal, medical) where the final top-5 decides the user's answer.
Ejercicios
- Fácil. Implementa el
hybrid_searchde arriba en un corpus de 500 documentos. Prueba 20 consultas. Compara el recall en 5 entre BM25 solo, denso solo e híbrido. - Medio. Agrega el cálculo de MRR. Para cada consulta de prueba con un documento correcto conocido, encuentra el ranking del documento correcto en los rankings BM25, denso e híbrido. Reporta el MRR de cada uno.
- Difícil. Haz fine-tuning de un encoder denso en tu dominio usando MultipleNegativesRankingLoss (Sentence Transformers). Construye un conjunto de entrenamiento a partir de 500 pares consulta-documento. Compara el recall antes y después del fine-tuning.
Términos Clave
| Término | Lo que la gente dice | Lo que realmente significa |
|---|---|---|
| BM25 | Búsqueda por palabra clave | Okapi BM25. Puntúa documentos por frecuencia de términos, IDF y longitud. |
| Recuperación densa | Búsqueda vectorial | Codifica consulta + documento en vectores, encuentra vecinos más cercanos. |
| Bi-encoder | Modelo de embedding | Codifica consulta y documento de forma independiente. Rápido en el momento de la consulta. |
| Cross-encoder | Modelo reordenador | Codifica consulta + documento juntos. Lento pero preciso. |
| RRF | Fusión de rankings | Combina dos rankings sumando 1/(k + rank). |
| Recall@k | Métrica de recuperación | Fracción de consultas en las que un documento relevante está en el top-k. |
Lectura Adicional
- Robertson and Zaragoza (2009). The Probabilistic Relevance Framework: BM25 and Beyond — el tratamiento definitivo de BM25.
- Karpukhin et al. (2020). Dense Passage Retrieval for Open-Domain QA — DPR, el bi-encoder canónico.
- Formal et al. (2021). SPLADE: Sparse Lexical and Expansion Model — el recuperador disperso aprendido que cierra la brecha con el denso.
- Cormack, Clarke, Büttcher (2009). Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods — artículo de RRF.
- Khattab and Zaharia (2020). ColBERT: Efficient and Effective Passage Search — recuperación de interacción tardía.