Pipeline RAG multi-recherche
Mis à jour le 30 juillet 2026
Nous avons deux moteurs qui fonctionnent indépendamment — la recherche sémantique (embeddings vectoriels) et la recherche lexicale (BM25) — et une promesse à tenir : les combiner dans un pipeline de recherche unifié. Deux problèmes à résoudre pour y arriver : une architecture qui coordonne les index, et un algorithme qui fusionne leurs classements.
Interface unifiée
L’architecture d’abord. Les deux implémentations partagent des API quasi identiques — add_document() et search() — et ce n’était pas un hasard : cette interface commune permet de les encapsuler dans une seule classe Retriever qui coordonne les deux approches :
- Recevoir la question de l’utilisateur
- La transmettre au VectorIndex et au BM25Index
- Collecter les résultats des deux systèmes
- Fusionner les résultats avec un algorithme de classement
Vu de l’application, le Retriever est un moteur de recherche unique ; le fait qu’il interroge deux index en coulisses est un détail d’implémentation invisible.
Reciprocal Rank Fusion (RRF)
Reste le défi central : fusionner des résultats de méthodes de recherche différentes. Chaque système renvoie ses résultats avec son propre mécanisme de notation — similarité cosinus d’un côté, scores BM25 de l’autre — donc les scores ne sont pas combinables directement : additionner des grandeurs sans échelle commune ne veut rien dire. La technique qui contourne l’obstacle : Reciprocal Rank Fusion (RRF), qui ignore les scores bruts et ne raisonne que sur les positions dans chaque classement.
Fonctionnement du RRF
Supposons que le VectorIndex renvoie : Section 2, Section 7, Section 6 — et le BM25Index : Section 6, Section 2, Section 7.
La formule RRF calcule un score pour chaque document :
RRF_score(d) = sum(1 / (k + rank_i(d)))
Où k est une constante (typiquement 60) et rank_i(d) est le rang du document dans le i-ème système. Exemple avec k=1 :
- Section 2 : 1/(1+1) + 1/(1+2) = 0.833
- Section 6 : 1/(1+3) + 1/(1+1) = 0.750
- Section 7 : 1/(1+2) + 1/(1+3) = 0.583
Classement final : Section 2 (1er), Section 6 (2e), Section 7 (3e). Lisez ce que la formule récompense : la section 2 l’emporte parce qu’elle est bien classée des deux côtés — le consensus entre moteurs pèse plus qu’une première place isolée. C’est exactement l’intuition qu’on attend d’une fusion : un document que les deux approches jugent pertinent mérite la tête. La constante k amortit l’écart entre les premiers rangs — avec la valeur usuelle de 60, être 1er ou 3e diffère peu, ce qui évite qu’un seul moteur n’écrase l’autre.
Implémentation
class Retriever:
def __init__(self, *indexes):
self._indexes = list(indexes)
def add_document(self, document):
for index in self._indexes:
index.add_document(document)
def search(self, query_text, k=1, k_rrf=60):
# Obtenir les résultats de tous les index
# Calculer les scores RRF
# Renvoyer les résultats fusionnés et triés
Le constructeur accepte un nombre variable d’index (*indexes) — pas spécifiquement deux. add_document propage chaque document vers tous les index, qui l’indexent chacun à leur façon ; search interroge tout le monde puis applique le RRF. Rien dans cette classe ne connaît les détails d’un index particulier : elle ne parle qu’à l’interface commune.
Extensibilité
C’est ce qui rend le design durable : la modularité permet d’ajouter facilement de nouvelles méthodes de recherche. Tant qu’un index implémente add_document() et search(), il s’intègre naturellement dans le pipeline hybride — un index par mots-clés métier, un index de titres, ce que votre application exigera — et le RRF fusionne trois classements aussi bien que deux, sans changement.
L’approche hybride combine ainsi la compréhension sémantique de la recherche vectorielle avec la précision de la correspondance de mots-clés, le tout unifié par un algorithme de fusion mathématiquement solide. Notre pipeline répond désormais correctement aux deux familles de questions — celles qui reformulent, et celles qui citent exactement.
Points clés à retenir
- L’interface commune (
add_document/search) permet la façadeRetriever: un moteur unique vu de l’application - Les scores cosinus et BM25 sont incomparables — le RRF fusionne sur les rangs, pas sur les scores
- RRF récompense le consensus : bien classé partout bat premier quelque part
- k (≈60) amortit les écarts de tête et équilibre l’influence des moteurs
- Extensible par construction : tout index respectant l’interface rejoint le pipeline sans modification