Avanzado 11 minOptimización

Búsqueda híbrida: BM25 + vectoriel

Respuesta directa

La búsqueda híbrida lanza en paralelo una búsqueda por palabras clave (BM25) y una búsqueda vectorial sobre la misma pregunta, luego fusiona las dos clasificaciones, normalmente mediante Reciprocal Rank Fusion (RRF). Recupera casos en los que el embedding falla (identificadores, nombres propios, términos raros) sin perder la comprensión de las paráfrasis. Qdrant, Weaviate y Elasticsearch la ofrecen nativamente; con ChromaDB, se construye en treinta líneas de Python.

Una base vectorial recupera el sentido, no las coincidencias literales: una referencia como «RG/2024-117» o un número de ticket se le escapa. En cambio, la búsqueda por palabras clave no entiende que «automóvil» y «coche» se refieren a la misma cosa. Esta guía muestra cómo combinar ambos tipos de búsqueda, qué método de fusión elegir, qué hacen Qdrant y Weaviate, y una trampa de tokenización en francés que arruina silenciosamente la parte BM25.

Por Mohamed Meguedmi·Actualización 2026-09-30·Probado en Windows, macOS y Linux

#¿Por qué la búsqueda puramente vectorial falla en algunas preguntas?

La búsqueda vectorial transforma cada pasaje en un vector que resume su significado general y luego devuelve los pasajes cuyo vector está más cerca del de la pregunta. Esta compresión funciona muy bien para las paráfrasis y mal para todo lo que deba encontrarse literalmente: un identificador, un código de error, un nombre de persona, una sigla interna. Para el embedding, «PROD-4817» y «PROD-4871» se parecen, y un ticket sin relación puede aparecer por delante del correcto. La búsqueda híbrida responde a este defecto: añade una segunda clasificación basada en la presencia exacta de las palabras y fusiona ambas para que cada método compense los puntos ciegos del otro.

Identificadores y referencias
Números de ticket, de expediente, de contrato, SKU, códigos de error: son cadenas que un embedding no conserva de forma fiable y que BM25 encuentra en cuanto aparecen en el pasaje.
Vocabulario especializado poco frecuente
Términos médicos, jurídicos o técnicos poco comunes: el peso de una palabra rara es alto en BM25, mientras que su embedding puede ser vago.
Consultas muy cortas
Dos palabras como « factura 2024 » no aportan mucho para un embedding; las palabras clave, en cambio, se comparan tal como están.
Paráfrasis y reformulaciones
El caso inverso: «contrato de duración determinada» y «CDD» o «coche» y «automóvil» no comparten ninguna palabra; solo la búsqueda vectorial los acerca
i
Un caso típico
En una base de datos de tickets, la consulta «PROD-4817» con una búsqueda puramente vectorial devuelve tickets de contenido similar, mientras que la búsqueda por palabras clave identifica inmediatamente el único ticket con ese número.

#BM25: lo que hace la búsqueda por palabras clave

El kit RAG Local

Tus documentos, tu IA: un RAG local fiable sobre tus PDF, notas y correos — sin enviar nada a la nube.

  • Espacio en línea de por vida
  • PDF + archivos
  • Reembolsado 30 j

BM25 es la función de clasificación léxica que utilizan Lucene, Elasticsearch y OpenSearch, y que SQLite ofrece en su módulo FTS5 (la documentación describe su función bm25() como una función que devuelve un valor que indica el grado de coincidencia de la fila con la consulta). Se basa en tres ideas: una palabra poco frecuente pesa más que una palabra frecuente, una palabra repetida cuenta cada vez menos a partir de cierto número de apariciones y un pasaje largo se penaliza ligeramente frente a uno corto. El parámetro k1 regula esta saturación: según la descripción de Elastic, limita la influencia que un solo término de la consulta puede tener sobre la puntuación de un documento.

Fortalezas
Términos raros, identificadores, consultas cortas, vocabulario especializado, ningún modelo que cargar ni entrenar, índice compacto, resultado explicable (se sabe qué palabra hizo que el pasaje apareciera entre los resultados).
Debilidades
Sinónimos, reformulaciones, paráfrasis, errores de tecleo; sin lematización, «firmado» y «firma» son dos palabras diferentes.
Requisitos previos
Una tokenización cuidadosa: minúsculas, eliminación de acentos y, si procede, reducción a la raíz. Es el punto en el que la mayoría de las implementaciones propias se equivocan; ver más abajo.

