Stack de classement pour la recherche : BM25, embeddings et reranking

Traduction automatique Cet article a été traduit automatiquement depuis la version originale en anglais.

La recherche doit satisfaire à la fois l’intention exacte et l’intention sémantique. Une requête comme « wireless headphones » doit correspondre à ces mots, mais l’ordre final peut aussi dépendre de la qualité du produit, des préférences de l’utilisateur et de la disponibilité. Aucune méthode de classement unique ne gère correctement tous ces signaux.

Cet article construit le stack étape par étape : retrieval BM25, embeddings denses, Reciprocal Rank Fusion, reranking par cross-encoder, puis classement listwise par LLM. Un dépôt de démonstration associé contient du code exécutable pour ces étapes sur un échantillon des données de recherche de produits Amazon ESCI.

En bref : construisez la recherche comme une suite d’étapes mesurées. Commencez par BM25, ajoutez le retrieval dense lorsqu’il améliore le recall sur vos requêtes, ne fusionnez les résultats que si les deux retrievers commettent des erreurs complémentaires, et ne faites du reranking que sur le jeu de candidats compatible avec votre budget de latence. Les résultats pré-LLM présentés ci-dessous constituent un exemple détaillé obtenu sur un échantillon ESCI qui n’est pas held-out. La comparaison LLM de la démo est uniquement illustrative, car son parser ne valide pas le classement renvoyé ; elle ne prouve donc pas que tous les stacks de production ont besoin d’un LLM en ligne.

Pour un guide synthétique de sélection des étapes, consultez BM25 vs Embeddings vs Rerankers.


Choisir les étapes selon le mode d’échec

Le stack de production est un funnel, mais le funnel approprié dépend de la requête et de la surface métier.

Cas d’usageStack de départ candidatÀ valider
Recherche de produitsBM25 + retrieval dense + RRF + cross-encoderRecall des attributs, substitutions, latence, contraintes métier
Recherche documentaireRetrieval hybride + cross-encoderIdentifiants exacts, questions sémantiques, filtres de version
Déflexion du supportRetrieval hybride + vérification des citationsRecall du retrieval, grounding, abstention
Marketplace ou annoncesFiltres lexicaux + retrieval dense + reranker métierDisponibilité, fraîcheur, conformité, diversité des vendeurs
Petit corpus interneBaseline BM25, puis un rerankerSi le décalage de vocabulaire justifie un index dense
Recherche juridique ou médicale à forts enjeuxRetrieval axé recall et revue experteCouverture, provenance, abstention calibrée

Commencez par BM25 comme baseline. Ajoutez le retrieval dense lorsque le décalage de vocabulaire dégrade le recall. Ajoutez un cross-encoder lorsque la première page contient les bons candidats, mais dans le mauvais ordre. N’ajoutez un LLM qu’une fois la latence acceptable et les décisions de classement évaluables.

Comment en sommes-nous arrivés là ?

Le stack est plus facile à comprendre comme trois couches. Le retrieval lexical trouve les termes exacts, le retrieval dense comble les écarts de vocabulaire et les rerankers comparent en détail les meilleurs candidats.

BM25 et retrieval lexical

Pendant des décennies, BM25 a été la méthode par défaut. Il s’agit d’un modèle probabiliste qui attribue un score aux documents selon la fréquence des termes de la requête dans le document, normalisée par la longueur du document et l’inverse de la fréquence documentaire (IDF).

BM25 est performant lorsque les termes littéraux portent l’intention : codes d’erreur, SKU de produits, noms et identifiants d’API. Sa principale limite est le décalage de vocabulaire. Une requête comme « cheap laptop » peut ne pas retrouver un document parlant d’un « budget notebook computer » si le texte indexé ne fournit aucun lien entre ces expressions.

Cela dit, BM25 constitue une baseline solide. Le classement BEIR rapporte un nDCG@10 moyen de 0.429 sur 18 datasets pour son exécution BM25 multifield. La reproduction de Pyserini utilise un index multifield Lucene avec contents=1.0 et title=1.0, interrogé avec --bm25. Il ne s’agit pas de l’implémentation rank_bm25 à tokenisation par espaces utilisée dans cette démo. BM25 surpasse également certains modèles neuronaux sur des tâches de retrieval argumentatif comme Touche-2020.

Retrieval dense et embeddings

Les encodeurs de type BERT ont rendu le retrieval dense pratique. Ils projettent les requêtes et les documents dans un espace vectoriel partagé, puis classent les candidats avec une fonction de similarité telle que la similarité cosinus ou le produit scalaire.

L’architecture bi-encoder (ou « two-tower ») traite indépendamment la requête et le document au moyen de deux tours d’encodeur distincts, et produit des embeddings de longueur fixe. Les vecteurs des documents peuvent être pré-calculés et indexés offline, puis retrouvés rapidement grâce à des algorithmes Approximate Nearest Neighbor (ANN). « cheap laptop » et « budget notebook » se retrouvent alors proches dans l’espace vectoriel.

Certains bi-encoders utilisent une architecture Siamese, comme Sentence-BERT, où les deux côtés partagent leurs poids. D’autres utilisent des tours distincts pour les requêtes et les documents. Le pooling, la taille des vecteurs, la fonction de similarité et l’objectif d’entraînement sont des choix de modèle, pas des propriétés de tous les retrievers denses.

Ces modèles sont entraînés par apprentissage contrastif, généralement avec la loss InfoNCE. Étant donné un batch de paires (query, positive_document), l’objectif maximise sim(query, positive_doc) tout en minimisant sim(query, negative_docs). Les négatifs proviennent des positifs d’autres requêtes du même batch (négatifs in-batch). Un paramètre de température τ\tau contrôle la netteté avec laquelle le modèle doit séparer les deux.

Les données d’entraînement comptent souvent davantage que la dimension des embeddings. Les modèles de retrieval apprennent à partir de paires query-positive et de hard negatives soigneusement sélectionnés : des documents plausibles, mais non pertinents. La section consacrée à l’entraînement montre ensuite comment SimANS évite à la fois les négatifs triviaux et les faux négatifs probables.

Le coût à payer est le goulot d’étranglement de la représentation. Les bi-encoders compressent toute la nuance sémantique dans un vecteur unique de taille fixe ; ils manquent donc souvent les interactions fines entre certains termes de la requête et certains éléments du document.

Cross-encoders et LLMs

Les cross-encoders (Nogueira & Cho, 2019) transmettent la requête et le document ensemble à un Transformer, sous la forme d’une séquence concaténée ([CLS] Query [SEP] Document), de sorte que chaque token de la requête puisse porter son attention sur chaque token du document via une self-attention complète. Cette interaction profonde capture des nuances que l’encodage indépendant ne détecte pas.

Le reranking par LLM utilise un modèle guidé par un prompt pour comparer plusieurs candidats simultanément. RankGPT a montré de solides résultats listwise zero-shot avec GPT-4 sur les benchmarks évalués, mais la stabilité des sorties, le coût et l’adéquation au domaine doivent encore être testés séparément.

Ces scores ne peuvent pas être pré-calculés pour des requêtes arbitraires ; le reranking intervient donc après le retrieval. Cette asymétrie des coûts motive le funnel multi-étapes.


Le funnel multi-étapes

Exécuter un cross-encoder ou un LLM coûteux sur des millions de documents n’est pas viable ; les stacks de recherche modernes utilisent donc un funnel. Chaque étape réduit le pool de candidats tandis que la complexité du modèle augmente.

Un retriever peu coûteux manque également de précision finale : le funnel utilise donc chaque modèle uniquement là où son coût reste raisonnable.

Funnel de classement multi-étapesFunnel de classement multi-étapes

ÉtapeÉchelle d’entréeObjectif principalMéthodes typiquesMesure de sortie
RetrievalCorpus ou indexRecall des candidatsBM25, bi-encodersRecall au cutoff des candidats
Pre-rankingGrand jeu de candidatsFiltrage peu coûteuxModèles légers, règlesRecall conservé par milliseconde
Full rankingShortlistQualité du top du classementCross-encoders, LLMsNDCG/MRR, latence, coût
BlendingListes classées ou slots finauxContraintes et mélangeRègles, classement multi-objectifsPolitique, diversité, garde-fous métier

Le retrieval définit le plafond et le reranking optimise à l’intérieur de ce plafond. Si un document pertinent ne survit pas au retrieval, aucun modèle en aval ne peut le récupérer.


La démo : un pipeline en cinq étapes

Pour rendre cela concret, j’ai construit une démo search-ranking-stack qui exécute un pipeline en cinq étapes sur le benchmark de recherche de produits Amazon ESCI. Chaque étape est mesurée indépendamment afin d’identifier précisément l’origine des gains.

Architecture du pipeline de la démoArchitecture du pipeline de la démo

Le pipeline :

  1. Retrieval BM25 sparse — baseline lexicale (rank_bm25)
  2. Retrieval par bi-encoder dense — génération de candidats sémantiques (all-MiniLM-L6-v2)
  3. Fusion hybride RRF — fusion fondée sur le rang des résultats sparse et denses
  4. Reranking par cross-encoder — scores de pertinence pairwise (ms-marco-MiniLM-L-12-v2)
  5. Reranking listwise par LLM — comparaison guidée par prompt de la shortlist finale (Ollama, API ou modèle local)

Les étapes 1 à 3 constituent l’étape de retrieval du funnel (maximiser le recall) ; les étapes 4 et 5 constituent l’étape de full ranking (maximiser la précision). La démo ignore le pre-ranking et le blending. Avec environ 8 500 documents, on peut envoyer tous les résultats hybrides directement au reranking.

Démarrage rapide

git clone https://github.com/slavadubrov/search-ranking-stack.git
cd search-ranking-stack
uv sync

# Download and sample ESCI dataset (~2.5GB download, ~5MB sample)
uv run download-data

# Run the full pipeline (without LLM reranking)
uv run run-all

# Run with LLM reranking via Ollama
uv run run-all --llm-mode ollama

Dataset et échantillonnage : Amazon ESCI

La démo utilise l’Amazon Shopping Queries Dataset (ESCI) de la KDD Cup 2022, un benchmark réel de recherche de produits avec quatre niveaux de labels de pertinence :

LabelGainSignificationExemple (requête : « wireless headphones »)
Exact (E)3Satisfait toutes les exigencesSony WH-1000XM5 Wireless Headphones
Substitute (S)2Alternative fonctionnelleCasque filaire avec adaptateur Bluetooth
Complement (C)1Article connexe et utileÉtui de transport pour casque
Irrelevant (I)0Relation non significativeCâble de charge USB

La pertinence graduée est importante, car elle permet d’utiliser NDCG (Normalized Discounted Cumulative Gain), qui distingue un classement « parfait » d’un classement « simplement acceptable ». Les métriques binaires leur attribuent le même score.

J’ai utilisé l’échantillon small_version de la démo : environ 500 requêtes, 8 500 produits et 12 000 jugements. Son downloader lit l’unique split train de tasksource/esci, filtre la locale anglaise us et small_version == 1, puis utilise la seed 42 pour échantillonner jusqu’à 500 identifiants de requête uniques sans remise. Le corpus contient les produits uniques issus de ces lignes de jugement sélectionnées. Il ne s’agit ni d’un split held-out ni d’un corpus indépendant. L’échantillon est suffisamment petit pour s’exécuter sur un laptop, mais trop réduit et trop spécifique au domaine pour établir un classement de production. Utilisez-le pour reproduire les étapes et examiner les modes d’échec ; utilisez des requêtes held-out représentatives pour prendre des décisions de déploiement.


Retrieval : recherche hybride

Le rôle de la couche de retrieval est de maximiser le recall : faire entrer autant de documents pertinents que possible dans le jeu de candidats.

BM25 : la baseline lexicale

BM25 attribue un score aux documents selon le recouvrement des termes avec la requête, avec saturation de la fréquence des termes et normalisation de la longueur du document :

