Avancé 11 minOptimisation

Recherche hybride : BM25 + vectoriel

Réponse directe

La recherche hybride lance en parallèle une recherche par mots-clés (BM25) et une recherche vectorielle sur la même question, puis fusionne les deux classements, le plus souvent par Reciprocal Rank Fusion (RRF). Elle rattrape les cas où l'embedding échoue (identifiants, noms propres, termes rares) sans perdre la compréhension des paraphrases. Qdrant, Weaviate et Elasticsearch la proposent nativement ; avec ChromaDB, on la construit en trente lignes de Python.

Une base vectorielle retrouve le sens, pas la lettre : une référence comme « RG/2024-117 » ou un numéro de ticket lui échappe. À l'inverse, la recherche par mots-clés ne comprend pas qu'« automobile » et « voiture » désignent la même chose. Ce guide montre comment combiner les deux, quelle méthode de fusion choisir, ce que font Qdrant et Weaviate, et un piège de tokenisation en français qui ruine silencieusement la partie BM25.

Par Mohamed Meguedmi·Màj 2026-09-30·Testé sur Windows, macOS, Linux

#Pourquoi le pur vectoriel rate certaines questions

La recherche vectorielle transforme chaque passage en un vecteur qui résume son sens général, puis renvoie les passages dont le vecteur est le plus proche de celui de la question. Cette compression fonctionne très bien pour les paraphrases, et mal pour tout ce qui doit être retrouvé à la lettre : un identifiant, un code d'erreur, un nom de personne, un sigle interne. Pour l'embedding, « PROD-4817 » et « PROD-4871 » se ressemblent, et un ticket sans rapport peut arriver devant le bon. La recherche hybride répond à ce défaut : elle ajoute un second classement fondé sur la présence exacte des mots, et fusionne les deux pour que chaque méthode compense les angles morts de l'autre.

Identifiants et références
Numéros de ticket, de dossier, de contrat, SKU, codes d'erreur : ce sont des chaînes qu'un embedding ne garde pas de façon fiable et que BM25 retrouve dès qu'elles figurent dans le passage.
Vocabulaire de spécialité rare
Termes médicaux, juridiques ou techniques peu fréquents : le poids d'un mot rare est élevé en BM25, alors que son embedding peut être flou.
Requêtes très courtes
Deux mots comme « facture 2024 » donnent peu de matière à un embedding ; les mots-clés, eux, se comparent tels quels.
Paraphrases et reformulations
Le cas inverse : « contrat à durée déterminée » et « CDD » ou « voiture » et « automobile » ne partagent aucun mot ; seule la recherche vectorielle les rapproche.
i
Un cas typique
Sur une base de tickets, la question « PROD-4817 » en pur vectoriel renvoie des tickets au contenu voisin, alors que la recherche par mots-clés isole immédiatement le seul ticket portant ce numéro.

#BM25 : ce que fait la recherche par mots-clés

Le kit RAG Local

Tes documents, ton IA : un RAG local fiable sur tes PDF, notes et mails — sans rien envoyer dans le cloud.

  • Espace en ligne à vie
  • PDF + fichiers
  • Mises à jour à vie

BM25 est la fonction de classement lexical qui équipe Lucene, Elasticsearch et OpenSearch, et que SQLite propose dans son module FTS5 (la documentation décrit sa fonction bm25() comme renvoyant une valeur qui indique la qualité de correspondance de la ligne avec la requête). Elle repose sur trois idées : un mot rare pèse plus qu'un mot fréquent, un mot répété compte de moins en moins au-delà d'un certain nombre d'occurrences, et un long passage est légèrement pénalisé par rapport à un court. Le paramètre k1 règle cette saturation : selon la description d'Elastic, il limite l'influence qu'un seul terme de la requête peut avoir sur le score d'un document.

Forces
Termes rares, identifiants, requêtes courtes, vocabulaire de spécialité, aucun modèle à charger ni à entraîner, index compact, résultat explicable (on sait quel mot a fait remonter le passage).
Faiblesses
Synonymes, reformulations, paraphrases, fautes de frappe ; sans lemmatisation, « signé » et « signature » sont deux mots différents.
Pré-requis
Une tokenisation soignée : minuscules, retrait des accents, éventuellement racinisation. C'est le point où la plupart des implémentations maison se trompent, voir plus bas.

#Recherche hybride : le principe

On exécute les deux recherches sur la même question, chacune renvoie sa liste de candidats (20 à 50 passages), puis on fusionne les deux listes en un seul classement. Le gain vient d'une observation simple : les passages qui apparaissent dans les deux listes sont presque toujours pertinents, et chaque méthode ramène en plus des passages que l'autre a manqués. Weaviate définit la recherche hybride en ces termes : elle combine les résultats d'une recherche vectorielle et d'une recherche par mots-clés en fusionnant les deux jeux de résultats, avec une méthode de fusion et des poids relatifs configurables. Le gain chiffré dépend du corpus et des questions : aucun chiffre général n'est fiable, et il faut le mesurer sur vos propres documents (voir plus bas).

