¿Por Qué Combinar Recuperadores?
A estas alturas tenemos dos enfoques de recuperación que son buenos en cosas muy diferentes. La recuperación dispersa (BM25, SPLADE) destaca en el emparejamiento exacto de palabras clave y términos técnicos raros, mientras que la recuperación densa (bi-encoders) destaca en el manejo de paráfrasis semánticas y sinónimos. Ninguno es estrictamente mejor, así que ¿por qué no usar ambos?
Una observación empírica lo confirma. Cuando calculamos la superposición entre los 10 mejores resultados de BM25 y los 10 mejores de un bi-encoder para la misma consulta, la superposición tiende a ser moderada (aproximadamente 40-60% en benchmarks estándar de IR, aunque la cifra exacta varía considerablemente según el conjunto de datos y el dominio). Cada sistema encuentra documentos relevantes que el otro pierde, y un sistema combinado puede capturar ambos conjuntos.
El desafío es descubrir cómo combinar dos listas ordenadas que provienen de funciones de puntuación diferentes con escalas diferentes. Las puntuaciones de BM25 son números reales derivados de log-probabilidades, mientras que las puntuaciones densas son similitudes coseno en [-1, 1]. No podemos simplemente sumarlas porque una puntuación BM25 de 12 y una puntuación coseno de 0.8 no son comparables. Entonces, ¿cómo fusionamos estas listas de manera justa?
Fusión de Rango Recíproco
Reciprocal Rank Fusion (Cormack et al., 2009) (RRF) evita por completo el problema de calibración de puntuaciones al trabajar solo con rangos, no con puntuaciones. Para un documento $d$ que aparece en el rango $r$ en la lista ordenada $\ell$, su contribución RRF es $\frac{1}{k + r}$. Sumada a través de todas las listas:
La constante $k$ (típicamente 60) actúa como un piso que evita que un solo resultado en primera posición domine la puntuación fusionada. Incluso el documento mejor clasificado de un sistema contribuye como máximo $\frac{1}{61}$, y los documentos que no están en una lista particular se les asigna $r = \infty$, contribuyendo 0 desde esa lista.
Para ver por qué esta ponderación importa, consideremos dos documentos. El documento A está clasificado 2.º por BM25 y 3.º por el recuperador denso, mientras que el documento B está clasificado 1.º por BM25 pero solo 50.º por el recuperador denso. Sin fusión podríamos preferir B (tiene un resultado de rango 1), pero RRF cuenta una historia diferente. El documento A puntúa $\frac{1}{62} + \frac{1}{63} \approx 0.0320$, mientras que el documento B puntúa $\frac{1}{61} + \frac{1}{110} \approx 0.0255$. El documento en el que ambos sistemas coinciden gana, aunque ningún sistema lo clasificó primero.
RRF también tiende a ser robusto a la elección de sistemas de recuperación e hiperparámetros. El artículo original mostró que $k=60$ funcionó bien en muchos benchmarks de recuperación sin ajuste, y trabajo más reciente confirma que frecuentemente supera a la combinación lineal ponderada de puntuaciones normalizadas porque la normalización de puntuaciones es altamente sensible a la distribución de puntuaciones de cada sistema de recuperación.
La siguiente implementación muestra RRF en acción sobre dos listas de recuperación simuladas con superposición parcial, junto con un gráfico que desglosa la contribución de cada documento desde cada sistema.
import math, json
import js
def rrf_fuse(ranked_lists, k=60):
"""
ranked_lists: list of lists of doc_ids, ordered by relevance (best first)
Returns: sorted list of (doc_id, rrf_score) tuples
"""
scores = {}
for lst in ranked_lists:
for rank, doc_id in enumerate(lst, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(scores.items(), key=lambda x: -x[1])
# Simulate two retrieval systems with partial overlap
# Docs 1-5 are relevant (suppose); systems disagree on order
bm25_results = ["doc3", "doc7", "doc1", "doc9", "doc5", "doc2", "doc11", "doc4", "doc8", "doc6"]
dense_results = ["doc1", "doc5", "doc3", "doc12", "doc2", "doc8", "doc6", "doc10", "doc4", "doc7"]
fused = rrf_fuse([bm25_results, dense_results], k=60)
fused_docs = [d for d, _ in fused[:10]]
fused_scores = [round(s, 4) for _, s in fused[:10]]
# Compute rank for each doc in BM25 and dense (0 if not in top-10)
def get_rank(lst, doc):
return lst.index(doc) + 1 if doc in lst else None
all_docs_in_fused = fused_docs[:8]
bm25_ranks = [get_rank(bm25_results, d) or 0 for d in all_docs_in_fused]
dense_ranks = [get_rank(dense_results, d) or 0 for d in all_docs_in_fused]
rrf_scores = [round(1/(60+r) if r > 0 else 0, 4) for r in bm25_ranks]
# We'll plot the RRF scores of top-8 fused docs
plot_data = [
{
"title": "RRF Scores for Top-8 Fused Results",
"x_label": "Document",
"y_label": "RRF Score",
"x_data": all_docs_in_fused,
"lines": [
{"label": "RRF Score", "data": [round(s, 4) for s in fused_scores[:8]], "color": "#10b981"},
{"label": "BM25 contribution", "data": [round(1/(60+r), 4) if r > 0 else 0 for r in bm25_ranks], "color": "#3b82f6"},
{"label": "Dense contribution", "data": [round(1/(60+r), 4) if r > 0 else 0 for r in dense_ranks], "color": "#f59e0b"},
]
}
]
js.window.py_plot_data = json.dumps(plot_data)
Enfoques de Normalización de Puntuaciones
Si RRF descarta las puntuaciones reales, una pregunta natural es si podríamos en su lugar normalizar ambas distribuciones de puntuaciones a la misma escala y sumarlas. Ese era el enfoque estándar antes de RRF, y vale la pena entender por qué resultó ser frágil. Las dos normalizaciones más comunes son min-max y z-score.
- Min-max reescala cada puntuación como $s' = \frac{s - s_{\min}}{s_{\max} - s_{\min}}$. Es simple pero sensible a valores atípicos (una sola puntuación BM25 muy alta comprime todas las demás puntuaciones cerca de cero).
- Z-score centra y escala por desviación estándar, $s' = \frac{s - \mu}{\sigma}$. Es más robusto a valores atípicos, pero las puntuaciones resultantes pueden ser negativas y usualmente necesitan una transformación sigmoide para mapear de vuelta a [0, 1].
El problema fundamental con ambos enfoques es que la normalización se calcula sobre los candidatos devueltos, que cambian de consulta en consulta. Una puntuación BM25 de 12 podría ser baja en un contexto de consulta y alta en otro, por lo que la fusión basada en puntuaciones requiere que las puntuaciones estén calibradas (la misma puntuación absoluta debería representar el mismo nivel de relevancia entre consultas). Lograr ese tipo de calibración requiere un entrenamiento cuidadoso del modelo y rara vez está garantizado en la práctica.
RRF evita todo esto porque los rangos están inherentemente calibrados. El rango 1 siempre significa "mejor resultado de este recuperador para esta consulta", independientemente de la puntuación bruta detrás.
Recuperación Híbrida en Producción
La mayoría de las bases de datos vectoriales (Weaviate, Qdrant, Milvus, entre otras) ahora soportan búsqueda híbrida como característica integrada. Una sola consulta activa tanto una búsqueda BM25 (o TF-IDF) como una búsqueda ANN (vecino más cercano aproximado), y los resultados se fusionan con RRF antes de ser devueltos al llamador. Elasticsearch y OpenSearch exponen una capacidad similar a través de un tipo de consulta `hybrid`, y Azure AI Search empaqueta recuperación BM25, recuperación vectorial y un reranker BERT opcional en una sola llamada API.
Para la mayoría de los sistemas RAG, el camino práctico tiende a verse así.
- Comenzar con BM25 solo como la línea base más simple posible.
- Agregar un recuperador denso cuando aparezcan fallos de paráfrasis semántica (usuarios buscando de forma diferente a como están escritos los documentos).
- Fusionar con RRF usando $k=60$, y solo ajustar $k$ si tenemos datos de evaluación etiquetados y el ajuste mejora una métrica medida (NDCG@10, Recall@10).
- Agregar un reranker sobre los top-K candidatos fusionados si la precisión en el primer resultado importa.
Quiz
Pon a prueba tu comprensión de la búsqueda híbrida y la fusión de rangos.
¿Por qué no se pueden simplemente sumar las puntuaciones de BM25 y de recuperación densa para combinar dos recuperadores?
En RRF con $k=60$, ¿qué logra la constante $k$?
El documento A está clasificado 1.º por BM25 y no lo recupera la búsqueda densa. El documento B está clasificado 5.º por ambos. Con RRF ($k=60$), ¿cuál puntúa más alto?
¿Cuál es la principal debilidad de la normalización min-max para la fusión basada en puntuaciones?
¿Cuándo vale la pena entrenar un modelo de fusión aprendida en lugar de usar RRF?