L’outil de récupération RAG : BM25, Dense, HyDE, RRF, MMR et réclassement
L’outil de récupération RAG : BM25, vecteurs denses, HyDE, RRF, MMR et réclassement, quand utiliser chacun d’eux, latence approximative par étape, et comment un pipeline hybride les intègre.
Sources: The Retrieval Toolkit That’s Quietly Deciding Whether Your RAG Pipeline Works.
Après avoir utilisé quelques pipelines RAG, on se heurte généralement au même problème. Les embeddings sont de bonne qualité et les prompts sont clairs, pourtant les réponses restent insuffisantes. Souvent, ce n’est pas la faute du modèle : c’est ce qui lui a été donné qui est en cause. La récupération d’informations constitue le goulot d’étranglement discret, et la plupart des échecs sont dus à un petit nombre de techniques que les équipes ignorent ou utilisent de manière incorrecte.
Voici cet ensemble d’outils : comment chaque élément fonctionne, pourquoi il existe, quand l’utiliser, et comment ces éléments s’assemblent dans un pipeline de production, ainsi que le temps nécessaire pour obtenir une réponse.
1. BM25 : une méthode de pondération des mots-clés intemporelle
BM25 (Best Matching 25) est une fonction de classement lexical sparsa et une amélioration par rapport à TF-IDF. Elle classe les documents en fonction du nombre de termes de la requête qu’ils partagent, en tenant compte de la fréquence des termes et de la longueur du document.
Pour chaque terme de recherche, un document obtient des points lorsque ce terme apparaît fréquemment, mais les gains diminuent progressivement : vingt occurrences ne sont pas du tout vingt fois plus pertinentes qu’une seule. Les documents longs sont pénalisés afin qu’ils ne puissent pas accumuler de correspondances simplement en étant trop longs.
from rank_bm25 import BM25Okapi
corpus = [doc.split() for doc in documents] # tokenized docs
bm25 = BM25Okapi(corpus)
query = "vector database indexing".split()
scores = bm25.get_scores(query)
top_k = sorted(range(len(scores)), key=lambda i: -scores[i])[:5]
BM25 se concentre sur le token littéral et non sur son sens, ce qui en fait un outil efficace pour :
- Les codes de produit, les SKUs, les codes d’erreur et les identifiants
- Les entités nommées telles que les personnes, les entreprises et les noms de médicaments
- Les termes rares ou techniques que les embeddings ont tendance à confondre
Utilisez-le lorsque la précision concernant des termes spécifiques prime sur la tolérance à la paraphrase : dans les domaines juridique, médical, de codage et de recherche de journaux d’activité. Il est peu coûteux et rapide, et ne nécessite ni entraînement ni GPU. Sa faiblesse réside dans l’absence totale de compréhension sémantique, de sorte que « voiture » et « automobile » lui semblent sans rapport.
2. Recherche vectorielle dense : correspondance sémantique
La récupération dense transforme les requêtes et les documents en vecteurs d’incorporation, comptant des centaines à des milliers de dimensions sans zéros, générés par un encodeur neuronal. La similarité est déterminée à l’aide du produit cosinus ou du produit scalaire, et un index de voisin le plus proche approximatif tel que HNSW ou IVF permet de trouver les éléments les plus proches.
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("BAAI/bge-small-en-v1.5")
doc_embeddings = model.encode(documents, normalize_embeddings=True)
query_embedding = model.encode([query], normalize_embeddings=True)
scores = np.dot(doc_embeddings, query_embedding.T).flatten()
top_k = np.argsort(-scores)[:5]
Celui-ci capture la similarité sémantique : « démission du dirigeant » trouve « le PDG a quitté ses fonctions » sans mots en commun. On appelle souvent cela une recherche sémantique, bien que, stricto sensu, ce soit le mécanisme de récupération dense qui est utilisé et que la correspondance sémantique en soit le résultat.
Utilisez-le pour des requêtes conversationnelles, des questions paraphrasées, une récupération interlinguistique, ou chaque fois que les utilisateurs n’emploient pas le vocabulaire exact de vos documents. Il peut être moins précis avec des termes rares, des codes ou des nombres exacts, car l’espace d’incorporation atténue les détails que BM25 conserve.
3. Réécriture de la requête et HyDE : réparer d’abord la requête
Parfois, le système de récupération est en ordre et le problème vient de la requête elle-même. Les utilisateurs tapent des questions courtes, vagues ou formulées de manière étrange, tandis que tant BM25 que la récupération dense fonctionnent le mieux avec du texte bien structuré et riche en informations.
La réécriture de la requête consiste pour un LLM à développer ou à préciser la question avant la recherche, en transformant « à quelle vitesse » en une question précise concernant le nombre de documents par seconde que le pipeline d’indexation X peut gérer en conditions optimales.
HyDE (Hypothetical Document Embeddings) va encore plus loin. Au lieu d’incorporer la requête, on demande à un LLM d’inventer une réponse plausible, et cette réponse fabriquée est ensuite incorporée pour trouver des documents réels. L’intuition est que une réponse inventée se trouve souvent plus proche, dans l’espace d’incorporation, des réponses authentiques que la question elle-même.
hypothetical_doc = llm.generate(f"Write a short passage answering: {query}")
hyde_embedding = model.encode([hypothetical_doc], normalize_embeddings=True)
scores = np.dot(doc_embeddings, hyde_embedding.T).flatten()
Les requêtes courtes ou ambiguës comptent parmi les causes les plus fréquentes d’un faible taux de récupération, car elles ne transmettent tout simplement pas assez d’informations. Cela est particulièrement problématique dans les interfaces de type chat et dans les domaines où le vocabulaire de l’utilisateur diffère de celui du corpus. Le coût consiste en une appel supplémentaire au LLM avant le début de la récupération, ce qui entraîne une latence et des frais supplémentaires ; de plus, une réponse hypothétique peu fiable peut orienter la récupération dans la mauvaise direction.
4. Recherche hybride : utilisation des deux méthodes
BM25 et la récupération dense échouent chacun à des extrémités opposées. La recherche hybride exécute les deux méthodes en parallèle et fusionne les listes de résultats, permettant de détecter à la fois les termes exacts et les paraphrases en une seule passe. Le problème est que les scores de BM25 et les similarités cosinus se situent sur des échelles différentes, ce qui rend l’arithmétique moyenne inefficace. C’est là que intervient la fusion par rang réciproque.
5. Fusion par rang réciproque : combiner les rangs, pas les scores
RRF évite complètement la normalisation des scores en se basant sur la position dans le classement et en ignorant les scores bruts.
RRF_score(doc) = Σ 1 / (k + rank_r(doc)) for each ranker r
La constante k, généralement égale à 60, atténue cet effet afin qu’un seul classement en première position ne puisse pas dominer.
def reciprocal_rank_fusion(ranked_lists, k=60):
scores = {}
for ranked_list in ranked_lists:
for rank, doc_id in enumerate(ranked_list):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: -x[1])
bm25_ranked = ["doc3", "doc1", "doc5"]
dense_ranked = ["doc1", "doc4", "doc3"]fused = reciprocal_rank_fusion([bm25_ranked, dense_ranked])
Les documents qui obtiennent de hauts scores de la part de plusieurs moteurs de recherche remontent naturellement en tête, sans nécessité de calibrage des échelles de scores. C’est la méthode habituelle pour fusionner des résultats hybrides, et elle convient à tout cas où deux ou plusieurs listes de classement provenant de moteurs différents sont combinées, comme BM25 associé à des méthodes denses, ou plusieurs modèles d’incorporation.
6. Relevance marginale maximale : réduction de la redondance
Même un classement parfait peut être inutile si les cinq premiers éléments disent tous la même chose. MMR réclasse en fonction à la fois de la pertinence et de la diversité.
MMR = λ · Sim(doc, query) − (1 − λ) · max(Sim(doc, selected))
Le paramètre λ définit l’équilibre. Lorsque λ = 1, on obtient simplement la méthode top-k, tandis que lorsque λ = 0, la diversité est maximisée indépendamment de la pertinence.
import numpy as np
def mmr(query_vec, doc_vecs, doc_ids, lambda_param=0.5, top_k=5):
selected, remaining = [], list(range(len(doc_ids)))
sim_to_query = doc_vecs @ query_vec
while len(selected) < top_k and remaining:
if not selected:
idx = remaining[np.argmax(sim_to_query[remaining])]
else:
mmr_scores = []
for i in remaining:
sim_to_selected = max(doc_vecs[i] @ doc_vecs[j] for j in selected)
mmr_scores.append(
lambda_param * sim_to_query[i] - (1 - lambda_param) * sim_to_selected
)
idx = remaining[np.argmax(mmr_scores)]
selected.append(idx)
remaining.remove(idx)
return [doc_ids[i] for i in selected]
Sans MMR, on peut consacrer toute la fenêtre de contexte à cinq textes presque identiques au lieu de cinq segments couvrant différents aspects de la réponse. Cela s’avère avantageux pour des corpus contenant du contenu superposé ou répété, comme des pages FAQ, des documents de produit avec du texte générique, ou tout ensemble de passages presque identiques.
7. Réclassement par cross-encoder : l’atout de la précision
Tout ce qui a été présenté jusqu’à présent relève du monde du bi-encoder, où les requêtes et les documents sont encodés séparément avant d’être comparés. Un cross-encoder introduit simultanément la requête et les candidats dans un seul transformateur, permettant ainsi au modèle de prendre en compte les deux textes. Cela permet des jugements de pertinence bien plus précis que la comparaison de vecteurs précalculés.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
pairs = [(query, doc) for doc in candidate_docs]
scores = reranker.predict(pairs)
reranked = [doc for _, doc in sorted(zip(scores, candidate_docs), reverse=True)]
Les cross-encoders surpassent systématiquement la similarité des bi-encoders en termes de classement de la pertinence, car ils analysent conjointement la requête et le document. Utilisez-en un comme étape finale de finition sur un petit ensemble de candidats, généralement les 20 à 50 meilleurs résultats après fusion ou MMR, et jamais sur l’ensemble complet du corpus, car chaque paire de documents nécessite son propre traitement. Après l’appel à l’LLM, c’est l’élément qui ajoute le plus de latence dans cette liste, d’où sa position à la fin du pipeline et non au début.
8. Temps de réponse : le coût de tout cela
Chaque technique mentionnée améliore la qualité de la récupération des données, mais aucune n’est gratuite. Chacune ajoute de la latence, et le temps total, de la soumission d’une requête à la réception de la réponse finale, détermine si un pipeline soigneusement ajusté peut être utilisé en production. Coûts typiques par étape :
- Réécriture de la requête ou HyDE, si utilisé : environ 200 ms à 1 s, avec une appel supplémentaire à un LLM avant le début de la récupération.
- Recherche BM25 : environ 5 à 20 ms, à partir d’un index inversé en mémoire peu coûteux.
- Recherche vectorielle dense (ANN) : environ 10 à 50 ms, en fonction de la taille et du type de l’index, HNSW ou plat.
- Fusion RRF : 1 à 5 ms, opérations arithmétiques simples sur des listes de classement.
- Re-classement MMR : environ 20 à 100 ms, le temps augmente avec le nombre de candidats car c’est un processus itératif et par paires.
- Re-classement avec encodeur croisé, si utilisé : environ 50 à 300 ms, souvent le coût le plus élevé après la génération.
- Génération par LLM : d’environ 500 ms à plusieurs secondes, généralement la composante la plus importante.
Pour garder ce total sous contrôle :
- Exécuter BM25 et la récupération dense en parallèle. Ils ne dépendent pas l’un de l’autre.
- Appliquer MMR à un petit ensemble, par exemple les 20 à 30 meilleurs résultats après RRF, car les vérifications de similarité par paires sont très coûteuses en ressources.
- Mémoriser les embeddings des requêtes fréquentes ou répétées.
- Ajouter un encodeur croisé uniquement lorsque l’amélioration de la précision justifie clairement le retard induit.
- Suivre le temps de réponse en tant que métrique essentielle, aux côtés de la qualité de la réponse telle que la fidélité ou le recall@k. Un système 2 % plus précis mais trois fois plus lent peut ne pas valoir la peine d’être déployé.
Comment les éléments s’assemblent
Un pipeline de récupération hybride en production fonctionne généralement de la manière suivante :
- La réécriture de la requête ou HyDE met en forme une requête vague avant la récupération, facultativement, au détriment de la latence.
- BM25 détecte les termes exacts.
- La récupération dense prend en compte le sens.
- RRF fusionne les deux listes sans avoir à s’occuper de la normalisation des scores.
- MMR réduit la liste fusionnée pour obtenir un ensemble de candidats divers et non redondant.
- Un encodeur croisé effectue une dernière étape de haute précision sur cet ensemble réduit.
- Le temps de réponse est l’indicateur qui montre si tout est prêt à être utilisé.
Guide de décision rapide
- Des requêtes courtes, vagues ou hors vocabulaire ? Réécrivez-les ou essayez HyDE.
- Précision par correspondance exacte pour des codes, des identifiants et des termes rares ? BM25.
- Tolérance aux paraphrases ? Recherche dense.
- Les deux en même temps ? Fusionnez avec RRF.
- Des segments redondants ou dupliqués dans le contexte ? MMR.
- La plus haute pertinence sur un petit ensemble final ? Réclassement par encodeur croisé.
- Précis mais lent ? Mesurez chaque étape avant d’optimiser aveuglément.
Aucune technique isolée ne constitue une solution miracle. Les meilleurs systèmes de production les combinent, en les ajustant en fonction de journaux de requêtes réels et non de l’intuition, et le mélange idéal dépend fortement du corpus ainsi que du budget de latence.