Aller au contenu
Développement medium

Re-ranking : la passe qui fiabilise le retrieval

9 min de lecture

logo python

La recherche d'un RAG — dense, lexicale ou hybride — est rapide mais approximative : elle compare des représentations calculées séparément pour la question et pour les documents, sans jamais les confronter vraiment. Résultat : un document hors sujet remonte parfois en tête. Le re-ranking corrige cela par une seconde passe : un modèle cross-encoder lit chaque paire (question, candidat) ensemble et la note finement. Ce guide montre le schéma en deux temps — recherche large, puis re-ranking du lot —, son implémentation avec un cross-encoder, et le critère pour décider s'il vaut son coût. Public visé : développeur dont le RAG rate encore des réponses malgré une recherche correcte.

  • Pourquoi la recherche seule reste approximative.
  • La différence entre bi-encoder et cross-encoder.
  • Le schéma en deux temps : présélection large, puis re-ranking.
  • Implémenter un re-ranking avec un cross-encoder.
  • Quand le re-ranking vaut son coût.
  • Avoir un RAG fonctionnel à améliorer.
  • Une instance Ollama avec nomic-embed-text.
  • Python 3.10+ — le cross-encoder se télécharge au premier lancement.

La recherche d'un RAG repose sur un bi-encoder : un modèle qui calcule, séparément, le vecteur de la question et le vecteur de chaque document. La comparaison se fait ensuite entre ces vecteurs déjà figés.

Cette séparation est ce qui rend la recherche rapide — les vecteurs des documents sont calculés une fois, à l'indexation. Mais c'est aussi sa faiblesse : la question et le document ne sont jamais « lus ensemble ». Le modèle ne voit jamais la question à la lumière d'un document précis. Il compare deux résumés indépendants.

Conséquence concrète, observée dans le lab de ce guide : sur la question « comment garder les données quand on supprime un conteneur ? », la recherche dense place en tête un document sur la commande prune — hors sujet —, et relègue plus bas le document qui parle réellement de conserver les données. La recherche a échoué, alors que le bon document était bien dans l'index.

Le cross-encoder travaille autrement. Au lieu de calculer deux vecteurs séparés, il prend la paire complète — question et document concaténés — et la passe en une fois dans le modèle. Il en sort un score de pertinence unique.

La différence est décisive. Le cross-encoder « lit » la question en présence du document : il peut juger si ce document précis répond à cette question précise. Là où le bi-encoder compare deux photos prises séparément, le cross-encoder regarde les deux côte à côte.

Bi-encoder (recherche)Cross-encoder (re-ranking)
CalculVecteurs séparésPaire lue ensemble
VitesseRapideLent
PrécisionApproximativeÉlevée
Passe à l'échelleTout le corpusUne présélection

Le cross-encoder est plus juste — mais bien plus lent. L'appliquer à un corpus entier serait inenvisageable : il faudrait, à chaque question, le faire tourner sur chaque document. D'où le schéma en deux temps.

L'idée : combiner les deux. On utilise le bi-encoder pour ce qu'il fait bien — présélectionner vite —, puis le cross-encoder pour ce qu'il fait bien — classer juste.

La recherche dense remonte une présélection large — pas le top 3, mais une douzaine de candidats. Le cross-encoder note ensuite chaque paire (question, candidat) de ce petit lot, et on garde le top 3 ou 5 de ce reclassement.

def recherche_avec_reranking(question: str, k: int = 3) -> list[str]:
"""Recherche large, puis re-ranking : le pipeline en deux temps."""
candidats = recherche_dense(question, n=5) # présélection large
return reclasser(question, candidats, k) # reclassement fin

Le cross-encoder ne tourne que sur quelques candidats — pas sur le corpus entier. Sa lenteur devient négligeable, et sa précision profite à la sélection finale. C'est le meilleur des deux modèles.

Le cross-encoder s'utilise via la classe CrossEncoder de sentence-transformers. On lui donne des paires (question, document) ; il renvoie un score par paire.

from sentence_transformers import CrossEncoder
MODELE_RERANK = "BAAI/bge-reranker-v2-m3" # cross-encoder multilingue
def reclasser(question: str, candidats: list[str], k: int = 3) -> list[str]:
"""Seconde passe : le cross-encoder note chaque paire et reclasse."""
paires = [(question, candidat) for candidat in candidats]
scores = CrossEncoder(MODELE_RERANK).predict(paires)
classement = sorted(zip(candidats, scores), key=lambda c: c[1], reverse=True)
return [texte for texte, _ in classement[:k]]

On construit les paires, le cross-encoder les note, on trie par score décroissant. Sur le cas qui mettait la recherche dense en échec, le re-ranking rétablit le bon ordre :

Après recherche dense (1er) : Pour purger les conteneurs arrêtés...
Après re-ranking (1er) : Un volume Docker conserve les données après
la suppression du conteneur.

Le document pertinent, que la recherche dense avait relégué, remonte en première place. C'est exactement le rôle du re-ranking : rattraper les erreurs de la première passe.

Le re-ranking améliore la qualité — mais il coûte. Le cross-encoder est un modèle à charger et à exécuter ; chaque question ajoute une passe d'inférence. Ce coût n'est pas toujours justifié.

Le re-ranking se justifie quand la qualité du retrieval, mesurée, plafonne malgré une recherche correcte — un recall honnête, mais le bon document trop souvent en deuxième ou troisième position plutôt qu'en première. C'est le symptôme classique qu'une seconde passe peut corriger.

Il ne se justifie pas par principe. Si la recherche — surtout en mode hybride — place déjà le bon document en tête, ajouter un cross-encoder alourdit sans gagner. La règle est constante dans tout le parcours RAG : on mesure d'abord, on optimise ensuite. Le re-ranking est un levier qu'on actionne sur la foi d'un chiffre, pas d'une intuition.

SymptômeCause probableSolution
Le re-ranking ne change rienRecherche déjà bonneNormal — le re-ranking n'a rien à corriger
Le re-ranking dégrade le classementCross-encoder faible ou non multilingueChoisir bge-reranker-v2-m3 ou équivalent
Latence trop forteCross-encoder sur trop de candidatsRéduire la présélection (n) avant re-ranking
Premier lancement très longTéléchargement du modèleNormal — le modèle est mis en cache ensuite
Aucun gain mesuréRe-ranking ajouté sans diagnosticMesurer le recall avant de l'ajouter
  • La recherche s'appuie sur un bi-encoder : rapide, mais elle compare des vecteurs figés séparément.
  • Le cross-encoder lit la paire (question, document) ensemble — plus juste, mais plus lent.
  • Le schéma en deux temps : la recherche présélectionne large, le cross-encoder reclasse le lot.
  • Le re-ranking rattrape les erreurs de la première passe — il remonte le bon document en tête.
  • La qualité du reranker est déterminante — un modèle faible se trompe encore.
  • Le re-ranking s'ajoute sur la foi d'une mesure, pas par principe.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn