Comment mesurer et améliorer le rappel de recherche Elasticsearch : de 0,43 à 0,75 avec la recherche hybride

Découvrez comment mesurer et améliorer le rappel de recherche dans Elasticsearch en combinant la recherche lexicale BM25 avec les embeddings vectoriels de Jina AI, en utilisant l’API rank_eval pour valider l’amélioration avec des données chiffrées.

La recherche lexicale utilisant l’algorithme de classement BM25 est peu coûteuse, rapide et très efficace pour de nombreuses requêtes. Mais elle présente un inconvénient : les requêtes qui ne partagent pas de jetons avec vos documents. Dans cet article, vous allez mesurer exactement les points faibles de la BM25. Nous utiliserons l’API d’évaluation de classement (rank_eval) d’Elasticsearch et comblerons cet écart en ajoutant des embeddings Jina AI via Elastic Inference Service (EIS). Vous verrez le score de rappel passer de 0.43 à 0.75 et vous comprendrez pourquoi.

Qu'est-ce que le rappel ?

Le rappel mesure, sur une échelle allant de 0 à 1, le nombre de documents réellement souhaités par vos utilisateurs qui apparaissent quelque part dans vos résultats de recherche. Si une requête doit faire apparaître trois produits et que votre recherche ne renvoie que deux d’entre eux dans le top 10, recall@10 = 0.67 pour cette requête. C’est une métrique basée sur des ensembles : la position des documents pertinents dans ces k résultats n’a pas d’importance. Un document pertinent en position 10 compte autant qu’un document en position 1. Un taux de rappel élevé signifie que vous ne perdez pas de résultats pertinents.


Le diagramme montre deux ensembles : tous les documents pertinents (à gauche) et ce que BM25 a réellement récupéré (les 10 premiers, à droite). Seules les intersections comptent pour le rappel, prod_1 et prod_2 ont été trouvés, tandis que prod_3, prod_4 et prod_6 ont été totalement manqués. Résultat : Recall@10 = 2/5 = 0.40.

Produits requis

Entrons dans le vif du sujet pour mieux comprendre le fonctionnement du rappel. Cette démonstration utilise Python. Vous pouvez la suivre dans le cahier d’accompagnement (notebook.ipynb), où chaque bloc de code est une cellule prête à être exécutée.

Le code fourni utilise les éléments suivants :

  • Elasticsearch 9.3+
  • Python 3.10+
  • Un fichier .env avec vos identifiants Elasticsearch

L’ensemble de données

Nous utiliserons un catalogue de produits de 1 000 articles, couvrant des catégories telles que les chaussures, l’électronique, les outils, et bien d’autres.

Chaque document comporte quatre champs :

ChampType
`title`Texte
descriptionTexte
marquemot-clé
Catégoriemot-clé

L’ensemble de données est chargé à partir de dataset.csv.

BM25 est l’algorithme de classement par défaut d’Elasticsearch et de la plupart des moteurs de recherche. Il attribue des scores aux documents en fonction de la fréquence d’apparition de vos termes de requête dans ceux-ci, ajustée en fonction de la longueur du document et de la fréquence de ces termes dans l’ensemble de l’index. Vous disposez d’analyseurs en plus : normalisation des minuscules, troncature et suppression des mots vides. Une requête pour « chaussures de course » correspondra à « Chaussures de course » et probablement aussi à « courir ».

Cette méthode fonctionne bien pour une grande catégorie de requêtes :

  • « chaussures de course » associe immédiatement les produits dont le titre correspond exactement à ces termes.
  • L’expression « enceinte Bluetooth » fait apparaître des produits audio portables, car les termes apparaissent tels quels.

Les résultats sont déterministes et explicables : un document est bien classé parce que les termes de la requête y apparaissent. La pertinence du débogage est simple.

Les cas d’échec

Essayons maintenant ces requêtes sur le même catalogue :

  • « Routine de soins de la peau » : Le mot « routine » n’apparaît dans aucun titre de produit. BM25 peut correspondre partiellement à la requête « soins de la peau », mais les sérums pour le visage, les huiles corporelles et les crèmes hydratantes sont décrits à l’aide de termes comme « vitamine C », « rétinol » ou « éclaircissant », dont aucun ne correspond à la requête. Les produits qui forment une routine de soins de la peau complète sont dispersés dans l’index sans aucun élément commun permettant de les regrouper.
  • « Accessoires de voyage pour animaux de compagnie » : il s’agit d’un regroupement de cas d’utilisation, et non d’une catégorie de produits. Un sac kangourou pour chien, un siège auto pour animal et une caisse de voyage sont tous pertinents, mais leurs descriptions parlent de portabilité, de sécurité et de confort plutôt que d’« accessoires de voyage ». BM25 trouve des correspondances pour le terme « animal de compagnie » au sens large, mais ne comporte aucun signal permettant de distinguer les produits spécifiques aux voyages du reste du catalogue pour animaux de compagnie.