BM25(q,d)=tqIDF(t)tf(t,d)(k1+1)tf(t,d)+k1(1b+bd/avgdl)\text{BM25}(q, d) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{tf(t,d) \cdot (k_1 + 1)}{tf(t,d) + k_1 \cdot (1 - b + b \cdot |d|/\text{avgdl})}

IDF(t)\text{IDF}(t) est l’inverse de la fréquence documentaire du terme tt, tf(t,d)tf(t,d) est la fréquence du terme dans le document dd, d|d| est la longueur du document et avgdl\text{avgdl} la longueur moyenne des documents du corpus. Deux paramètres sont importants : k1k_1 (généralement compris entre 1,2 et 2,0) contrôle la saturation TF — la vitesse à laquelle les répétitions d’un terme cessent d’apporter de la valeur — et bb (généralement 0,75) contrôle la normalisation de la longueur des documents.

L’implémentation est courte. Tokenisation simple par espaces avec rank_bm25 :

# src/search_ranking_stack/stages/s01_bm25.py

from rank_bm25 import BM25Okapi

def run_bm25(data: ESCIData, top_k: int = 100):
    doc_ids = list(data.corpus.keys())
    tokenized_corpus = [text.lower().split() for text in data.corpus.values()]

    bm25 = BM25Okapi(tokenized_corpus)

    results = {}
    for query_id, query_text in data.queries.items():
        scores = bm25.get_scores(query_text.lower().split())
        top_indices = np.argsort(scores)[::-1][:top_k]
        results[query_id] = {doc_ids[idx]: float(scores[idx]) for idx in top_indices}

    return results

BM25 atteint un Recall@100 de 0.741 : 74 % des produits pertinents apparaissent quelque part dans le top 100. Ce n’est pas mauvais pour une méthode purement lexicale, mais 26 % des éléments pertinents sont invisibles pour toutes les étapes suivantes.

Retrieval dense par bi-encoder

Le bi-encoder projette indépendamment les requêtes et les documents dans un espace d’embeddings partagé :

# src/search_ranking_stack/stages/s02_dense.py

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

# Encode corpus once, cache to disk
corpus_embeddings = model.encode(
    doc_texts,
    batch_size=128,
    normalize_embeddings=True,  # Cosine sim = dot product
    convert_to_numpy=True,
)

# At query time: encode query, compute dot product
query_embeddings = model.encode(query_texts, normalize_embeddings=True)
similarity_matrix = np.dot(query_embeddings, corpus_embeddings.T)

Avec des embeddings normalisés, la similarité cosinus se réduit à un produit scalaire. La démo calcule la matrice complète requêtes-par-corpus, car 8 500 documents tiennent facilement en mémoire ; un corpus de production utiliserait normalement un index approximate-nearest-neighbor. Dans cet échantillon, all-MiniLM-L6-v2 fait passer le Recall@100 de 0.741 à 0.825.

Comment les bi-encoders apprennent de bonnes représentations

L’entraînement des bi-encoders se déroule généralement en deux phases. Premièrement, le modèle est pré-entraîné sur des datasets de Natural Language Inference (NLI) et de Semantic Textual Similarity (STS), qui lui enseignent une compréhension sémantique généraliste. C’est à ce moment qu’il apprend que « a cat sits on a mat » et « a feline rests on a rug » doivent produire des embeddings similaires. Deuxièmement, il est soumis à un fine-tuning sur des données spécifiques au retrieval, comme MS MARCO, où il apprend qu’une requête et son passage pertinent doivent être plus proches que la requête et les passages non pertinents.

La seconde phase dépend du hard negative mining. Les négatifs aléatoires, par exemple un document sur la cuisine associé à une requête sur les casques audio, sont trop faciles à distinguer ; le modèle en apprend donc peu. À la place, on utilise le modèle courant lui-même pour trouver des documents qu’il classe haut, mais qui ne sont en réalité pas pertinents.

L’approche SimANS (Simple Ambiguous Negatives Sampling) formalise cette méthode. On classe tous les documents avec le bi-encoder courant, puis on exclut les négatifs faciles, classés trop bas pour fournir un signal d’apprentissage, ainsi que les faux négatifs potentiels, classés si haut qu’ils pourraient être pertinents mais non annotés. Ce qui reste au milieu porte le signal d’entraînement le plus important.

# What a training triplet looks like after hard negative mining
training_triplet = {
    "query": "wireless noise canceling headphones",
    "positive": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
    "negative": "Sony headphone replacement ear pads",  # Hard negative: same brand, related product, but wrong intent
}
# The bi-encoder must learn that "ear pads" is NOT what the user wants,
# even though it shares many tokens with the positive document.

La fonction de loss contrastive (InfoNCE) relie ces éléments. Pour chaque requête qq avec un document positif d+d^+ et un ensemble de documents négatifs {d1,,dn}\{d^-_1, \ldots, d^-_n\} :

L=logesim(q,d+)/τesim(q,d+)/τ+i=1nesim(q,di)/τ\mathcal{L} = -\log \frac{e^{\text{sim}(q, d^+) / \tau}}{e^{\text{sim}(q, d^+) / \tau} + \sum_{i=1}^{n} e^{\text{sim}(q, d^-_i) / \tau}}

sim(q,d)\text{sim}(q, d) est la similarité cosinus entre les embeddings de la requête et du document, et τ\tau le paramètre de température (généralement compris entre 0,05 et 0,1). Des valeurs plus faibles rendent la loss plus sensible aux hard negatives. Il s’agit essentiellement d’une softmax cross-entropy : augmenter la similarité de la paire positive par rapport à celle de tous les négatifs. Lorsque τ\tau est faible, même de légères différences de similarité produisent des gradients importants, ce qui force le modèle à établir des distinctions plus fines.

Pipeline d’entraînement d’un bi-encoderPipeline d’entraînement d’un bi-encoder

Servir des embeddings de bi-encoder à grande échelle

