English · Español
03 — Búsqueda híbrida y reranking¶
Los dense embeddings entienden semántica; BM25 entiende palabras exactas. Combinar ambos (hybrid search, fusión vía RRF) consistentemente bate a cualquiera por separado en consultas factuales cortas. El reranker es un segundo paso de precisión sobre los top-k candidatos. Tres niveles de coste: BM25 (barato) → dense (medio) → cross-encoder (caro). Cada uno aporta donde el anterior falla.
La debilidad de la recuperación densa¶
Los dense embeddings destacan en similitud semántica pero pueden rendir poco en:
- Términos raros o fuera de distribución. "JIT" o "RoPE" o un verbo específico de nuestra KB pueden no aparecer en la distribución de pretraining del modelo de embedding. El embedding se vuelve casi aleatorio.
- Consultas de coincidencia exacta. "¿Cuál es el past participle de
eat?" — el token literaleates una señal fuerte. La recuperación densa aún podría encontrarlo (modulo la preocupación de términos raros) pero también podría derivar a chunks de significado similar pero verbo diferente. - Consultas cortas con un único token informativo. Solo "eat" tiene mucha más señal que la consulta completa — pero los dense embeddings promedian todo.
BM25: el baseline léxico¶
BM25 (Best Match 25) es una función de ranking probabilística de los años 90. Conceptualmente: "los documentos con más ocurrencias de los términos de consulta rankean más alto, ponderados por cuán raros son esos términos en el corpus".
Fórmula:
Donde:
- t recorre los términos de consulta.
- tf_{t,d} = cuántas veces aparece el término t en el doc d.
- IDF(t) = log((N - df_t + 0.5) / (df_t + 0.5) + 1) donde df_t = número de docs que contienen t, N = total de docs.
- k_1 ≈ 1.2 controla la saturación de frecuencia del término.
- b ≈ 0.75 controla la normalización por longitud.
Sin entrenamiento. BM25 es estadístico, no aprendido. Ha sido el estándar durante más de 30 años y sigue siendo un baseline fuerte.
Tokenización para BM25¶
BM25 opera sobre tokens. La elección de tokenizador importa:
- Whitespace + lowercase: simple, va bien para inglés.
- Sub-word (BPE / WordPiece): más robusto ante morfología (
eat,eats,eatense vuelven el mismo token raíz si es subword-aware). - Stemming (Porter, Snowball): reduce
eatingyeatsaeat. Lossy pero útil para corpora cortos.
Para la Fase 29 usamos whitespace + lowercase + stemming Porter. La KB es lo bastante pequeña como para que el stemming marque una diferencia real para consultas relacionadas con verbos.
Stopwords¶
Palabras como "is", "the", "what" aparecen en casi cada consulta. Su tf alto e IDF bajo significan que contribuyen poco al ranking pero inflan las puntuaciones. BM25 gestiona esto automáticamente vía IDF (un IDF casi cero para "the" lo hace despreciable).
Para la Fase 29 no eliminamos stopwords explícitamente. El IDF lo gestiona.
Por qué gana el híbrido¶
La observación empírica a lo largo de muchos benchmarks de RAG: dense + sparse juntos > cualquiera por separado.
- Dense: captura sinónimos, paráfrasis, estructura semántica latente.
- Sparse (BM25): captura coincidencia exacta, términos raros, precisión léxica.
Los modos de fallo de dense y BM25 son en gran medida ortogonales: dense puede perder consultas de coincidencia exacta que BM25 captura; BM25 puede perder consultas parafraseadas que dense captura.
Para nuestra KB de reglas gramaticales: "past participle of eat" tiene tanto un ancla literal (eat) como un ancla semántica (el concepto de past participle). BM25 encuentra el literal; dense encuentra el semántico. Juntos cubren ambos.
Fusión: Reciprocal Rank Fusion (RRF)¶
La forma estándar de combinar dos sistemas de recuperación:
Donde k = 60 es una constante de suavizado (el valor canónico de Cormack et al., 2009).
- Cada sistema de recuperación produce un ranking; el doc
dtiene rangorank_i(d)en el sistemai. - Un doc que aparece alto en ambos sistemas obtiene una puntuación fusionada alta.
- Un doc que aparece alto en uno pero no en el otro obtiene una puntuación moderada.
- Un doc que no aparece en ninguno obtiene cero.
RRF no requiere calibración de puntuaciones entre los dos sistemas. Las puntuaciones de BM25 y las similitudes coseno están en escalas distintas — RRF funciona solo sobre rangos, sorteando este problema.
El resultado es un único top-k fusionado.
Otros métodos de fusión¶
- Suma ponderada de puntuaciones:
α · cos_sim + (1-α) · BM25_normalized. Requiere normalización de puntuación. Más difícil de afinar. - CombSUM, CombMNZ: métodos más viejos de fusión IR. Menos comunes.
- Fusión aprendida (un modelo neuronal pequeño sobre rangos/puntuaciones): más potente pero requiere datos de entrenamiento.
RRF es de lejos el más popular en RAG de producción 2024-2026. Lo usamos.
Cuándo NO usar híbrido¶
Si tu corpus tiene un vocabulario muy estrecho sobre el que el modelo de embedding no fue preentrenado (p. ej., jerga médica, código, nombres de productos de tu empresa), dense en solitario puede engañar. BM25 en solitario puede ser más fiable. En ese caso: BM25 puro, o hacer fine-tune al modelo de embedding sobre datos in-domain (fuera del alcance de la Fase 29).
A la inversa, si tu corpus está "bien cubierto" por el pretraining del modelo de embedding (p. ej., texto en inglés general), dense en solitario puede bastar. BM25 añade poco.
Para nuestra KB de reglas gramaticales: en algún punto intermedio. Los verbos son inglés común; el modelo de embedding se entrenó sobre inglés general. Pero las consultas específicas como "past participle of X" se benefician del ancla léxica de X. El híbrido gana por ~5pp de hit-rate@5 (el Lab 02 lo confirma).
Reranking con un cross-encoder¶
Tras la recuperación (sea dense, BM25 o híbrida), tienes los top-k candidatos. El reranker los reescala con un cross-encoder:
Donde C es un cross-encoder. Para cada (q, d) en los top-k, ejecuta una pasada; ordena por las puntuaciones resultantes.
Tradeoff:
- Pro: Los cross-encoders atienden a través de
qyd. Ven interacciones a nivel de token. Mucho mayor precisión que los bi-encoders. - Contra: Una pasada por candidato. Para los top-100: 100 pasadas, ~5-10 segundos en CPU. Limita cuántos candidatos rerankeas.
Práctica estándar: el bi-encoder recupera los top-100 → cross-encoder rerankea → top-3 o top-5 se pasan al lector.
Elección de reranker¶
Para la Fase 29 usamos cross-encoder/ms-marco-MiniLM-L-6-v2 (60M params, entrenado sobre MS-MARCO, inglés). Rápido en CPU (~10 candidatos/seg).
Alternativas: rerankers BGE, API rerank de Cohere, entrenados a medida. Fuera del alcance.
Cuándo el reranker no ayuda¶
Si tu bi-encoder ya es muy preciso (recall@k alto y precision@1 alta), la ganancia marginal del reranker es pequeña. Mide primero.
Para nuestra KB de 50 docs con all-MiniLM-L6-v2, el recall@10 de recuperación es esencialmente 100% (cada doc gold está en los top-10 de 50). El reranker reordena los top-10. Si la reordenación mueve el doc gold hacia arriba depende de la calidad del cross-encoder.
El Lab 03 mide: el reranking mejora hit-rate@1 en ~3pp; hit-rate@5 sin cambios (ya que el gold ya estaba en top-5).
Juntándolo: el stack de recuperación de la Fase 29¶
# 1. Tokenize query (for BM25)
q_tokens = tokenize(query)
# 2. Embed query (for dense)
q_emb = encoder.encode([query]) # (1, 384)
q_emb = normalize(q_emb)
# 3. Retrieve top-10 dense
dense_results = faiss_index.search(q_emb, k=10) # [(doc_id, score), ...]
# 4. Retrieve top-10 BM25
bm25_results = bm25.search(q_tokens, k=10) # [(doc_id, score), ...]
# 5. RRF fuse → top-10 fused
fused = rrf_fuse(dense_results, bm25_results, k=60)
# 6. Rerank top-10 with cross-encoder → top-3
candidates = [(q, doc_text(d)) for d in fused[:10]]
rerank_scores = cross_encoder.predict(candidates)
top3 = sorted(zip(fused[:10], rerank_scores), key=lambda x: -x[1])[:3]
# 7. Pass top-3 to reader as context
Cada paso es testeable de forma independiente.
Presupuesto de latencia¶
Para un objetivo de <500ms end-to-end en CPU:
- Embed query: ~50ms.
- Búsqueda FAISS-flat (50 docs): ~1ms.
- Búsqueda BM25 (50 docs): ~5ms.
- RRF fuse: <1ms.
- Rerank cross-encoder (10 candidatos): ~200ms.
- Reader (MiniGPT, salida de 50 tokens): ~100ms con KV-cache.
- Total: ~360ms. Encaja.
Para KBs más grandes, FAISS-HNSW mantiene la recuperación sub-lineal; el cuello de botella se desplaza al lector.
Problemas de drill¶
Soluciones al abrir la fase en solutions/03-hybrid-and-reranking-ref.md.
- Calcula BM25 a mano para un corpus de 3 docs y una consulta de 2 términos. (Elige un corpus concreto.)
- Dos sistemas de recuperación rankean el doc
Den la posición 1. ¿Puntuación RRF deD? ¿En las posiciones 1 y 10? ¿En posición 1 y "no en top-10"? - El coste forward del cross-encoder es
O(L²)en la longitud de entradaL = |q| + |d|. Para 100 candidatos conL = 64, ¿cuál es el cómputo total? ¿Cómo escala siL = 256? - Si el bi-encoder ya recupera el doc gold en la posición 1, ¿ayuda el reranker cross-encoder? Construye un escenario donde sí (gold no en posición 1 inicialmente).
- Argumenta: en un dominio donde el modelo de embedding no tiene pretraining in-domain (p. ej., jerga propia), ¿preferirías (a) híbrido, (b) BM25-solo, © dense con fine-tune?
Resumen en un párrafo¶
La recuperación densa captura la similitud semántica; BM25 captura la coincidencia léxica. El híbrido los combina vía Reciprocal Rank Fusion — promediando rangos entre sistemas — para batir consistentemente a cualquiera por separado. El reranking con un cross-encoder es un paso de precisión sobre los top-k candidatos; es caro por consulta pero acotado. El stack estándar de producción: BM25 + dense → RRF top-10 → cross-encoder rerank top-3 → lector. Para nuestra pequeña KB de reglas gramaticales, híbrido + reranker es sobredimensionado pero pedagógicamente informativo — queremos que Borja sienta la contribución de cada componente. El Lab 02 los ablaciona; se espera que el híbrido gane por ~5pp y que el reranker añada otros ~3pp en hit-rate@1.
Siguiente: theory/04-evaluation.md.