Pipeline RAG multi-index
Sur cette page
On dispose de deux index, l’un sémantique, l’autre lexical. Cette leçon les réunit dans un même objet, le Retriever, qui interroge les deux et fusionne leurs classements par la fusion des rangs réciproques (Reciprocal Rank Fusion, ou RRF).
Une interface commune
VectorIndex (leçon 38) et BM25Index (leçon 39) offrent les mêmes méthodes : add_document(document) pour indexer, et search(query, k), qui renvoie des paires (document, valeur) de la meilleure à la moins bonne. Cette interface commune permet de les envelopper dans une classe qui ignore tout de leur fonctionnement interne.
Fusionner des rangs, pas des scores
On ne peut pas additionner une distance cosinus et un score BM25 : ils n’ont ni la même échelle ni le même sens de lecture. La fusion des rangs réciproques ne regarde donc que la place de chaque document dans chaque classement :
score_RRF(d) = Σ 1 / (k + rang_i(d))
rang_i(d) est la place du document d dans le classement de l’index i, 1 pour le premier, et k une constante qui atténue l’écart entre les premières places. La valeur 60 est la plus répandue ; le cours officiel prend 1 dans son exemple, pour rendre les écarts visibles.
| Section | Rang vectoriel | Rang BM25 | Score RRF, k = 1 |
|---|---|---|---|
| Section 2 | 1 | 2 | 1/2 + 1/3 = 0,833 |
| Section 6 | 3 | 1 | 1/4 + 1/2 = 0,75 |
| Section 7 | 2 | 3 | 1/3 + 1/4 = 0,583 |
La section 2, bien classée par les deux index, passe en tête. Avec k = 60, l’ordre reste le même, mais les écarts se resserrent : 0,0325, 0,0323 et 0,0320.
Le Retriever
Le code du cours officiel laisse la fusion en commentaires. En voici une version complète :
class Retriever:
def __init__(self, *indexes):
if not indexes:
raise ValueError("il faut au moins un index")
self.indexes = list(indexes)
def add_document(self, document):
for index in self.indexes:
index.add_document(document)
def search(self, query, k=1, k_rrf=60, candidates=20):
scores, documents = {}, {}
for index in self.indexes:
for rank, (doc, _) in enumerate(index.search(query, k=candidates), start=1):
key = id(doc)
documents[key] = doc
scores[key] = scores.get(key, 0.0) + 1 / (k_rrf + rank)
best = sorted(scores, key=scores.get, reverse=True)[:k]
return [(documents[key], scores[key]) for key in best]
add_documentajoute le même objet document à tous les index. C’est ce qui permet de le reconnaître d’un classement à l’autre, avecid(doc).- Chaque index renvoie ses
candidatesmeilleurs résultats. Un document absent d’une liste ne reçoit rien de cet index : BM25 ne renvoie que les documents qui contiennent au moins un terme de la question. - Les valeurs propres à chaque index, distance ou score, sont ignorées : seul le rang compte.
retriever = Retriever(VectorIndex(generate_embedding), BM25Index())
for chunk in chunk_by_section(text):
retriever.add_document({"content": chunk})
for doc, score in retriever.search("Que s'est-il passé avec INC-2023-Q4-011 ?", k=3):
print(round(score, 4), doc["content"][:120])
Ici, VectorIndex.add_document vectorise les morceaux un par un, avec un appel par morceau, comme le fait de toute façon generate_embedding sur Google Cloud. Pour un gros corpus, Agent Platform propose aussi des prédictions d’embeddings par lots ; on ajoute ensuite chaque vecteur avec add_vector, et le même objet document à l’index BM25.
Le résultat
Dans la démonstration du cours officiel, la recherche vectorielle seule plaçait la section 10 (cybersécurité) en tête, puis la section 3 (analyse financière), qui ne mentionne pas l’incident. Avec le Retriever hybride, on obtient la section 10 (rapport d’incident de cybersécurité), puis la section 2 (ingénierie logicielle, stabilisation du projet Phoenix), puis la section 5 (développements juridiques).
Un pipeline extensible
Tout index qui offre add_document et search peut rejoindre le Retriever : un index par mots-clés, un index de graphe, un index spécialisé dans un domaine. La fusion n’a pas à changer. C’est aussi l’architecture que l’article d’Anthropic décrit pour une RAG standard : embeddings et BM25 en parallèle, puis fusion des rangs et dédoublonnage des résultats.
À vous de jouer
Comparer trois recherches. Sur votre document, préparez trois questions : une conceptuelle, une avec un terme exact, une qui mélange les deux. Pour chacune, comparez les trois premiers résultats de VectorIndex seul, de BM25Index seul et du Retriever. Refaites l’essai avec k_rrf=1 puis k_rrf=60, et notez si l’ordre change.
Sources officielles : A multi-index RAG pipeline (Claude Academy) · Contextual Retrieval (Anthropic : embeddings, BM25 et fusion des rangs)
Points clés à retenir
- Le
Retrieverinterroge plusieurs index qui partagent la même interface. - On fusionne des rangs, pas des scores : une distance et un score BM25 ne se comparent pas.
- Score RRF : somme de 1 / (k + rang) ; k = 60 est la valeur la plus répandue.
- Le même objet document est ajouté à tous les index, ce qui permet de le reconnaître.
- Un nouvel index rejoint la fusion sans que le
Retrieverchange.