L’avantage architectural d’un bi-encoder est la séparation offline/online. Les embeddings des documents sont calculés au moment de l’indexation et stockés dans un index vectoriel. Au moment de la requête, le système encode la requête et recherche dans ces vecteurs stockés. La latence dépend de l’encodeur, du matériel, de l’index, des filtres et de la cible de recall ; profilez donc séparément ces deux étapes.

Dans la démo, les calculs restent modestes : 8 500 documents ×\times 384 dimensions ×\times 4 octets par float = environ 13 Mo d’embeddings. À l’échelle de la production, les chiffres changent de nature : 1 milliard de documents avec des embeddings de 768 dimensions nécessitent environ 3 Tio de stockage. C’est là qu’interviennent la quantification (compression des floats 32 bits en entiers 8 bits), la product quantization (décomposition des vecteurs en sous-espaces) et les index adossés à des SSD comme DiskANN. La section consacrée à l’indexation des vecteurs denses couvre les algorithmes d’indexation.

Pipeline de serving d’un bi-encoderPipeline de serving d’un bi-encoder

Pourquoi tester le retrieval hybride

Les deux méthodes échouent souvent de manière différente. BM25 est bien adapté aux noms propres, aux SKU de produits et aux codes d’erreur. Le retrieval dense peut récupérer des cas de décalage de vocabulaire, comme « cheap laptop » contre « budget notebook computer ». L’intérêt de la fusion dépend de la fréquence de ces cas complémentaires dans le jeu de requêtes cible.

Une expérience courante consiste à tester la recherche hybride : exécuter les deux méthodes de retrieval, puis fusionner leurs listes classées.

Reciprocal Rank Fusion (RRF)

BM25 et le retrieval dense produisent des scores dont la signification et l’échelle diffèrent. Une combinaison linéaire nécessite donc calibration et validation chaque fois que les retrievers ou le corpus changent.

Recherche hybride avec RRFRecherche hybride avec RRF

Reciprocal Rank Fusion (Cormack et al., 2009) ignore complètement les scores bruts et utilise uniquement la position dans le classement :

RRF(d)=rRankings1k+rank(d,r)\text{RRF}(d) = \sum_{r \in \text{Rankings}} \frac{1}{k + \text{rank}(d, r)}

Ici, kk est une constante de lissage ; 60 constitue une valeur de départ courante. RRF favorise les éléments classés près du sommet dans plusieurs listes d’entrée sans comparer leurs scores bruts. Il évite la calibration de l’échelle des scores, mais les cutoffs de retrieval, les poids et kk doivent toujours être évalués.

L’implémentation :

# src/search_ranking_stack/stages/s03_hybrid_rrf.py

def reciprocal_rank_fusion(ranked_lists, k=60, top_k=100):
    fused_results = {}

    for query_id in all_query_ids:
        rrf_scores = defaultdict(float)

        for results in ranked_lists:
            sorted_docs = sorted(results[query_id].items(),
                                 key=lambda x: x[1], reverse=True)

            for rank, (doc_id, _score) in enumerate(sorted_docs, start=1):
                rrf_scores[doc_id] += 1.0 / (k + rank)

        sorted_rrf = sorted(rrf_scores.items(),
                            key=lambda x: x[1], reverse=True)[:top_k]
        fused_results[query_id] = dict(sorted_rrf)

    return fused_results

Le RRF hybride atteint un Recall@100 de 0.842 et un NDCG@10 de 0.628, dépassant BM25 (0.585) et Dense (0.611) utilisés seuls. Il suffit qu’un document soit bien classé par une méthode pour survivre à la fusion.


Reranking par cross-encoder

Avec 100 candidats hybrides par requête, on peut se permettre un modèle plus coûteux. Le cross-encoder traite la requête et le document ensemble au moyen d’un Transformer unique, avec une cross-attention complète entre tous les tokens.

Bi-encoder contre cross-encoderBi-encoder contre cross-encoder

Interaction au niveau des tokens

La différence se situe dans la matrice d’attention. Dans un bi-encoder, l’attention est diagonale par blocs : les tokens de la requête ne portent leur attention que sur les autres tokens de la requête, et les tokens du document uniquement sur ceux du document. Les deux représentations ne se rencontrent jamais au niveau des tokens ; elles ne se croisent qu’à la fin via un produit scalaire. Un cross-encoder calcule la matrice d’attention complète, où chaque token de la requête porte son attention sur chaque token du document, et inversement. Cette cross-attention rend possible une interaction profonde au niveau des tokens.

Architecture d’attention d’un cross-encoderArchitecture d’attention d’un cross-encoder

Dans un bi-encoder, la requête « apple » est encodée avant l’observation de tout document. Un cross-encoder voit la requête et le candidat ensemble, et peut donc exploiter leur relation au niveau des tokens. Cela peut aider dans des cas tels que :

  • Négation : « headphones that are not wireless ». Un embedding poolé peut sous-pondérer la négation, tandis que l’encodage conjoint fournit au modèle une interaction directe entre la requête et le document. Il s’agit d’une hypothèse à vérifier sur un échantillon ciblé, pas d’une garantie.
  • Qualification : « laptop under $500 ». L’encodage conjoint peut relier la contrainte à un prix présent dans le texte du produit, même si des filtres structurés sur le prix sont plus sûrs lorsque le champ est disponible.

L’entrée du cross-encoder est formatée comme [CLS] query tokens [SEP] document tokens [SEP]. [CLS] est un token de classification dont l’état caché final passe par une tête linéaire pour produire un score de pertinence unique. Les embeddings de segment distinguent les tokens de la requête de ceux du document, et [SEP] marque la frontière entre les segments.

Comment entraîner les cross-encoders

Les cross-encoders peuvent apprendre à partir d’exemples (query, document, relevance_label) avec des objectifs pointwise, pairwise ou listwise. L’exemple pointwise ci-dessous utilise un seul label de pertinence ; ce n’est pas le seul design d’entraînement possible.

# Cross-encoder training data format
training_example = {
    "query": "wireless headphones",
    "document": "Sony WH-1000XM5 Wireless Headphones",
    "label": 1.0,  # Relevant
}
# Forward pass: [CLS] hidden state → Linear layer → sigmoid → score
# Loss: binary cross-entropy between predicted score and label