Il s'agit d'un problème de rappel. Les documents pertinents se trouvent dans votre index. BM25 ne peut tout simplement pas les trouver car les mots de l'utilisateur et les mots du document ne correspondent pas suffisamment étroitement.

L'ajout de synonymes est utile pour les cas connus. Mais vous ne pouvez pas énumérer toutes les façons dont un utilisateur pourrait exprimer une intention. C'est là que les vecteurs entrent en jeu.

Pourquoi il est conseillé de mesurer le rappel

Avant de résoudre un problème, il faut le quantifier.

Recall@k mesure combien de documents vos utilisateurs souhaitent réellement voir apparaître dans vos résultats de recherche. Au sens strict :

Precision@k mesure les k premiers résultats et combien sont réellement pertinents :

Une grande précision garantit la qualité des résultats obtenus. Dans le commerce électronique, l’absence d’un produit pertinent (faible taux de rappel) est souvent pire que l’affichage d’un résultat légèrement imparfait (précision moindre), car un produit caché est une vente perdue.

L’API d’Elasticsearch rank_eval vous permet de mesurer les deux de manière systématique. Vous fournissez une liste de requêtes, chacune avec un ensemble de documents évalués, et Elasticsearch calcule les métriques pour vous pour l’ensemble des requêtes.

Configuration de l'évaluation

L’API rank_eval nécessite un ensemble de données d’évaluations : un mappage des requêtes vers les documents pertinents pour chacune d’elles, accompagné d’un grade de pertinence (0 = non pertinent, 1 = pertinent, 2 = très pertinent).

Dans le cahier, il s’agit de la liste des jugements :

Le mélange est intentionnel : q1 est une requête que BM25 gère bien (jetons exacts dans les titres des produits), tandis que q2, q3, et q4 sont des requêtes basées sur l’intention où l’intention de l’utilisateur est exprimée sous forme de concept plutôt que de mots-clés spécifiques sur les produits.

Mesurer le rappel de référence du BM25

Commencez par configurer le client Elasticsearch et indexez les données textuelles brutes :

Maintenant, créez la requête rank_eval pour BM25. Chaque requête dans la liste combine une requête avec ses notations :

Voici le résultat.

0.43 Signifie que sur l’ensemble des quatre requêtes, BM25 ne trouve que 43 % des documents qu’il devrait trouver. Le problème se situe principalement dans les requêtes basées sur l’intention : « routine de soins de la peau » ne trouve pas les sérums pour le visage et les huiles corporelles car le mot « routine » n’apparaît jamais dans les titres des produits, et « accessoires de voyage pour animaux de compagnie » renvoie des produits pour animaux de compagnie hors sujet tout en ne trouvant pas les cages et les caisses de transport dont la description précise les fonctionnalités de portabilité et de sécurité plutôt que d’« accessoires de voyage ».

Ceci est notre référence. Nous avons maintenant un chiffre à battre.

Ajout de la recherche vectorielle avec les embeddings Jina

Vector search encode les documents et les requêtes sous forme de vecteurs de grande dimension, composés de centaines ou de milliers de valeurs numériques, chacune encodant une fonctionnalité spécifique des données représentées. Les documents ayant une signification similaire se retrouvent proches les uns des autres dans l’espace vectoriel, même s’ils ne partagent aucun mot. « Équipement de gym » et « kit d’haltères » seront proches l’un de l’autre, car les concepts sont liés. J’ai choisi Elasticsearch comme base vectorielle, car il prend en charge la recherche hybride, ce qui me permet de bénéficier d'emblée à la fois d’une compréhension sémantique et d’une précision par mot-clé.

EIS inclut une prise en charge prête à l’emploi pour l’intégration de modèles via son API d’inférence.

Étape 1 : utiliser les embeddings Jina v5 comme point de terminaison d’inférence

Si votre cluster dispose de ressources GPU (disponibles dans Elastic Cloud et Elasticsearch 9.3+), les embeddings sont générés sur GPU, ce qui est nettement plus rapide que l’inférence CPU, et supprime le compromis de performance qui rendait auparavant l’utilisation des vecteurs coûteuses à grande échelle.

Pourquoi spécifiquement les embeddings Jina ? jina-embeddings-v5-text est un modèle multilingue (plus de 119 langues) avec une fenêtre de contexte de 32 000 jetons et une prise en charge des adaptateurs Low-Rank Adaptation (LoRA) spécifiques à la tâche. Il fonctionne bien pour les courtes descriptions de produits prêtes à l’emploi. En savoir plus sur le modèle jina-embeddings-v5-text ici.

Étape 2 : créer l’index avec un champ sémantique

Le type de champ semantic_text est ici la clé. C’est une abstraction de niveau supérieur par rapport à dense_vector : vous le dirigez vers un point de terminaison d’inférence, et Elasticsearch se charge de générer automatiquement les plongements.

La propriété copy_to sur title et description signifie que le contenu des deux champs est transmis à semantic_field pour intégration, de sorte qu’un seul vecteur capture la représentation complète du produit.

Étape 3 : indexer les produits

Au moment de l’indexation, Elasticsearch appelle le point de terminaison d’inférence pour chaque document et stocke le vecteur d’intégration résultant dans semantic_field. Aucun code supplémentaire n’est nécessaire de votre côté.

Recherche hybride : combinaison de BM25 et de vecteurs avec RRF

L'ajout de vecteurs améliore le taux de rappel, mais le recours exclusif aux vecteurs risque d'entraîner une perte de précision pour les requêtes en correspondance exacte. Les résultats pour « chaussures de course » devraient toujours classer en premier les correspondances exactes. La recherche hybride conserve la composante lexicale spécifiquement pour préserver cette précision.

La recherche hybride avec la Fusion des rangs réciproques (RRF) combine le meilleur des deux :

  • BM25 gère les requêtes exactes et quasi-exactes avec une grande précision.
  • La recherche sémantique gère les requêtes basées sur l’intention et multilingues avec un rappel élevé.
  • RRF fusionne les deux listes classées en un seul classement.

La formule RRF attribue à chaque document un score basé sur son classement dans chaque liste de résultats :

Un document bien classé dans les deux listes obtient un score combiné plus élevé. Le paramètre rank_constant détermine le poids attribué aux documents moins bien classés.

Voici le résultat.

Hybrid s’améliore nettement par rapport à BM25 (0.43) et préserve la précision des requêtes de correspondance exacte, comme « chaussures de course ».

Résultats : avant et après

Voici un comparatif complet des trois approches :

Voici le résultat.

MéthodeRecall@10
BM25 (Lexical)0,43
Hybride (BM25 + vecteurs)0,75

Répartition des données par requête :

Conclusion

Tout au long de cet article, nous avons vu que la recherche lexicale BM25 est fiable lorsque les utilisateurs tapent des requêtes exactes, mais qu’elle perd en rappel lorsqu’ils recherchent par intention plutôt que par mots-clés. En utilisant rank_eval, nous avons établi une base reproductible pour mesurer cet écart avec des nombres réels. Ensuite, nous avons ajouté un champ semantic_text alimenté par les embeddings Jina et relancé l’évaluation. Le résultat : la recherche hybride a amélioré le rappel de 0.43 à 0.75, tout en préservant la précision des requêtes à correspondance exacte, bien que la marge réelle dépende de la composition de vos requêtes.

Le modèle s’étend au-delà de cet exemple : collectez les jugements à partir des requêtes réelles de vos utilisateurs, exécutez rank_eval comme référence, ajoutez semantic_text, puis mesurez à nouveau. Vous saurez exactement ce qui s’est amélioré et de combien.

Étapes suivantes

Ce contenu vous a-t-il été utile ?

Pas utile

Plutôt utile

Très utile

Pour aller plus loin

Prêt à créer des expériences de recherche d'exception ?

Une recherche suffisamment avancée ne se fait pas avec les efforts d'une seule personne. Elasticsearch est alimenté par des data scientists, des ML ops, des ingénieurs et bien d'autres qui sont tout aussi passionnés par la recherche que vous. Mettons-nous en relation et travaillons ensemble pour construire l'expérience de recherche magique qui vous permettra d'obtenir les résultats que vous souhaitez.

Jugez-en par vous-même