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

Recuperación híbrida: BM25 + denso + RRF + reordenamiento con cross-encoder

Cuatro capas. Elige las que necesites.

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. Fácil. Implementa el hybrid_search de arriba en un corpus de 500 documentos. Prueba 20 consultas. Compara el recall en 5 entre BM25 solo, denso solo e híbrido.
  2. 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.
  3. 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

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).