#Búsqueda híbrida: el principio

Se ejecutan las dos búsquedas sobre la misma pregunta; cada una devuelve su lista de candidatos (20 a 50 pasajes) y luego se fusionan ambas listas en una única clasificación. La mejora se basa en una observación sencilla: los pasajes que aparecen en ambas listas son casi siempre pertinentes y, además, cada método recupera pasajes que el otro ha pasado por alto. Weaviate define la búsqueda híbrida en estos términos: combina los resultados de una búsqueda vectorial y una búsqueda por palabras clave fusionando ambos conjuntos de resultados, con un método de fusión y pesos relativos configurables. La mejora cuantificada depende del corpus y de las preguntas: ninguna cifra general es fiable y hay que medirla con tus propios documentos (véase más abajo).

#Fusionar sin normalizar: Fusión de Rango Recíproco

El error clásico consiste en sumar los puntajes brutos. Un puntaje BM25 es un número positivo sin límite; un puntaje de similitud vectorial es una distancia o un coseno, dentro de un rango limitado: las dos escalas son incomparables, y el menor cambio de corpus las desplaza. La Fusión de Rango Recíproco soluciona el problema utilizando solo los rangos. La documentación de Elasticsearch la presenta como un método que no requiere ajustes y cuyos indicadores de relevancia no necesitan estar relacionados entre sí.

Fórmula RRF
score(doc) = somme, sur chaque liste i où le doc apparaît, de 1 / (k + rang_i(doc))

k = 60 par défaut (valeur par défaut d'Elasticsearch)
rang_i = 1 pour le premier de la liste, 2 pour le deuxième, etc.
Un doc absent d'une liste n'ajoute rien pour cette liste.

Un fragmento clasificado en primer lugar por BM25 y en quinto por la búsqueda vectorial obtiene 1/61 + 1/65, es decir, aproximadamente 0,0318; un fragmento clasificado en decimoquinto lugar en ambas listas obtiene 2/75, es decir, aproximadamente 0,0267. La constante k atenúa la ventaja del primer puesto: cuanto mayor es, más cuentan los puestos más alejados. Elasticsearch documenta esta constante con el nombre rank_constant, con un valor predeterminado de 60, y un tamaño de ventana (rank_window_size) que fija la longitud de cada lista antes de la fusión. Una ventana más grande mejora la relevancia a costa del rendimiento, precisa la documentación.

#¿Y la fusión por puntuaciones?

Algunos motores ofrecen una alternativa: normalizar las puntuaciones de cada lista y luego combinarlas aplicando un peso. Weaviate documenta ambos métodos: una clasificación por posiciones y una fusión por puntuaciones relativas. Esta última es el método predeterminado desde la versión 1.24 y es necesaria para usar autocut con el operador híbrido. Qdrant ofrece RRF y DBSF; esta última conserva las puntuaciones brutas, pero normaliza su distribución (media y desviación estándar) antes de combinarlas. La fusión por posiciones es más robusta cuando no se conoce la distribución de las puntuaciones; la fusión por puntuaciones permite un ajuste más fino cuando se pueden realizar mediciones.

#Qué herramienta elegir: motores con búsqueda híbrida nativa

Búsqueda híbrida según la herramienta (documentación oficial, septiembre de 2026)
HerramientaHíbrido nativoFusiónAjuste del peso
QdrantSí, mediante la API Query (disponible desde la versión 1.10)RRF o DBSFPeso por solicitud y constante k ajustables en las versiones recientes
WeaviateSí, operador hybridPosiciones o puntuaciones relativas (opción predeterminada desde la versión 1.24)Parámetro alpha: 1 = búsqueda exclusivamente vectorial, 0 = búsqueda exclusivamente por palabras clave
ElasticsearchSí, recuperador rrfRRFrank_constant (60 por defecto) y rank_window_size
SQLite FTS5 + extensión vectorialPor ensamblarPor escribirA tu elección
ChromaDB + rank_bm25Para montar en PythonPor escribir (RRF en 6 líneas)A tu elección

La diferencia de fondo no es la fusión en sí, que cabe en unas pocas líneas, sino el índice: un motor nativo mantiene ambos índices actualizados conjuntamente, mientras que la solución casera mantiene el índice BM25 en memoria y debe reconstruirlo cada vez que se añade un documento. Para un corpus de unos pocos miles de pasajes que cambia rara vez, la solución casera es más que suficiente. Para corpus más grandes, o en cuanto los documentos cambian a diario, un motor nativo evita las incoherencias entre los dos índices. La guía sobre Weaviate presenta esta herramienta en detalle.

#Implementación propia: ChromaDB, rank_bm25 y RRF

El código siguiente ensambla las tres piezas. Corrige un defecto frecuente en los ejemplos: la función de normalización debe eliminar los signos diacríticos antes de filtrar; de lo contrario, cada letra acentuada corta una palabra en dos.

Híbrido BM25 + ChromaDB con RRF
import re, unicodedata
import chromadb
from rank_bm25 import BM25Okapi

def normalize(txt):
    txt = unicodedata.normalize("NFKD", txt.lower())
    txt = "".join(c for c in txt if not unicodedata.combining(c))  # retire les accents
    return re.findall(r"[a-z0-9]+", txt)

# Indexation BM25 (en mémoire) : l'indice de la liste = l'identifiant du passage
all_docs = [d["text"] for d in load_docs()]
bm25 = BM25Okapi([normalize(d) for d in all_docs])

# ChromaDB : les ids doivent être les mêmes, sous forme de chaînes "0", "1", ...
coll = chromadb.PersistentClient("./chroma_db").get_collection("docs")

def rrf_fuse(rankings, weights=None, k=60):
    weights = weights or [1.0] * len(rankings)
    scores = {}
    for ranking, w in zip(rankings, weights):
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] = scores.get(doc_id, 0) + w / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)

