
Je suis Jules Ginhac, Co-Founder & ingénieur IA chez Apogée Consult à Lyon. Je conçois et déploie des architectures IA génératives (RAG, agents, LLMOps) pour des PME, startups et organisations publiques.
Hybrid search BM25 + embeddings : quand le coût d'infra est justifié
- rag
Recherche vectorielle pure ou hybride BM25 + embeddings ? Analyse des cas où l'infra supplémentaire est rentable, avec le détail de la fusion RRF et ses limites.
Hybrid search BM25 + embeddings : quand le coût d'infra est justifié
La recherche vectorielle seule échoue sur des requêtes qui contiennent des termes exacts rares : un numéro de contrat, un code produit, un acronyme métier non standard. L'embedding encode la sémantique, pas l'orthographe. BM25 encode l'orthographe, pas la sémantique. L'hybride combine les deux, mais il ajoute de la complexité opérationnelle.
La vraie question n'est pas "hybride ou vectoriel" mais "dans quel cas le delta de qualité justifie le delta de coût".
Ce que chaque approche fait bien
La recherche vectorielle excelle quand la requête est paraphrasée différemment du document source. "Comment résilier mon abonnement" retrouve un chunk intitulé "Procédure de fin de contrat" parce que les embeddings capturent la proximité sémantique entre ces formulations.
BM25 (Best Match 25, défini dans Robertson & Zaragoza, 2009) excelle sur la correspondance de termes. Si le document contient "SKU-4472-B" et que la requête est "SKU-4472-B", BM25 le retrouve avec un score élevé. Un embedding aplatit ce code en un vecteur voisin d'autres codes produits, la discrimination est perdue.
Les cas où l'hybride est justifié
Trois patterns récurrents dans nos projets où le vectoriel seul produisait un recall insuffisant :
Corpus avec identifiants métier denses. Références produit, numéros de dossier, codes RNCP, ISBNs, codes postaux. L'embedding ne discrimine pas ces identifiants entre eux.
Requêtes très courtes (1 à 3 tokens). Sur une requête d'un mot, le vecteur est peu discriminant. BM25 apporte une pondération TF-IDF qui favorise les termes rares dans le corpus.
Corpus multilingue ou avec translittérations. Un embedding multilingue peut rater des noms propres translittérés. BM25 fait correspondre les caractères exacts.
Requêtes de type "lookup" plutôt que "conceptuelles". "Quel est le délai de rétractation article L.221-18" est une requête de référence légale. L'article de loi cité doit correspondre exactement.
La fusion : Reciprocal Rank Fusion
Pour combiner les résultats BM25 et vectoriel, on utilise RRF (Cormack, Clarke & Buettcher, 2009). La formule est simple :
def reciprocal_rank_fusion(
bm25_results: list[tuple[str, float]],
vector_results: list[tuple[str, float]],
k: int = 60,
alpha: float = 0.5,
) -> list[tuple[str, float]]:
"""
Fusionne deux listes de résultats par RRF.
k=60 est la valeur standard de l'article original.
alpha contrôle le poids relatif des deux sources (0.5 = équilibré).
Retourne une liste triée par score RRF décroissant.
"""
scores: dict[str, float] = {}
for rank, (doc_id, _) in enumerate(bm25_results):
scores[doc_id] = scores.get(doc_id, 0) + alpha / (k + rank + 1)
for rank, (doc_id, _) in enumerate(vector_results):
scores[doc_id] = scores.get(doc_id, 0) + (1 - alpha) / (k + rank + 1)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)RRF a une propriété importante : il est robuste aux différences d'échelle entre les scores BM25 et les scores de similarité cosinus. Pas besoin de normaliser. C'est pour ça qu'on le préfère à une fusion par scores pondérés.
Le coût réel de l'infrastructure
Un index vectoriel seul : Pinecone, Qdrant, ou pgvector suffisent. Un index BM25 en production nécessite une solution dédiée.
Options courantes :
- Elasticsearch / OpenSearch : robuste, mais coût opérationnel élevé (mémoire, maintenance).
- BM25S (bibliothèque Python pure, papier 2024) : index en mémoire, sans serveur, suffisant pour des corpus < 500 000 documents.
- Weaviate, Qdrant : proposent nativement un mode hybride avec BM25 intégré, ce qui supprime le besoin d'un système séparé.
Sur nos derniers projets, on utilise Qdrant en mode hybride quand le corpus le justifie. L'index BM25 est maintenu en mémoire par Qdrant lui-même, ce qui évite de gérer deux systèmes distincts.
Le vrai surcoût n'est pas l'infrastructure mais le double ingestion : chaque document doit être indexé deux fois (tokens pour BM25, vecteurs pour l'embedding). Le pipeline d'ingestion est plus complexe, les mises à jour incrémentales aussi.
Mesures sur un cas réel
Sur un corpus de documentation technique industrielle (fiches produits, datasheets, manuels de maintenance, environ 80 000 chunks), on a mesuré sur 200 requêtes de test :
| Approche | Recall@5 | Latence médiane |
|---|---|---|
| Vectoriel seul (ada-002) | 71 % | 180 ms |
| BM25 seul (BM25S) | 58 % | 45 ms |
| Hybride RRF (alpha=0.5) | 83 % | 220 ms |
Sur les requêtes contenant des références produit exactes, le recall vectoriel seul tombait à 44 %. L'hybride remontait à 91 %.
Le delta de +12 points de recall global justifiait l'ajout de la couche BM25 sur ce corpus. Ce ne serait pas le cas sur un corpus de FAQ en prose naturelle, où le vectoriel seul atteint 88 % et l'hybride n'apporte que 2 points.
Quand ne pas passer à l'hybride
Si votre corpus est homogène, en prose naturelle, sans identifiants exacts critiques, le vectoriel seul suffit la plupart du temps. Ajouter BM25 n'apportera pas grand chose et vous paierez la complexité opérationnelle.
Si votre contrainte principale est la latence (< 100 ms P95), l'hybride avec un système BM25 externe est difficile à tenir. BM25S en mémoire est plus rapide mais nécessite que l'index tienne en RAM.
La limite que nous n'avons pas encore levée : comment ajuster dynamiquement alpha par requête selon le type détecté (lookup vs conceptuel), sans ajouter une couche de classification qui devient elle-même un point de défaillance ?