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’usage | Stack de départ candidat | À valider |
|---|---|---|
| Recherche de produits | BM25 + retrieval dense + RRF + cross-encoder | Recall des attributs, substitutions, latence, contraintes métier |
| Recherche documentaire | Retrieval hybride + cross-encoder | Identifiants exacts, questions sémantiques, filtres de version |
| Déflexion du support | Retrieval hybride + vérification des citations | Recall du retrieval, grounding, abstention |
| Marketplace ou annonces | Filtres lexicaux + retrieval dense + reranker métier | Disponibilité, fraîcheur, conformité, diversité des vendeurs |
| Petit corpus interne | Baseline BM25, puis un reranker | Si le décalage de vocabulaire justifie un index dense |
| Recherche juridique ou médicale à forts enjeux | Retrieval axé recall et revue experte | Couverture, 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 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.
| Étape | Échelle d’entrée | Objectif principal | Méthodes typiques | Mesure de sortie |
|---|---|---|---|---|
| Retrieval | Corpus ou index | Recall des candidats | BM25, bi-encoders | Recall au cutoff des candidats |
| Pre-ranking | Grand jeu de candidats | Filtrage peu coûteux | Modèles légers, règles | Recall conservé par milliseconde |
| Full ranking | Shortlist | Qualité du top du classement | Cross-encoders, LLMs | NDCG/MRR, latence, coût |
| Blending | Listes classées ou slots finaux | Contraintes et mélange | Règles, classement multi-objectifs | Politique, 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.
Le pipeline :
- Retrieval BM25 sparse — baseline lexicale (
rank_bm25) - Retrieval par bi-encoder dense — génération de candidats sémantiques (
all-MiniLM-L6-v2) - Fusion hybride RRF — fusion fondée sur le rang des résultats sparse et denses
- Reranking par cross-encoder — scores de pertinence pairwise (
ms-marco-MiniLM-L-12-v2) - 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 :
| Label | Gain | Signification | Exemple (requête : « wireless headphones ») |
|---|---|---|---|
| Exact (E) | 3 | Satisfait toutes les exigences | Sony WH-1000XM5 Wireless Headphones |
| Substitute (S) | 2 | Alternative fonctionnelle | Casque filaire avec adaptateur Bluetooth |
| Complement (C) | 1 | Article connexe et utile | Étui de transport pour casque |
| Irrelevant (I) | 0 | Relation non significative | Câ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 :
Où est l’inverse de la fréquence documentaire du terme , est la fréquence du terme dans le document , est la longueur du document et la longueur moyenne des documents du corpus. Deux paramètres sont importants : (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 (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 avec un document positif et un ensemble de documents négatifs :
Où est la similarité cosinus entre les embeddings de la requête et du document, et 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 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.
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 384 dimensions 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.
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.
Reciprocal Rank Fusion (Cormack et al., 2009) ignore complètement les scores bruts et utilise uniquement la position dans le classement :
Ici, 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 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.
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.
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.
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 :
| Mode | Modèle | Mode d’exécution |
|---|---|---|
ollama | llama3.2:3b (configurable) | Local via l’API Ollama |
api | claude-haiku-4-5-20251001 | API Anthropic |
local | Qwen/Qwen2.5-1.5B-Instruct | HuggingFace 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.
| Étape | NDCG@10 | MRR | Recall@100 | Delta NDCG |
|---|---|---|---|---|
| BM25 | 0.585 | 0.812 | 0.741 | — |
| Bi-Encoder dense | 0.611 | 0.808 | 0.825 | +0.026 |
| Hybride (RRF) | 0.628 | 0.834 | 0.842 | +0.017 |
| + Cross-Encoder | 0.645 | 0.860 | 0.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 :
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é.
| Dimension | Graphe HNSW | Clusters IVF |
|---|---|---|
| Contrôle de la requête | efSearch | nprobe |
| Contrôle de la construction | Connectivité et beam de construction | Nombre de clusters et échantillon d’entraînement |
| Profil mémoire | Arêtes du graphe et vecteurs | Centroïdes, listes et vecteurs stockés |
| Comportement des mises à jour | Réparation/nettoyage propre à la base | Maintenance des listes propre à la base |
| À évaluer avec | Courbe recall-latence-mémoire-churn | Courbe 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 :
- Utiliser un LLM puissant (l’enseignant) pour rerank des milliers de requêtes d’entraînement
- 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
- 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) :
- Prendre le corpus de documents spécifique au domaine
- Demander à un LLM, via un prompt, de « Generate a search query that this document would answer »
- 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é :
É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
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
- Reciprocal Rank Fusion — Cormack et al., 2009
- RankGPT: LLMs as Zero-Shot Listwise Rerankers — Sun et al., article distingué EMNLP 2023
- Rank1: Reasoning-Based Reranking — Weller et al., COLM 2025
- SCaLR: Self-Calibrated Listwise Reranking — Framework de reranking listwise auto-calibré
- GCCP: Global-Consistent Comparative Pointwise — Traitement de la calibration dans le classement pointwise par LLM
- Rank-DistiLLM: Knowledge Distillation for Reranking — Schlatt et al., ECIR 2025
- Query2doc: LLM Query Expansion — Wang et al., EMNLP 2023
- GPL: Generative Pseudo Labeling — Adaptation au domaine pour le retrieval dense
- InRanker: Distilled Reranker — Réduction de taille de 50x avec des performances compétitives
- Search-o1: Agentic Retrieval — EMNLP 2025
- TALEC: LLM-as-a-Judge for Search — Framework d’évaluation
- BEIR Benchmark — Thakur et al., NeurIPS 2021
- BEIR Leaderboard — Baseline
BM25 multifieldet agrégat sur 18 datasets - Sentence-BERT — Reimers & Gurevych, EMNLP 2019
- InfoNCE / CPC — van den Oord et al., 2018
- SimANS: Hard Negative Sampling — Zhou et al., EMNLP 2022
- Passage Reranking with BERT — Nogueira & Cho, 2019
- HNSW — Malkov & Yashunin, 2016
- DiskANN — Subramanya et al., NeurIPS 2019
- RaBitQ — Gao & Long, SIGMOD 2024
- Replacing Judges with Juries — Verga et al., 2024
- RAG-Fusion — Rackauckas, 2024
- InPars — Bonifacio et al., SIGIR 2022
- BRIGHT Benchmark — Su et al., ICLR 2025
- Rank-without-GPT — Zhang et al., ECIR 2025
- Pinterest LLM Search Relevance — Wang et al., 2024
- OmniSearchSage — Agarwal et al., WWW 2024
- PAL: Position-bias Aware Learning — Guo et al., RecSys 2019
Datasets et benchmarks
- Amazon ESCI: Shopping Queries Dataset — KDD Cup 2022
- BEIR: Benchmarking IR — Benchmark hétérogène pour l’évaluation zero-shot
- MTEB: Massive Text Embedding Benchmark — Classement des modèles d’embeddings
- ESCI Paper — Reddy et al., 2022
Modèles utilisés dans la démo
- all-MiniLM-L6-v2 — Bi-encoder de 22M paramètres
- ms-marco-MiniLM-L-12-v2 — Cross-encoder de 33M paramètres
- Sentence-Transformers — Framework de modèles de retrieval neuronaux
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
- Airbnb Listing Embeddings — Grbovic & Cheng, KDD 2018
- Uber Delivery Search — Uber Engineering, 2025
- Uber Two-Tower Embeddings — Uber Engineering, 2023
- Elastic Rerank — Elastic, 2024
Projet de démonstration
- search-ranking-stack — Démo fonctionnelle contenant tout le code de cet article