def hybrid_search(question, top_k=5, n_candidates=20):
    s = bm25.get_scores(normalize(question))
    bm25_top = sorted(range(len(all_docs)), key=lambda i: -s[i])[:n_candidates]
    bm25_ranking = [str(i) for i in bm25_top]
    vec_ranking = coll.query(query_texts=[question], n_results=n_candidates)["ids"][0]
    fused = rrf_fuse([bm25_ranking, vec_ranking])
    return [all_docs[int(i)] for i in fused[:top_k]]
!
Una trampa de la tokenización en francés
Con la normalización habitual «NFKD y luego reemplazar todo lo que no sea a-z o un dígito por un espacio», «référence» se convierte en «re fe rence»: cada acento deja un signo combinante que se reemplaza por un espacio. La parte BM25 sigue funcionando con los identificadores, pero no detecta ninguna de las palabras acentuadas. Prueba normalize con tres frases antes de indexar.

Dos precauciones prácticas. Primero, los identificadores deben ser idénticos en ambos índices: aquí, el índice de la lista, convertido en cadena, sirve de identificador de Chroma. Segundo, el índice BM25 en memoria desaparece al detener el programa: reconstrúyelo al arrancar, lo que tarda unos segundos para decenas de miles de fragmentos de texto, o guarda la lista de documentos junto a la base.

#Ajustar el equilibrio entre BM25 y la búsqueda vectorial

Por defecto, RRF trata las dos listas por igual. Si tu corpus contiene muchas referencias (jurisprudencia, tickets, catálogos), da más peso a BM25; si las preguntas son conversacionales, mantén el equilibrio o favorece la búsqueda vectorial. En la función anterior, basta con pasar weights=[0.6, 0.4] para dar un peso del 60 % a BM25. Qdrant permite el mismo ajuste: su documentación indica que el peso de cada consulta es 1 por defecto, lo que reproduce la fórmula original de RRF, y que la constante k es ajustable en las versiones recientes. En Weaviate, se ajusta alpha: 1 equivale a una búsqueda puramente vectorial y 0 a una búsqueda exclusivamente por palabras clave.

