La recherche vectorielle retrouve des formulations proches ; BM25 excelle sur les termes exacts, identifiants et mots rares. Les combiner améliore souvent la couverture d’un corpus technique, à condition de fusionner correctement les résultats et de mesurer le bénéfice.
Deux signaux complémentaires
BM25 classe selon la présence et la rareté des termes. Il retrouve bien un code d’erreur ou un nom de fonction, mais manque certains synonymes. Les embeddings rapprochent le sens, mais peuvent ignorer une référence exacte. Lancez les deux recherches sur la même requête, après filtres d’accès et de métadonnées.
Fusionner les résultats
Les scores bruts ne sont généralement pas comparables. Une fusion par rang comme Reciprocal Rank Fusion combine les positions plutôt que les échelles. Une pondération peut favoriser lexical ou vectoriel selon la catégorie de requête. Récupérez davantage de candidats, fusionnez, puis utilisez éventuellement un reranker.
Évaluer la récupération
Construisez un dataset avec questions sémantiques, termes exacts, acronymes, fautes et cas multi-langues. Mesurez rappel et précision dans les premiers résultats, puis latence. Comparez lexical seul, vectoriel seul et hybride. Inspectez les cas où la fusion fait descendre une bonne source.
Production
Versionnez tokenizer lexical, embedding et paramètres de fusion. Appliquez les mêmes filtres aux deux branches. Surveillez résultats vides, distributions de scores et coût du reranking. Une recherche hybride ne corrige pas un mauvais chunking ou des métadonnées absentes.
FAQ
Faut-il toujours un reranker ?
Non. Ajoutez-le uniquement si l’évaluation montre un gain sur les premiers résultats.
Quelle pondération choisir ?
Commencez équilibré puis ajustez sur le dataset, idéalement par type de requête.
BM25 nécessite-t-il des embeddings ?
Non. C’est une recherche lexicale indépendante.
Fusionner les classements avec RRF
~~~python from collections import defaultdict
def rrf(*rankings, k=60): scores, documents = defaultdict(float), {} for ranking in rankings: for rank, doc in enumerate(ranking, start=1): documents[doc["id"]] = doc scores[doc["id"]] += 1 / (k + rank) return sorted( documents.values(), key=lambda doc: scores[doc["id"]], reverse=True, )
results = rrf(bm25_results, vector_results)[:10] ~~~
Appliquez les mêmes filtres d’autorisation aux deux recherches avant fusion. Mesurez rappel lexical, vectoriel et combiné sur vos requêtes. Un reranker peut améliorer le top final, avec un coût de latence. Conservez la provenance et le rang de chaque résultat pour diagnostiquer les régressions.