#Fusionner sans normaliser : Reciprocal Rank Fusion

Le piège classique consiste à additionner les scores bruts. Un score BM25 est un nombre positif sans borne ; un score de similarité vectorielle est une distance ou un cosinus, dans une plage bornée : les deux échelles sont incomparables, et le moindre changement de corpus les décale. La Reciprocal Rank Fusion contourne le problème en n'utilisant que les rangs. La documentation d'Elasticsearch la présente comme une méthode qui n'exige aucun réglage et dont les indicateurs de pertinence n'ont pas besoin d'être liés entre eux.

Formule 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 passage classé premier par BM25 et cinquième par le vectoriel obtient 1/61 + 1/65, soit environ 0,0318 ; un passage classé quinzième dans les deux listes obtient 2/75, soit environ 0,0267. La constante k atténue l'avantage du tout premier rang : plus elle est grande, plus les rangs lointains comptent. Elasticsearch documente cette constante sous le nom rank_constant, avec 60 par défaut, et une taille de fenêtre (rank_window_size) qui fixe la longueur de chaque liste avant fusion. Une fenêtre plus grande améliore la pertinence au prix de la performance, précise la documentation.

#Et la fusion par scores ?

Certains moteurs proposent l'alternative : normaliser les scores de chaque liste puis les combiner par un poids. Weaviate documente les deux méthodes, un classement par rangs et une fusion par scores relatifs, cette dernière étant la méthode par défaut depuis la version 1.24 ; elle est requise pour utiliser l'autocut avec l'opérateur hybride. Qdrant propose RRF et DBSF, cette dernière conservant les scores bruts mais normalisant leur distribution (moyenne et écart-type) avant combinaison. La fusion par rangs est plus robuste quand on ne connaît pas la distribution des scores ; la fusion par scores permet un réglage plus fin quand on peut mesurer.

#Quel outil : moteurs qui font l'hybride nativement

Recherche hybride selon l'outil (documentation officielle, septembre 2026)
OutilHybride natifFusionRéglage du poids
QdrantOui, via l'API Query (disponible depuis la version 1.10)RRF ou DBSFPoids par requête et constante k réglables dans les versions récentes
WeaviateOui, opérateur hybridRangs ou scores relatifs (défaut depuis la 1.24)Paramètre alpha : 1 = vectoriel pur, 0 = mots-clés purs
ElasticsearchOui, récupérateur rrfRRFrank_constant (60 par défaut) et rank_window_size
SQLite FTS5 + extension vectorielleÀ assemblerÀ écrireÀ vous
ChromaDB + rank_bm25À assembler en PythonÀ écrire (RRF en 6 lignes)À vous

La différence de fond n'est pas la fusion elle-même, qui tient en quelques lignes, mais l'index : un moteur natif tient les deux index à jour ensemble, alors que l'assemblage maison garde l'index BM25 en mémoire et doit le reconstruire à chaque ajout de document. Pour un corpus de quelques milliers de passages qui change rarement, l'assemblage maison suffit largement. Au-delà, ou dès que les documents changent chaque jour, un moteur natif évite les incohérences entre les deux index. Le guide sur Weaviate présente cet outil en détail.

#Implémentation maison : ChromaDB, rank_bm25 et RRF

Le code suivant assemble les trois briques. Il corrige un défaut fréquent dans les exemples : la fonction de normalisation doit retirer les signes diacritiques avant de filtrer, sinon chaque lettre accentuée coupe un mot en deux.

Hybride BM25 + ChromaDB avec 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]]
!
Piège de tokenisation en français
Avec la normalisation courante « NFKD puis remplacer tout ce qui n'est pas a-z ou chiffre par un espace », « référence » devient « re fe rence » : chaque accent laisse un signe combinant qui est remplacé par un espace. La partie BM25 continue de fonctionner sur les identifiants, mais elle rate tous les mots accentués. Testez normalize sur trois phrases avant d'indexer.

Deux précautions pratiques. D'abord, les identifiants doivent être identiques dans les deux index : ici l'indice de la liste, converti en chaîne, sert d'identifiant Chroma. Ensuite, l'index BM25 en mémoire disparaît à l'arrêt du programme : reconstruisez-le au démarrage, ce qui prend quelques secondes pour des dizaines de milliers de passages, ou enregistrez la liste des documents à côté de la base.

#Régler l'équilibre entre BM25 et vectoriel

