Aller au contenu principal

É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