Un classifieur courant projette la représentation [CLS] finale en un score. Les labels binaires peuvent utiliser une binary cross-entropy ; la pertinence graduée peut utiliser des losses de régression, ordinales, pairwise ou listwise. Choisissez en fonction de métriques de classement held-out, plutôt que de supposer qu’un objectif est universellement supérieur.

Le hard negative mining est encore plus important pour les cross-encoders que pour les bi-encoders. Les cross-encoders sont coûteux à entraîner : chaque exemple nécessite une passe forward complète sur la séquence concaténée. Il est donc inutile de gaspiller du calcul sur des négatifs triviaux. La recette pratique consiste à utiliser un bi-encoder pour récupérer les candidats top-K de chaque requête d’entraînement, puis à extraire les hard negatives de plages de rangs spécifiques (par exemple, les rangs 10 à 100). On obtient ainsi des exemples où distinguer le pertinent du non-pertinent exige réellement une interaction profonde au niveau des tokens.

# src/search_ranking_stack/stages/s04_cross_encoder.py

from sentence_transformers import CrossEncoder

model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2")

def run_cross_encoder(data, hybrid_results, top_k_rerank=50):
    reranked_results = {}

    for query_id, query_text in data.queries.items():
        candidates = list(hybrid_results[query_id].items())[:top_k_rerank]

        # Form (query, document) pairs for joint encoding
        pairs = []
        doc_ids = []
        for doc_id, _ in candidates:
            doc_text = data.corpus.get(doc_id, "")[:2048]
            pairs.append([query_text, doc_text])
            doc_ids.append(doc_id)

        # Score all pairs with full cross-attention
        scores = model.predict(pairs, batch_size=64)

        # Rerank by cross-encoder score
        scored_docs = sorted(zip(doc_ids, scores),
                             key=lambda x: x[1], reverse=True)
        reranked = {
            doc_id: float(score) for doc_id, score in scored_docs
        }

        # Keep original hybrid candidates at ranks 51--100 for Recall@100.
        for doc_id, score in list(hybrid_results[query_id].items())[top_k_rerank:100]:
            if doc_id not in reranked:
                reranked[doc_id] = float(score) * 0.01

        reranked_results[query_id] = reranked

    return reranked_results

Lors de l’exécution enregistrée de la démo, ms-marco-MiniLM-L-12-v2 rerank 50 candidats par requête et fait passer le NDCG@10 de 0.628 à 0.645. Mesurez sa latence sur le matériel de déploiement ; le modèle, la longueur des séquences, la taille des batchs et le runtime influencent tous le résultat.

Le compromis vitesse-qualité

Pourquoi ne pas utiliser des cross-encoders partout ? Parce qu’il est impossible de pré-calculer les scores. Les embeddings de documents d’un bi-encoder sont indépendants de la requête : on les calcule une fois et on les stocke. La sortie d’un cross-encoder dépend à la fois de la requête et du document. Le score de pertinence de « wireless headphones » associé à un produit Sony provient de la cross-attention complète entre ces tokens précis. Il est impossible de le mettre en cache ou de le réutiliser pour une autre requête.

Un bi-encoder nécessite un encodage de la requête, puis une recherche vectorielle sur des embeddings de documents pré-calculés. Un cross-encoder évalue chaque paire requête-document de la shortlist, avec un coût qui augmente avec le nombre de candidats et la longueur des séquences. Le batching aide, mais scorer 100 000 candidats reste un mauvais point de fonctionnement ; faites d’abord le retrieval et benchmarkez la shortlist la plus grande compatible avec vos objectifs de qualité et de latence.

Dans la démo, le Recall@100 reste stable à 0.842 pendant l’étape du cross-encoder. Le reranking peut réordonner les résultats, mais pas ajouter de documents. Le retrieval définit le plafond.


Reranking listwise par LLM

La dernière étape de la démo utilise un LLM pour effectuer un reranking listwise. Au lieu de scorer chaque document indépendamment, le modèle observe les 10 premiers et renvoie un ordre. Inspiré de RankGPT, le prompt explicite la comparaison relative, mais introduit aussi des limites de contexte, un biais de position, des échecs de parsing et une variance d’une exécution à l’autre.

Approches de reranking par LLMApproches de reranking par LLM

Le prompt listwise

Le template de prompt demande au LLM de prendre en compte la hiérarchie de pertinence ESCI :

# src/search_ranking_stack/stages/s05_llm_rerank.py

def _create_listwise_prompt(query, documents, max_words=200):
    n = len(documents)

    doc_texts = []
    for i, (doc_id, doc_text) in enumerate(documents, start=1):
        words = doc_text.split()[:max_words]
        doc_texts.append(f"[{i}] {' '.join(words)}")

    return (
        f"I will provide you with {n} product listings, each indicated by "
        f"a numerical identifier [1] to [{n}]. Rank the products based on "
        f'their relevance to the search query: "{query}"\n\n'
        "Consider:\n"
        "- Exact matches should rank highest\n"
        "- Substitutes should rank above complements\n"
        "- Irrelevant products should rank lowest\n\n"
        f"{chr(10).join(doc_texts)}\n\n"
        "Output ONLY a comma-separated list of identifiers: [3], [1], [2], ...\n"
        "Do not explain your reasoning."
    )

Trois modes d’exécution

La démo prend en charge trois backends pour le reranking par LLM :

ModeModèleMode d’exécution
ollamallama3.2:3b (configurable)Local via l’API Ollama
apiclaude-haiku-4-5-20251001API Anthropic
localQwen/Qwen2.5-1.5B-InstructHuggingFace Transformers

Parsing et fallback

Les sorties des LLMs ne respectent pas nécessairement le schema demandé ; le parsing et le chemin de fallback sont donc importants :

def _parse_ranking(output: str, n: int) -> list[int] | None:
    """Parse LLM output to extract ranking order."""
    matches = re.findall(r"\[(\d+)\]", output)

    if not matches:
        return None

    positions = [int(m) - 1 for m in matches]

    # Pad with remaining positions if LLM returned partial output
    if len(positions) < n:
        seen = set(positions)
        for i in range(n):
            if i not in seen:
                positions.append(i)

    return positions[:n]

