Pipeline RAG multi-index
Nous avons construit separement une recherche semantique (avec les embeddings vectoriels) et une recherche lexicale (avec BM25). Il est temps de les combiner dans un pipeline de recherche unifie 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 methodes add_document() et search(). Cette coherence permet de les encapsuler facilement dans une nouvelle classe appelee Retriever.
Le Retriever joue le role de coordinateur : il transmet les requetes utilisateur aux deux index, collecte leurs resultats, et les fusionne via une technique appelee Reciprocal Rank Fusion.
Comprendre le Reciprocal Rank Fusion (RRF)
Fusionner les resultats de differentes methodes de recherche n’est pas aussi simple que concatener des listes. Chaque methode utilise un systeme de scoring different, il faut donc un moyen de normaliser et combiner les classements de maniere equitable.
Exemple concret
Supposons qu’on cherche des informations sur INC-2023-Q4-011 et qu’on obtienne ces resultats :
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 resultats 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)) )
Ou k est une constante (souvent 60, mais ici on utilise 1 pour plus de clarte) et rang_i(d) est le rang du document d dans le classement i.
Calcul pour notre exemple (k=1)
| Chunk | Rang Vector | Rang BM25 | Score RRF |
|---|---|---|---|
| Section 2 | 1 | 2 | 1/(1+1) + 1/(1+2) = 0.833 |
| Section 6 | 3 | 1 | 1/(1+3) + 1/(1+1) = 0.75 |
| Section 7 | 2 | 3 | 1/(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 a bien performe dans les deux index, elle remonte donc en tete. La Section 6 a un excellent rang BM25 mais un rang mediocre en vectoriel, elle se place en deuxieme.
Details d’implementation
La classe Retriever encapsule plusieurs index de recherche et fournit une interface unifiee :
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 resultats 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 resultats fusionnes et tries
Le point essentiel est qu’en maintenant des APIs coherentes entre les differentes implementations de recherche, on peut les combiner facilement sans couplage fort.
Test de l’approche hybride
Rappelez-vous le probleme precedent : la recherche sur “que s’est-il passe avec INC-2023-Q4-011 ?” retournait des resultats inattendus avec l’approche vectorielle seule. L’incident cybersecurite (Section 10) arrivait en premier, mais l’analyse financiere (Section 3) arrivait en deuxieme au lieu de la section ingenierie logicielle, pourtant plus pertinente.
Avec notre Retriever hybride, les resultats sont bien meilleurs :
- Section 10 : Cybersecurite — Rapport de reponse aux incidents (le plus pertinent)
- Section 2 : Ingenierie logicielle — Ameliorations de stabilite du Projet Phoenix
- Section 5 : Developpements juridiques
Cela demontre comment combiner recherche semantique et lexicale permet de depasser les limites de chaque approche utilisee seule.
Extensibilite
La beaute de cette architecture reside dans son extensibilite. Puisque tous les index implementent le meme protocole SearchIndex avec les methodes add_document() et search(), vous pouvez facilement ajouter de nouvelles methodologies de recherche :
- Envie d’ajouter un index par mots-cles ? Un index par graphe ? Un index de domaine specialise ? Il suffit d’implementer la meme interface et le
Retrieverl’integrera automatiquement dans le processus de fusion.
Cette approche modulaire garde chaque implementation de recherche focalisee et testable, tout en offrant un moyen propre de combiner leurs forces dans le systeme final.
Exercice : Construisez votre Retriever hybride
- Implementez un
Retrieverqui combineVectorIndexetBM25Index - Chargez un document de votre choix et ajoutez les chunks aux deux index
- Testez avec ces trois types de requetes :
- Une requete conceptuelle (“Quels sont les principaux defis ?”)
- Une requete avec un terme exact (“INC-2023-Q4-011”)
- Une requete mixte (“Quels problemes lies a INC-2023-Q4-011 ont impacte la performance ?”)
Questions :
- Pour quelle requete la fusion RRF apporte-t-elle le plus de valeur ?
- Que se passe-t-il si vous changez la constante
kde 1 a 60 ? - Comment ajouteriez-vous un troisieme type d’index a votre
Retriever?