Aller au contenu principal

Pipeline RAG multi-index

Mis à jour le 30 juillet 2026

Nous avons construit séparément une recherche sémantique (avec les embeddings vectoriels) et une recherche lexicale (avec BM25). Il est temps de les combiner dans un pipeline de recherche unifié qui exploite les forces des deux approches.

L’architecture multi-index

Nos classes VectorIndex et BM25Index partagent des APIs quasi identiques : elles disposent toutes les deux de méthodes add_document() et search(). Cette cohérence permet de les encapsuler facilement dans une nouvelle classe appelée Retriever.

Le Retriever joue le rôle de coordinateur : il transmet les requêtes utilisateur aux deux index, collecte leurs résultats, et les fusionne via une technique appelée Reciprocal Rank Fusion.

Comprendre le Reciprocal Rank Fusion (RRF)

Fusionner les résultats de différentes méthodes de recherche n’est pas aussi simple que concaténer des listes. Chaque méthode utilise un système de scoring différent, il faut donc un moyen de normaliser et combiner les classements de manière équitable.

Exemple concret

Supposons qu’on cherche des informations sur INC-2023-Q4-011 et qu’on obtienne ces résultats :

VectorIndex retourne : Section 2 (rang 1), Section 7 (rang 2), Section 6 (rang 3)

BM25Index retourne : Section 6 (rang 1), Section 2 (rang 2), Section 7 (rang 3)

On combine ces résultats dans un tableau unique montrant le rang de chaque chunk dans les deux index, puis on applique la formule RRF :

RRF_score(d) = SUM( 1 / (k + rang_i(d)) )

Où k est une constante (souvent 60, mais ici on utilise 1 pour plus de clarté) et rang_i(d) est le rang du document d dans le classement i.

Calcul pour notre exemple (k=1)

ChunkRang VectorRang BM25Score RRF
Section 2121/(1+1) + 1/(1+2) = 0.833
Section 6311/(1+3) + 1/(1+1) = 0.75
Section 7231/(1+2) + 1/(1+3) = 0.583

Classement final : Section 2 (0.833) > Section 6 (0.75) > Section 7 (0.583)

C’est logique : la Section 2 à bien performe dans les deux index, elle remonte donc en tête. La Section 6 à un excellent rang BM25 mais un rang médiocre en vectoriel, elle se place en deuxième.

Détails d’implémentation

La classe Retriever encapsule plusieurs index de recherche et fournit une interface unifiée :

class Retriever:
    def __init__(self, *indexes: SearchIndex):
        if len(indexes) == 0:
            raise ValueError("Au moins un index doit etre fourni")
        self._indexes = list(indexes)

    def add_document(self, document: Dict[str, Any]):
        """Ajoute un document a tous les index."""
        for index in self._indexes:
            index.add_document(document)

    def search(self, query_text: str, k: int = 1, k_rrf: int = 60):
        """Recherche dans tous les index et fusionne via RRF."""
        # Obtenir les résultats de tous les index
        all_results = []
        for idx, results in enumerate(all_results):
            for rank, (doc, _) in enumerate(results):
                # Suivre les rangs des documents a travers les index
                # Appliquer la formule de scoring RRF
        # Retourner les résultats fusionnés et triés

Le point essentiel est qu’en maintenant des APIs cohérentes entre les différentes implémentations de recherche, on peut les combiner facilement sans couplage fort.

Test de l’approche hybride

Rappelez-vous le problème précédent : la recherche sur “que s’est-il passe avec INC-2023-Q4-011 ?” retournait des résultats inattendus avec l’approche vectorielle seule. L’incident cybersécurité (Section 10) arrivait en premier, mais l’analyse financière (Section 3) arrivait en deuxième au lieu de la section ingénierie logicielle, pourtant plus pertinente.

Avec notre Retriever hybride, les résultats sont bien meilleurs :

  1. Section 10 : Cybersécurité — Rapport de réponse aux incidents (le plus pertinent)
  2. Section 2 : Ingénierie logicielle — Améliorations de stabilité du Projet Phoenix
  3. Section 5 : Développements juridiques

Cela démontre comment combiner recherche sémantique et lexicale permet de dépasser les limites de chaque approche utilisée seule.

Extensibilite

La beauté de cette architecture réside dans son extensibilite. Puisque tous les index implémentent le même protocole SearchIndex avec les méthodes add_document() et search(), vous pouvez facilement ajouter de nouvelles méthodologies de recherche :

  • Envie d’ajouter un index par mots-clés ? Un index par graphe ? Un index de domaine spécialisé ? Il suffit d’implémenter la même interface et le Retriever l’intégrera automatiquement dans le processus de fusion.

Cette approche modulaire garde chaque implémentation de recherche focalisée et testable, tout en offrant un moyen propre de combiner leurs forces dans le système final.

Exercice : Construisez votre Retriever hybride
  1. Implémentez un Retriever qui combine VectorIndex et BM25Index
  2. Chargez un document de votre choix et ajoutez les chunks aux deux index
  3. Testez avec ces trois types de requêtes :
    • Une requête conceptuelle (“Quels sont les principaux défis ?”)
    • Une requête avec un terme exact (“INC-2023-Q4-011”)
    • Une requête mixte (“Quels problèmes liés à INC-2023-Q4-011 ont impacte la performance ?”)

Questions :

  • Pour quelle requête la fusion RRF apporte-t-elle le plus de valeur ?
  • Que se passe-t-il si vous changez la constante k de 1 à 60 ?
  • Comment ajouteriez-vous un troisième type d’index à votre Retriever ?