Si le parsing échoue complètement, la démo revient à l’ordre produit par le cross-encoder. En production, un parser devrait également rejeter les identifiants hors plage et les doublons, ajouter les candidats omis dans leur ordre précédent, journaliser l’échec et comparer le taux de fallback à un seuil de mise en production.


Résultats sur cet échantillon ESCI

Ces résultats pré-LLM enregistrés utilisent le small_version de la démo : la locale anglaise us après le filtre small_version == 1, avec jusqu’à 500 identifiants de requête échantillonnés sans remise à l’aide de la seed 42, et un corpus construit à partir de leurs produits jugés. Il ne s’agit ni d’une évaluation held-out ni d’un corpus indépendant. Les valeurs MRR utilisent la règle binaire relevance > 0 de l’évaluateur de la démo : un résultat Complement, Substitute ou Exact est considéré comme pertinent, tandis qu’un résultat Irrelevant ne l’est pas. recip_rank évalue chaque liste de candidats renvoyée, qui contient jusqu’à 100 candidats ; il ne s’agit pas de MRR@10.

ÉtapeNDCG@10MRRRecall@100Delta NDCG
BM250.5850.8120.741
Bi-Encoder dense0.6110.8080.825+0.026
Hybride (RRF)0.6280.8340.842+0.017
+ Cross-Encoder0.6450.8600.842+0.017

Observations clés

La recherche hybride surpasse chaque méthode utilisée seule. Le NDCG du RRF (0.628) dépasse celui de BM25 (0.585) et du modèle Dense (0.611). Les deux méthodes peuvent échouer sur des requêtes différentes ; leur combinaison permet donc de récupérer des documents que l’une ou l’autre aurait manqués seule.

Le recall est fixé par le retrieval. Le Recall@100 reste stable à 0.842 jusqu’à l’étape du cross-encoder. Les rerankers réordonnent les résultats, mais n’ajoutent pas de documents. Pour augmenter le recall, corrigez la couche de retrieval. Les valeurs MRR ci-dessus utilisent la même règle que l’ensemble de la démo : Complement ou supérieur est considéré comme pertinent.

Le résultat du LLM n’est pas reporté. Le parser accepte les identifiants en double et hors plage, puis son chemin de padding peut modifier silencieusement la liste de candidats. La comparaison LLM précédente est donc illustrative, et non un résultat auditable. Relancez-la avec une validation stricte des identifiants, un comportement de fallback journalisé, des exécutions répétées, des mesures de latence et de coût, ainsi qu’un jeu de données de domaine held-out avant d’attribuer un quelconque gain au reranker.

Le dépôt associé est utile pour l’installation et le code, mais son README indique encore le NDCG@10 invalide + LLM Reranker de 0.717. Considérez le tableau pré-LLM de cet article et la mise en garde sur le LLM ci-dessus comme la référence faisant foi jusqu’à ce que le dépôt propose une évaluation LLM auditable.

Le retrieval dense surpasse BM25 sur cet échantillon. Examinez des sous-ensembles de requêtes avant d’en attribuer la cause. Le décalage de vocabulaire est une explication plausible, mais la construction de l’échantillon, le tokenizer, le domaine d’entraînement du modèle et les champs du corpus influencent également la comparaison.


Évaluation : mesurer ce qui compte

La démo utilise trois métriques, chacune examinant le classement sous un angle différent :

NDCG@10 (métrique principale)

Le Normalized Discounted Cumulative Gain mesure la qualité du classement dans le top 10 à l’aide de la pertinence graduée. Il récompense le placement des documents très pertinents près du sommet, avec une décote logarithmique :

DCG@k=i=1k2reli1log2(i+1)NDCG@k=DCG@kIDCG@k\text{DCG@k} = \sum_{i=1}^{k} \frac{2^{rel_i} - 1}{\log_2(i + 1)} \qquad \text{NDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}}

Parmi les trois métriques, le NDCG est la seule qui exploite pleinement les quatre niveaux de pertinence d’ESCI. Un système qui place une correspondance Exact en position 1 obtient un score supérieur à celui qui y place une correspondance Substitute. C’est pourquoi il s’agit de la métrique principale ici.

MRR (premier résultat Complement ou supérieur)

Le Mean Reciprocal Rank utilise la position du premier résultat considéré comme pertinent. Dans cette démo, l’évaluateur transmet les qrels gradués d’ESCI à pytrec_eval et applique recip_rank à chaque liste renvoyée, qui contient jusqu’à 100 candidats ; relevance > 0 constitue donc le seuil binaire : Complement (1), Substitute (2) et Exact (3) sont considérés comme pertinents. Irrelevant (0) ne l’est pas. Un résultat pertinent en position 1 donne un rang réciproque de 1,0 ; en position 3, il donne 0,333. Il s’agit du MRR calculé sur les listes de candidats renvoyées, et non du MRR@10.

Recall@100 (couverture du retrieval)

Le recall mesure la fraction des documents jugés pertinents qui apparaissent dans le top 100. Il s’agit d’une métrique du plafond de candidats pour les jugements évalués : un reranker ne peut pas ajouter un document omis par le retrieval, tandis que des jugements incomplets peuvent rendre ce plafond apparent incertain.


Indexer des vecteurs denses au-delà de la démo

Les embeddings denses ne deviennent vraiment utiles à grande échelle qu’avec un index Approximate Nearest Neighbor (ANN). La démo utilise une similarité cosinus brute, ce qui convient pour environ 8 500 documents, mais les systèmes de production nécessitent des index spécialisés.

HNSW (hierarchical navigable small world)

HNSW construit un graphe multicouche : les couches supérieures, peu denses, permettent une navigation globale, tandis que les couches inférieures, plus denses, affinent le voisinage. M contrôle la connectivité du graphe, tandis que efSearch échange le coût de recherche contre le recall. Les valeurs utiles dépendent de la dimension, de la distribution des distances, des filtres, de l’implémentation et du recall cible.