Punto de partida según el tipo de corpus
Corpus y preguntasPeso inicial BM25 / vectorialLo que debes vigilar
Referencias, números, nombres propios (ámbito jurídico, tickets, catálogos)60 / 40¿Las preguntas por identificador aparecen en primer lugar?
Documentación redactada, preguntas en lenguaje natural40 / 60¿Las paráfrasis recuperan el pasaje correcto?
Corpus mixto o desconocido50 / 50Recall@5 con 30 a 50 preguntas reales
Siglas y jerga profesional55 / 45, con un diccionario de sinónimos para la parte de BM25¿La sigla y su forma desarrollada recuperan el mismo pasaje?
→
Medir antes de ajustar
Estos valores son puntos de partida, no resultados medidos. Prepara entre 30 y 50 preguntas reales con el pasaje esperado, calcula el recall entre los 5 primeros resultados para BM25 solo, la búsqueda vectorial sola y, después, la híbrida, y conserva la búsqueda híbrida únicamente si ofrece mejores resultados con tus documentos.

#Después de la fusión: añadir un reranker

La fusión da un conjunto de candidatos más variado que cada método por separado. Un reranker puede después ordenar este conjunto leyendo cada pasaje junto con la pregunta: las dos técnicas se combinan. El orden habitual es búsqueda híbrida, luego fusión, después aplicar un reranker a los primeros 20 a 50 candidatos y, por último, incluir los 3 a 5 mejores pasajes en el prompt. La guía sobre el reranker detalla esta última etapa, y la guía sobre chunking explica por qué el tamaño de los pasajes influye tanto en BM25 como en los embeddings.

#Preguntas frecuentes sobre búsqueda híbrida

FAQ
¿Qué es la búsqueda híbrida en un RAG?+
Es la combinación de dos búsquedas sobre la misma pregunta: una búsqueda vectorial, que encuentra pasajes de significado similar, y una búsqueda por palabras clave (BM25), que encuentra pasajes que contienen los mismos términos. Las dos clasificaciones se fusionan en una sola, normalmente mediante Reciprocal Rank Fusion, antes de enviar los mejores pasajes al modelo.
¿Hay que normalizar las puntuaciones BM25 y vectoriales antes de sumarlas?+
No es necesario con RRF, que solo utiliza rangos, y eso es precisamente el objetivo: las dos escalas no son comparables. Si prefieres combinar puntuaciones, debes normalizarlas primero, como hace la fusión por puntuaciones relativas de Weaviate o DBSF de Qdrant, y verificar que el resultado permanezca estable cuando cambie el corpus.
¿Qué valor de k elegir en RRF?+
Mantén 60, el valor predeterminado de Elasticsearch, mientras no tengas una razón para cambiarlo. Un valor más pequeño favorece más las primeras posiciones; uno más grande da más influencia a las posiciones más alejadas de la cabeza de la lista. Este ajuste influye poco en comparación con la calidad de la tokenización y el número de candidatos de cada lista.
¿Se puede hacer búsqueda híbrida con ChromaDB?+
Sí, combinando la búsqueda vectorial de Chroma, un índice BM25 en Python (la biblioteca rank_bm25) y una fusión RRF de unas pocas líneas. Asegúrate de usar los mismos identificadores en los dos índices y de eliminar los acentos durante la tokenización. Para un corpus que cambia frecuentemente, un motor con búsqueda híbrida nativa como Qdrant o Weaviate evita mantener dos índices.
¿Ralentiza mucho la búsqueda híbrida las consultas?+
En general, muy poco: BM25 sobre un índice en memoria es muy rápido, y la búsqueda vectorial ya se realiza. El verdadero costo proviene del reranker que pueda ejecutarse después y de la memoria del índice léxico. Mide el tiempo de principio a fin de tus consultas, no el tiempo de cada paso aislado.
¿Cómo saber si la búsqueda híbrida vale la pena para mis documentos?+
Compara, con entre 30 y 50 preguntas reales para las que conoces el pasaje esperado, la exhaustividad en los 5 primeros resultados de la búsqueda solo con BM25, de la búsqueda solo vectorial y de la búsqueda híbrida. Si la búsqueda híbrida no aporta ninguna mejora, puede que tu corpus sea puramente conversacional y baste con la búsqueda vectorial. Si mejora sobre todo con los identificadores, aumenta el peso de BM25.
¿Esta guía te ha ayudado?

¿Un comentario, un error, una precisión? Avísanos, eso mejora la guía para todos.