Reranking : améliorer la pertinence
Mis à jour le 29 juillet 2026
Objectifs
- Comprendre pourquoi le reranking améliore les résultats
- Implémenter un reranker avec un cross-encoder
- Intégrer le reranking dans un pipeline RAG
Le problème du bi-encoder
Toute la recherche que vous avez construite jusqu’ici repose sur un bi-encoder : la requête et les documents sont encodés séparément, chacun de leur côté, et on les compare par produit scalaire. C’est ce qui rend la recherche instantanée sur un million de chunks, puisque les vecteurs du corpus ont été calculés une fois pour toutes. C’est aussi ce qui la rend imprécise, car le modèle ne voit jamais la requête et le document ensemble : il doit résumer chaque texte en un vecteur sans savoir ce qu’on lui demandera plus tard.
Un cross-encoder, qu’on appelle reranker, prend la paire (requête, document) en entrée et produit un score de pertinence bien plus fin, parce qu’il lit les deux textes conjointement et peut repérer que le document parle bien du sujet mais dans un sens contraire. Ce gain a un prix : il faut un passage complet du modèle par document, ce qui interdit de l’utiliser sur tout le corpus. La solution consiste à l’employer là où il est rentable, sur un petit ensemble de candidats déjà présélectionnés.
D’où le pipeline en deux étapes, où chaque modèle fait ce qu’il sait faire : le rapide dégrossit, le précis tranche.
Requête → Bi-encoder → Top 50 candidats → Cross-encoder → Top 5 résultats
Reranking avec Cohere
Cohere expose un reranker performant via API, ce qui évite d’héberger un modèle. La fonction suivante enchaîne les deux étapes : elle récupère d’abord vingt candidats par similarité vectorielle, puis demande à rerank-v3.5 de n’en garder que cinq. Remarquez le champ index_original conservé dans le résultat : le reranker renvoie des positions relatives à la liste des candidats, et vous en aurez besoin pour remonter au document réel dans votre corpus.
import cohere
from openai import OpenAI
co = cohere.Client(api_key="votre-cle-cohere")
openai_client = OpenAI()
def rechercher_et_reranker(
question: str,
documents: list[str],
embeddings_corpus,
k_retrieval: int = 20,
k_final: int = 5
) -> list[dict]:
"""Recherche sémantique + reranking."""
# Étape 1 : Retrieval rapide (bi-encoder)
query_emb = openai_client.embeddings.create(
input=question,
model="text-embedding-3-large"
).data[0].embedding
import numpy as np
scores = embeddings_corpus @ np.array(query_emb)
top_indices = np.argsort(scores)[-k_retrieval:][::-1]
candidats = [documents[i] for i in top_indices]
# Étape 2 : Reranking (cross-encoder)
rerank_response = co.rerank(
query=question,
documents=candidats,
top_n=k_final,
model="rerank-v3.5"
)
return [
{
"texte": candidats[r.index],
"score_rerank": r.relevance_score,
"index_original": int(top_indices[r.index])
}
for r in rerank_response.results
]
Reranking avec un LLM
Si vous préférez ne pas ajouter un fournisseur supplémentaire à votre architecture — pour des raisons contractuelles, de localisation des données ou simplement de facturation —, un LLM que vous appelez déjà peut tenir le rôle. On lui soumet la question et un document, on lui demande une note de 0 à 10, et on trie. La température à zéro et le max_tokens réduit à cinq sont là pour rendre la sortie déterministe et bon marché ; le try/except absorbe les cas où le modèle répond autre chose qu’un nombre.
from openai import OpenAI
client = OpenAI()
def reranker_llm(
question: str,
candidats: list[str],
k: int = 5
) -> list[dict]:
"""Utilise un LLM pour scorer la pertinence."""
scores = []
for i, doc in enumerate(candidats):
response = client.chat.completions.create(
model="gpt-5.6-terra",
messages=[{
"role": "user",
"content": (
f"Sur une échelle de 0 à 10, à quel point ce document "
f"est-il pertinent pour répondre à la question ?\n\n"
f"Question : {question}\n\n"
f"Document : {doc}\n\n"
f"Réponds uniquement avec un nombre entre 0 et 10."
)
}],
temperature=0,
max_tokens=5
)
try:
score = float(response.choices[0].message.content.strip())
except ValueError:
score = 0.0
scores.append({"index": i, "texte": doc, "score": score})
scores.sort(key=lambda x: x["score"], reverse=True)
return scores[:k]
Cette version coûte un appel par candidat, soit vingt appels et autant de latences cumulées pour une seule question d’utilisateur. En pratique, c’est rédhibitoire dans une interface interactive. On demande donc au modèle de classer tous les documents d’un coup, en lui présentant chaque candidat tronqué à 200 caractères et numéroté, et en n’attendant en retour qu’une liste d’indices.
def reranker_llm_batch(
question: str,
candidats: list[str],
k: int = 5
) -> list[int]:
"""Reranking en un seul appel LLM."""
docs_formates = "\n".join(
f"[{i}] {doc[:200]}" for i, doc in enumerate(candidats)
)
response = client.chat.completions.create(
model="gpt-5.6-terra",
messages=[{
"role": "user",
"content": (
f"Classe ces documents par pertinence pour la question. "
f"Retourne les indices des {k} documents les plus "
f"pertinents, séparés par des virgules, du plus au "
f"moins pertinent.\n\n"
f"Question : {question}\n\n"
f"Documents :\n{docs_formates}\n\n"
f"Indices (ex: 3,1,7,0,5) :"
)
}],
temperature=0,
max_tokens=50
)
indices_str = response.choices[0].message.content.strip()
return [int(i.strip()) for i in indices_str.split(",")][:k]
Intégration dans le pipeline RAG
Dans un pipeline complet, le reranking s’insère entre le retrieval et la génération sans rien changer au reste. Le point à retenir dans la classe ci-dessous est le seuil de 0,3 sur relevance_score : il permet de renvoyer moins de cinq contextes quand aucun candidat n’est vraiment pertinent, plutôt que de nourrir le générateur avec du bruit. C’est souvent ce filtre, plus que le reclassement lui-même, qui fait disparaître les réponses inventées.
class PipelineRAGAvecReranking:
def __init__(self):
self.client = OpenAI()
self.co = cohere.Client()
def repondre(self, question: str, chunks, embeddings_corpus):
# 1. Retrieval large
candidats = self.rechercher(question, embeddings_corpus, k=20)
# 2. Reranking
docs_candidats = [chunks[i]["texte"] for i in candidats]
reranked = self.co.rerank(
query=question,
documents=docs_candidats,
top_n=5,
model="rerank-v3.5"
)
# 3. Sélectionner les meilleurs
contextes = [
chunks[candidats[r.index]]
for r in reranked.results
if r.relevance_score > 0.3 # Seuil de pertinence
]
# 4. Générer la réponse
return self.generer(question, contextes)
Impact du reranking
| Métrique | Sans reranking | Avec reranking | Gain |
|---|---|---|---|
| Recall@5 | 0.72 | 0.72 | = |
| Precision@5 | 0.58 | 0.78 | +34 % |
| MRR | 0.65 | 0.82 | +26 % |
Ces chiffres racontent précisément ce que fait un reranker. Le recall ne bouge pas, et c’est logique : les candidats sont les mêmes, le cross-encoder ne peut pas repêcher un document que le bi-encoder n’a pas remonté. En revanche la précision et le MRR progressent nettement, parce que le bon document passe de la sixième à la première place. Corollaire pratique : si votre recall est mauvais, le reranking ne vous sauvera pas ; il faut d’abord élargir le retrieval ou revoir le chunking.
Résumé
- Le reranking re-classe les candidats avec un modèle plus précis
- Pipeline typique : bi-encoder (top 20-50) → cross-encoder (top 5)
- Cohere rerank-v3.5 est une solution clé en main
- Un LLM peut servir de reranker si vous préférez éviter une dépendance
- Le gain en précision est significatif (20-35 % typiquement)