Les mises à jour et suppressions constituent un point opérationnel important, car les index de graphes peuvent nécessiter une réparation en arrière-plan. Le comportement varie selon la base de données. Un ticket Qdrant, par exemple, rapporte une dégradation de la qualité de la recherche filtrée dans une configuration HNSW multi-tenant. Le rapport mesure une précision moyenne@100 de 0.597 ± 0.0541 avec un filtre library_id, tandis que la recherche exacte atteint 100 % de recall dans son analyse approfondie. La modification de payload_m a forcé une reconstruction qui a restauré la qualité de la recherche filtrée. Le rapport traite des filtres de tenant, de la configuration HNSW et d’une reconstruction forcée de l’index HNSW, et non d’une charge de travail fortement axée sur les suppressions. Reproduisez le profil de churn cible et incluez le comportement de la compaction ou de la reconstruction dans l’évaluation.

IVF (inverted file)

Les index IVF partitionnent l’espace vectoriel en clusters, puis parcourent les nprobe clusters les plus proches de la requête. Ils peuvent offrir un compromis intéressant entre mémoire, temps de construction et recall, notamment lorsqu’ils sont associés à de la compression. La sémantique des mises à jour et les performances dépendent de l’implémentation, pas uniquement de la famille d’index.

À une échelle extrême, IVF_RaBitQ (Gao & Long, SIGMOD 2024) compresse les vecteurs à virgule flottante en représentations d’un seul bit. Dans un espace de grande dimension, le signe (+/-) d’une coordonnée contient suffisamment d’information angulaire pour calculer la similarité.

DimensionGraphe HNSWClusters IVF
Contrôle de la requêteefSearchnprobe
Contrôle de la constructionConnectivité et beam de constructionNombre de clusters et échantillon d’entraînement
Profil mémoireArêtes du graphe et vecteursCentroïdes, listes et vecteurs stockés
Comportement des mises à jourRéparation/nettoyage propre à la baseMaintenance des listes propre à la base
À évaluer avecCourbe recall-latence-mémoire-churnCourbe recall-latence-mémoire-churn

Dans une étude de cas Uber sur la recherche de livraisons, la réduction d’un paramètre de recherche au niveau du shard, de 1 200 à 200, a diminué la latence rapportée de 34 % et le CPU de 17 %, avec une faible perte de recall mesurée. La leçon réutilisable consiste à ajuster la courbe recall-coût sur un trafic représentatif de la production, et non à recopier la valeur 200.


Extensions facultatives après le pipeline principal

Une fois que le retrieval et le reranking disposent de mesures séparées, plusieurs extensions deviennent plus faciles à évaluer sans masquer le pipeline principal.

Compréhension des requêtes

L’expansion et la réécriture des requêtes peuvent traiter le décalage de vocabulaire avant le retrieval. Query2doc génère des pseudo-documents et rapporte des gains BM25 dans ses expériences MS MARCO. L’expansion peut aussi introduire une intention incorrecte ; comparez donc recall et précision sur des sous-ensembles de requêtes ambiguës, navigationnelles et contenant des identifiants exacts.

Patterns pratiques : expansion d’abréviations, enrichissement d’entités, décomposition en sous-requêtes pour le raisonnement multi-hop et RAG-Fusion — génération de plusieurs variantes de requête et combinaison des résultats via RRF.

Annotation de pertinence assistée par LLM

Les LLMs peuvent produire des labels de pertinence préliminaires lorsque les jugements humains sont rares. TALEC et les travaux de Pinterest sur l’annotation de pertinence présentent deux designs évalués. Un label produit par un LLM reste une sortie de modèle : calibrez-le avec des jugements humains en aveugle, examinez les sous-ensembles de désaccord et conservez un gold set humain pour les tests de régression.

Les contrôles utiles comprennent :

  • une grille d’évaluation avec des frontières de pertinence et des exemples concrets ;
  • une calibration humaine en aveugle et des vérifications périodiques ;
  • la randomisation de l’ordre et des jugements répétés pour les cas instables ;
  • des panels de modèles lorsque leur coût supplémentaire améliore l’accord ; et
  • des contrôles explicites des biais de position et de tendance centrale.

Distillation des connaissances

Lorsqu’un enseignant LLM apporte de la valeur sans pouvoir respecter les contraintes de serving, la distillation constitue une option :

  1. Utiliser un LLM puissant (l’enseignant) pour rerank des milliers de requêtes d’entraînement
  2. Entraîner un cross-encoder petit et rapide (l’étudiant, environ 100 à 200 millions de paramètres) pour imiter la distribution de classement du LLM
  3. Comparer l’étudiant à l’enseignant et à la baseline en termes de qualité, calibration et coût de serving

InRanker distille MonoT5-3B en modèles de 60M et 220M paramètres, soit une réduction de taille de 50x avec des performances compétitives. L’approche Rank-Without-GPT produit des rerankers listwise open source de 7B qui atteignent 97 % de l’efficacité de GPT-4 grâce au fine-tuning QLoRA.

Les résultats de compression publiés sont des points de départ, et non des ratios attendus en production. La distillation peut hériter des biais de l’enseignant et perdre en qualité sur les sous-ensembles de requêtes rares ; conservez donc les jugements de pertinence originaux dans la boucle d’évaluation.


Personnalisation et biais de position

La pertinence générique ne suffit pas toujours. Une recherche sur « apple » doit renvoyer des iPhone à un passionné de technologie et des recettes à base de pommes à une personne qui consulte des contenus de cuisine.

Une architecture de retrieval courante pour la personnalisation utilise un modèle d’embeddings à deux tours : le tour de requête encode la requête et le contexte utilisateur, tandis que le tour d’élément encode les éléments et leurs métadonnées. La séparation offline/online prend en charge le retrieval approximate-nearest-neighbor ; sa latence dépend toujours de l’encodeur, de l’index, des filtres et du système de serving.

