Évaluer la qualité de recherche
Objectifs
- Comprendre les métriques standard d’évaluation de la recherche
- Créer un dataset d’évaluation
- Mesurer et améliorer la qualité de votre moteur
Pourquoi évaluer ?
Sans métriques, vous ne pouvez pas savoir si un changement (nouveau modèle, paramètres de chunking, recherche hybride) améliore ou dégrade votre système.
Les métriques clés
Recall@k
Parmi les documents pertinents, combien sont dans les k premiers résultats ?
def recall_at_k(pertinents: set[str], resultats: list[str], k: int) -> float:
"""Proportion de documents pertinents trouvés dans le top-k."""
top_k = set(resultats[:k])
trouves = pertinents & top_k
return len(trouves) / len(pertinents) if pertinents else 0.0
Precision@k
Parmi les k premiers résultats, combien sont pertinents ?
def precision_at_k(pertinents: set[str], resultats: list[str], k: int) -> float:
"""Proportion de résultats pertinents dans le top-k."""
top_k = resultats[:k]
trouves = sum(1 for r in top_k if r in pertinents)
return trouves / k if k > 0 else 0.0
MRR (Mean Reciprocal Rank)
À quel rang se trouve le premier résultat pertinent ?
def mrr(pertinents: set[str], resultats: list[str]) -> float:
"""Rang réciproque du premier résultat pertinent."""
for i, r in enumerate(resultats):
if r in pertinents:
return 1.0 / (i + 1)
return 0.0
NDCG@k (Normalized Discounted Cumulative Gain)
Prend en compte non seulement la pertinence binaire mais aussi le degré de pertinence :
import numpy as np
def ndcg_at_k(relevances: list[float], k: int) -> float:
"""NDCG avec scores de pertinence gradués."""
dcg = sum(
rel / np.log2(i + 2) for i, rel in enumerate(relevances[:k])
)
ideal = sorted(relevances, reverse=True)[:k]
idcg = sum(
rel / np.log2(i + 2) for i, rel in enumerate(ideal)
)
return dcg / idcg if idcg > 0 else 0.0
Créer un dataset d’évaluation
Un dataset d’évaluation contient des paires (requête, documents pertinents) :
import json
# Structure du dataset
dataset_eval = [
{
"requete": "comment installer Python sur Windows",
"pertinents": ["doc_install_python", "doc_setup_env"],
"non_pertinents": ["doc_cuisine"]
},
{
"requete": "configurer nginx comme reverse proxy",
"pertinents": ["doc_nginx_proxy", "doc_nginx_config"],
"non_pertinents": ["doc_python_web"]
},
]
# Sauvegarder
with open("eval_dataset.json", "w") as f:
json.dump(dataset_eval, f, ensure_ascii=False, indent=2)
Générer des requêtes de test avec un LLM
Pour un gros corpus, générez automatiquement des requêtes à partir de vos documents :
from openai import OpenAI
client = OpenAI()
def generer_requetes_test(document: str, n: int = 3) -> list[str]:
"""Génère n requêtes auxquelles ce document devrait répondre."""
response = client.chat.completions.create(
model="gpt-5.3",
messages=[{
"role": "user",
"content": (
f"Voici un document :\n\n{document}\n\n"
f"Génère {n} questions auxquelles ce document répond. "
f"Retourne uniquement les questions, une par ligne."
)
}]
)
return response.choices[0].message.content.strip().split("\n")
Pipeline d’évaluation complet
class EvaluateurRecherche:
def __init__(self, moteur_recherche):
self.moteur = moteur_recherche
def evaluer(
self, dataset: list[dict], k_values: list[int] = [1, 3, 5, 10]
) -> dict:
"""Évalue le moteur sur un dataset complet."""
resultats = {f"recall@{k}": [] for k in k_values}
resultats.update({f"precision@{k}": [] for k in k_values})
resultats["mrr"] = []
for item in dataset:
# Exécuter la recherche
res = self.moteur.rechercher(item["requete"], k=max(k_values))
ids_resultats = [r["id"] for r in res]
pertinents = set(item["pertinents"])
# Calculer les métriques
resultats["mrr"].append(mrr(pertinents, ids_resultats))
for k in k_values:
resultats[f"recall@{k}"].append(
recall_at_k(pertinents, ids_resultats, k)
)
resultats[f"precision@{k}"].append(
precision_at_k(pertinents, ids_resultats, k)
)
# Moyenner
return {
metrique: round(np.mean(scores), 4)
for metrique, scores in resultats.items()
}
# Utilisation
evaluateur = EvaluateurRecherche(moteur)
metriques = evaluateur.evaluer(dataset_eval)
print("Résultats d'évaluation :")
for metrique, score in metriques.items():
print(f" {metrique}: {score}")
Comparer deux configurations
def comparer_configs(moteur_a, moteur_b, dataset, noms=("A", "B")):
"""Compare deux moteurs de recherche."""
eval_a = EvaluateurRecherche(moteur_a).evaluer(dataset)
eval_b = EvaluateurRecherche(moteur_b).evaluer(dataset)
print(f"{Métrique:<20} {noms[0]:<10} {noms[1]:<10} {Delta:<10}")
print("-" * 50)
for metrique in eval_a:
delta = eval_b[metrique] - eval_a[metrique]
signe = "+" if delta > 0 else ""
print(f"{metrique:<20} {eval_a[metrique]:<10.4f} "
f"{eval_b[metrique]:<10.4f} {signe}{delta:<10.4f}")
Résumé
- Recall@k, Precision@k, MRR et NDCG sont les métriques standard
- Créez un dataset d’évaluation avec des paires (requête, documents pertinents)
- Automatisez la génération de requêtes avec un LLM
- Comparez systématiquement les configurations avant de déployer