Hybrid search se describe a menudo como "BM25 más vectores", como si los dos fueran simplemente concatenados. La realidad es más interesante: BM25 y HNSW producen rankings que no son directamente comparables, y fusionarlos requiere un método que no dependa de la calibración de scores. Ese método es Reciprocal Rank Fusion (RRF).
El problema de comparación de scores
BM25 produce un score derivado de TF-IDF que puede ir de 0 a ~30 dependiendo de la estadística del corpus. HNSW cosine similarity produce un score en [-1, 1]. No podés sumarlos. No podés promediarlos. No podés normalizarlos de forma que sea justa entre queries, porque las distribuciones son query-dependientes y corpus-dependientes.
Reciprocal Rank Fusion
RRF esquiva el problema de calibración ignorando los scores por completo. Usa solo las posiciones de rank. La fórmula es simple: score = Σ 1/(k + rank), donde k es una constante tunable (defaulteamos a 60) y rank es la posición del documento en cada lista de resultados (1-indexed). Un documento que rankea #1 en BM25 y #3 en HNSW obtiene 1/61 + 1/63 ≈ 0.0323. Un documento que rankea #1 en ambos obtiene 1/61 + 1/61 ≈ 0.0328.
La elegancia de RRF es que no requiere normalización de scores, ni tuning de pesos entre sistemas, ni vocabulario compartido entre las dos funciones de ranking. Solo requiere que cada sistema produzca una lista rankeada. Por eso se ha convertido en la estrategia de fusión por defecto en sistemas de hybrid search en producción.
El query planner
En VantaDB, el query planner inspecciona cada search request. Si hay un vector y el namespace tiene un índice HNSW, el path vectorial corre. Si hay texto y el namespace tiene un índice BM25, el path léxico corre. Si están ambos, los dos corren en paralelo y RRF fusiona los resultados. Si solo está uno, el motor saltea el otro path completamente — nada de trabajo desperdiciado.
Prueba la búsqueda híbrida con una instalación
Instala el motor con pip install vantadb-py y corre tu primera query híbrida desde el quickstart en /docs. Dale una estrella al repo en GitHub y únete al Discord para mostrar lo que fusionaste — después lee SQLite for AI Agents: The Missing Memory Layer para las decisiones de storage detrás del planner.