Les embeddings d’annonces d’Airbnb, OmniSearchSage de Pinterest et les systèmes two-tower d’Uber illustrent différents designs de production. Leur échelle et les gains rapportés appartiennent à ces systèmes ; le pattern transférable est le tour d’items offline associé à un tour de requête/utilisateur online.

Les données de clics contiennent un biais de position et d’exposition. PAL constitue une approche de débiaisage : utiliser la position pendant l’entraînement, puis la maintenir constante en serving. Ce n’est pas une solution universelle ; des interventions randomisées, des méthodes d’inverse propensity et une évaluation contrefactuelle peuvent être plus adaptées à un autre produit.


Adaptation au domaine avec des requêtes synthétiques

Une erreur fréquente dans la stratégie de recherche consiste à supposer qu’un modèle entraîné sur des données web généralistes, comme MS MARCO, fonctionnera bien dans un domaine spécialisé. C’est le problème out-of-domain (OOD).

Les LLMs peuvent réduire, sans l’éliminer, le goulot d’étranglement des données annotées grâce au Generative Pseudo-Labeling (GPL, InPars) :

  1. Prendre le corpus de documents spécifique au domaine
  2. Demander à un LLM, via un prompt, de « Generate a search query that this document would answer »
  3. Utiliser les paires synthétiques (query, document) pour effectuer le fine-tuning du retriever et du reranker

Les paires synthétiques peuvent aider lorsque les requêtes réelles sont rares, mais elles reflètent le générateur et le prompt. Dédupliquez-les, filtrez les requêtes peu plausibles et validez les résultats sur un trafic réel held-out.

Une séquence d’expérimentation

N’ajoutez de la complexité que lorsque l’étape précédente révèle un échec mesuré :

Parcours pratique de maturitéParcours pratique de maturité

Étape 1 (baseline) : implémenter BM25 ou le système lexical actuel et constituer un jeu de requêtes jugées. Enregistrer le recall, le NDCG, la latence et les sous-ensembles d’échecs.

Étape 2 (recall des candidats) : tester le retrieval dense et la fusion uniquement si la baseline manque des documents pertinents. Ajuster le cutoff des candidats en fonction du recall et du coût.

Étape 3 (précision du classement) : ajouter un cross-encoder si les bons candidats existent, mais apparaissent dans le mauvais ordre. Choisir la taille de la shortlist à partir d’une courbe qualité-latence.

Étape 4 (adéquation au domaine) : effectuer un fine-tuning ou une distillation uniquement après avoir observé des échecs spécifiques au domaine, stables, avec des modèles généralistes. Conserver les jugements réels held-out séparés des données d’entraînement synthétiques.

Étape 5 (couche coûteuse facultative) : tester un reranking listwise ou fondé sur le raisonnement uniquement lorsque son gain incrémental de qualité résiste à des exécutions répétées et justifie la latence, le coût, la confidentialité et la complexité du fallback.


Axes de recherche à évaluer séparément

Les rerankers de raisonnement et les agents qui utilisent la recherche sont prometteurs, mais répondent à des questions différentes de celles de la démo de recherche de produits en cinq étapes.

Rerankers fondés sur le raisonnement

Rank1 entraîne des rerankers avec des traces de raisonnement et rapporte de solides résultats sur le benchmark BRIGHT. Ces résultats concernent le retrieval fortement orienté raisonnement, et ne constituent pas une prédiction directe pour la recherche de produits ESCI.

Pour la recherche juridique ou scientifique, comparez les rerankers de raisonnement à de solides baselines cross-encoder et listwise sur des jugements experts, les citations, la latence et la cohérence des échecs.

Recherche agentic

Search-o1 étudie un modèle qui effectue des recherches supplémentaires pendant la résolution de questions multi-hop. Il s’agit d’un problème d’orchestration — génération de requêtes, arrêt, utilisation des preuves et évaluation de la réponse — et non d’une nouvelle étape de reranking. Évaluez-le avec la justesse de la tâche finale et le support des citations, pas uniquement avec des métriques de retrieval.


Points clés à retenir

  1. Considérez le stack comme une suite d’expériences. Établissez une baseline lexicale et un jeu de requêtes jugées avant d’ajouter retrieval dense, fusion ou reranking.

  2. Mesurez séparément le recall des candidats et la précision du classement. Dans cet échantillon ESCI, le Recall@100 atteint 0.842 après la fusion et reste stable pendant les deux étapes de reranking.

  3. Utilisez le retrieval hybride lorsque les erreurs sont complémentaires. Le RRF améliore à la fois le Recall@100 et le NDCG@10 dans la démo, mais un autre corpus peut ne pas justifier deux index.

  4. Ajoutez un cross-encoder lorsque la shortlist est correcte, mais l’ordre incorrect. Choisissez le nombre de candidats à partir d’une courbe qualité-latence mesurée.

  5. Traitez le reranking par LLM comme une expérience finale facultative. La comparaison LLM actuelle de la démo est illustrative, car son parser ne valide pas une permutation complète des identifiants candidats. Le parsing strict, les fallbacks journalisés, les exécutions répétées et le coût doivent être intégrés à l’évaluation avant de rapporter un gain.

  6. Séparez les types de preuves. Un résultat publié, une étude de cas fournisseur, cette démo sur laptop et un test A/B de production répondent à des questions différentes.

Le code complet du pipeline se trouve dans le dépôt associé. Clonez-le pour reproduire les étapes pré-LLM ou tester différents modèles et paramètres ; ne considérez pas sa ligne LLM obsolète comme un résultat.

Références

Articles

Datasets et benchmarks

Modèles utilisés dans la démo

Outils et plateformes

  • rank_bm25 — Implémentation de BM25 en Python
  • Pyserini BEIR Reproductions — Commandes d’indexation et d’évaluation bm25-multifield
  • pytrec_eval — Toolkit d’évaluation TREC
  • Elasticsearch — Recherche hybride avec l’API Retrievers
  • Vespa — Moteur unifié de recherche et de recommandation
  • Weaviate — Base vectorielle avec recherche hybride
  • Qdrant — Base vectorielle avec requêtes multi-étapes

Références industrielles

Projet de démonstration