Reranking : améliorer la pertinence
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
La recherche par embeddings utilise un bi-encoder : la requête et les documents sont encodés séparément. C’est rapide mais imprécis car le modèle ne voit jamais la requête et le document ensemble.
Un cross-encoder (reranker) prend la paire (requête, document) en entrée et produit un score de pertinence plus fin. Il est trop lent pour chercher dans tout le corpus, mais parfait pour re-classer un petit ensemble de candidats.
Pipeline en deux étapes
Requête → Bi-encoder → Top 50 candidats → Cross-encoder → Top 5 résultats
Reranking avec Cohere
Cohere propose un reranker performant via API :
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
Vous pouvez utiliser un LLM pour le reranking, surtout si vous voulez éviter une dépendance externe :
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.3",
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]
Reranking par lots avec le LLM
Pour réduire le nombre d’appels, demandez au LLM de classer plusieurs documents en une seule requête :
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.3",
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
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 % |
Le reranking n’améliore pas le recall (les candidats sont les mêmes) mais améliore significativement la précision et le classement.
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)