Par défaut, RRF traite les deux listes à égalité. Si votre corpus est riche en références (jurisprudence, tickets, catalogues), donnez plus de poids à BM25 ; si les questions sont conversationnelles, laissez l'équilibre ou favorisez le vectoriel. Dans la fonction précédente, il suffit de passer weights=[0.6, 0.4] pour un poids de 60 % sur BM25. Qdrant permet le même réglage : sa documentation indique que le poids de chaque requête vaut 1 par défaut, ce qui redonne la formule RRF d'origine, et que la constante k est réglable dans les versions récentes. Dans Weaviate, on règle alpha : 1 revient au vectoriel pur, 0 aux mots-clés purs.

Point de départ selon le type de corpus
Corpus et questionsPoids de départ BM25 / vectorielCe que vous surveillez
Références, numéros, noms propres (juridique, tickets, catalogues)60 / 40Les questions par identifiant remontent-elles en premier ?
Documentation rédigée, questions en langage naturel40 / 60Les paraphrases retrouvent-elles le bon passage ?
Corpus mixte ou inconnu50 / 50Rappel@5 sur 30 à 50 questions réelles
Sigles et jargon de métier55 / 45, avec un dictionnaire de synonymes côté BM25Sigle et forme longue retrouvent-ils le même passage ?
→
Mesurer avant de régler
Ces valeurs sont des points de départ, pas des résultats mesurés. Constituez 30 à 50 questions réelles avec le passage attendu, calculez le rappel dans les 5 premiers résultats pour BM25 seul, vectoriel seul, puis hybride, et ne gardez l'hybride que s'il gagne sur vos documents.

#Après la fusion : ajouter un reranker

La fusion donne un lot de candidats plus varié que chaque méthode seule. Un reranker peut ensuite trier ce lot en lisant chaque passage avec la question : les deux techniques se cumulent. L'ordre habituel est recherche hybride, puis fusion, puis reranker sur les 20 à 50 premiers, puis les 3 à 5 meilleurs passages dans le prompt. Le guide sur le reranker détaille cette dernière étape, et celui sur le chunking explique pourquoi la taille des passages influence autant BM25 que les embeddings.

#Questions fréquentes sur la recherche hybride

FAQ
Qu'est-ce que la recherche hybride dans un RAG ?+
C'est la combinaison de deux recherches sur la même question : une recherche vectorielle, qui retrouve les passages de sens proche, et une recherche par mots-clés (BM25), qui retrouve les passages contenant les mêmes termes. Les deux classements sont fusionnés en un seul, le plus souvent par Reciprocal Rank Fusion, avant de transmettre les meilleurs passages au modèle.
Faut-il normaliser les scores BM25 et vectoriels avant de les additionner ?+
Ce n'est pas nécessaire avec RRF, qui n'utilise que les rangs, et c'est même le but : les deux échelles ne sont pas comparables. Si vous préférez combiner les scores, il faut les normaliser d'abord, comme le fait la fusion par scores relatifs de Weaviate ou DBSF de Qdrant, et vérifier que le résultat reste stable quand le corpus change.
Quelle valeur de k choisir dans RRF ?+
Gardez 60, la valeur par défaut d'Elasticsearch, tant que vous n'avez pas de raison de faire autrement. Une valeur plus petite favorise davantage les tout premiers rangs, une plus grande donne plus d'influence aux rangs lointains. Ce réglage pèse peu par rapport à la qualité de la tokenisation et au nombre de candidats de chaque liste.
Peut-on faire de la recherche hybride avec ChromaDB ?+
Oui, en assemblant : la recherche vectorielle de Chroma, un index BM25 en Python (la bibliothèque rank_bm25) et une fusion RRF de quelques lignes. Veillez à utiliser les mêmes identifiants dans les deux index et à retirer les accents à la tokenisation. Pour un corpus qui change souvent, un moteur à hybride natif comme Qdrant ou Weaviate évite de maintenir deux index.
La recherche hybride ralentit-elle beaucoup les requêtes ?+
Très peu en général : BM25 sur un index en mémoire est très rapide, et la recherche vectorielle a déjà lieu. Le vrai coût vient du reranker éventuel qui suit, et de la mémoire de l'index lexical. Mesurez le temps de bout en bout sur vos requêtes, et non le temps de chaque étape isolée.
Comment savoir si l'hybride vaut le coup sur mes documents ?+
Comparez, sur 30 à 50 vraies questions dont vous connaissez le passage attendu, le rappel dans les 5 premiers résultats de BM25 seul, du vectoriel seul et de l'hybride. Si l'hybride ne gagne rien, votre corpus est peut-être purement conversationnel et le vectoriel suffit. S'il gagne surtout sur les identifiants, augmentez le poids de BM25.
Ce guide vous a aidé ?

Un retour, une erreur, une précision ? Faites-nous signe, ça améliore le guide pour tout le monde.