Similaires sans être identiques : quand les synonymes boostent la puissance d'Elasticsearch

Pour les spécialistes des moteurs de recherche, les synonymes jouent indiscutablement un rôle primordial. Si on a tendance à sous-estimer leur importance lorsqu'on débute, en réalité, presque aucun système de recherche ne peut s'en passer. Leur utilisation présente toutefois quelques difficultés et subtilités qui ne sont parfois pas appréciées à leur juste valeur, même par les utilisateurs avertis. Les filtres de synonymes font partie du processus d'analyse qui convertit le texte d'entrée en termes que l'utilisateur peut rechercher. Et bien qu'il soit relativement facile de se lancer, ils peuvent être exploités de diverses manières et nécessitent une connaissance approfondie de certains concepts avant d'être appliqués efficacement à un scénario du monde réel.

Par ailleurs, nous avons récemment apporté des améliorations à l'analyse dans Elasticsearch. Parmi elles, citons notamment la possibilité de recharger les analyseurs au moment de la recherche, qui permet à son tour la modification et le rechargement des synonymes utilisés au moment de la recherche. Dans cet article de blog, nous allons vous en dire plus sur cette nouvelle API, répondre à des questions fréquentes sur l'utilisation des synonymes, tout en soulignant les précautions à prendre lorsque vous les exploitez.

Pourquoi utiliser des synonymes ?

Pour comprendre l'utilité et la flexibilité des synonymes, penchons-nous brièvement sur le fonctionnement interne des moteurs de recherche d'aujourd'hui. Les documents et les requêtes sont analysés et réduits à leurs plus petites unités (souvent appelées "tokens"), qui consistent essentiellement en des symboles abstraits. Lors d'une recherche, le processus de mise en correspondance s'appuie sur une simple similarité entre les chaînes. C'est pourquoi même une petite erreur de frappe ("mason" au lieu de "maison") ou l'utilisation d'un pluriel ("maisons") ne renverra pas les documents dans lesquels ce terme n'apparaît qu'au singulier ("maison"). Les analyseurs morphologiques ou les recherches approximatives répondent à certains des problèmes les plus courants, mais ils ne parviennent pas à combler l'écart qui existe entre des concepts et des idées connexes, ou entre les légères différences de vocabulaire que peuvent présenter les documents et les requêtes.

C'est là que les synonymes prennent tout leur sens. Le mot "synonyme" nous vient du grec ancien ; il est composé du préfixe σύν (syn, "avec", "ensemble") et de ὄνομα (ónoma, "nom"). Étymologiquement, nous voyons déjà que les synonymes décrivent des termes différents qui ont exactement ou quasiment le même sens, dans la même langue ou dans le même domaine. En pratique, cela peut s'appliquer à des synonymes généraux ("épuisé" ou "exténué"), à des abréviations ("kg" ou "kilogramme"), à différentes variantes orthographiques d'un nom de produit dans le domaine de la vente en ligne ("iPod" ou "i-Pod"), à de petites différences linguistiques (comme "pain au chocolat" ou "chocolatine"), au langage professionnel ou au langage courant ("canins" ou "chiens"), ou simplement à exprimer le même concept de deux manières différentes ("univers" ou "cosmos"). Lorsqu'ils appliquent les règles synonymiques appropriées, les spécialistes des moteurs de recherche sont en mesure de fournir des informations sur les mots qui signifient quasiment la même chose dans leur domaine et qui doivent donc être traités de manière similaire.

Pour un moteur de recherche, il est important savoir quels termes contenus dans les documents et les requêtes doivent correspondre, même s'ils sont différents. Cette mise en correspondance étant extrêmement dépendante du domaine, les utilisateurs doivent fournir les règles appropriées. Les filtres de synonymes, qu'on peut utiliser dans des analyseurs personnalisés, permettent de remplacer des tokens ou d'en ajouter en fonction de règles définies par l'utilisateur. Cela se fait soit au moment de l'indexation, afin de stocker les deux variantes d'un mot dans un document indexé, par exemple, ou au moment de la recherche, afin d'étendre les termes de recherche et de faire correspondre un plus grand nombre de documents pertinents. Nous reviendrons sur les avantages et les inconvénients de ces deux approches un peu plus bas.

De l'utilisation prudente des synonymes dans certains cas

Les filtres de synonymes sont des outils très flexibles, ce qui entraîne dans certains cas leur surutilisation. Par exemple, ils sont parfois utilisés pour remplacer purement et simplement les analyseurs morphologiques, avec des fichiers de synonymes volumineux contenant les variantes grammaticales de verbes et de noms. Bien qu'il soit possible d'adopter cette approche, elle amoindrit habituellement les performances et rend la maintenance plus difficile qu'avec des analyseurs morphologiques ou des lemmatiseurs. Il en va de même pour la correction orthographique. En présence d'un nombre limité de fautes d'orthographe (paramètres d'un site de vente en ligne, par exemple), il peut être pertinent de les corriger grâce à des synonymes. Mais si celles-ci sont plus généralisées, l'utilisation de recherches approximatives ou de n-grammes de caractères s'avère plus viable. Il convient aussi de tenir compte des alternatives à l'extension des synonymes dans la chaîne d'analyse. Parfois, l'amélioration des documents via un pipeline d'ingestion ou via un autre processus côté client s'avère plus flexible et plus gérable que l'utilisation de synonymes dans le processus d'analyse, qui est plus limité. Par exemple, vous pouvez détecter des entités nommées dans vos documents grâce à des frameworks de reconnaissance d'entités nommées (REN) et les encoder en identifiants uniques dans votre pipeline de prétraitement ou au moment de l'ingestion. Si vous appliquez ensuite le même processus aux requêtes des utilisateurs avant de les envoyer vers Elasticsearch, vous obtenez le même effet, mais vous maîtrisez généralement mieux le processus.

Sans oublier qu'il est tentant d'exploiter les synonymes pour d'autres similitudes, comme leregroupement de certaines espèces animales sous un terme commun, voire la prise en charge d'une taxonomie pour votre domaine. C'est là que les choses deviennent vraiment intéressantes, avec beaucoup de pistes à explorer. Toutefois, gardez à l'esprit que les synonymes ne constituent pas toujours la meilleure solution et qu'ils peuvent entraîner un comportement inattendu du système si vous ne les utilisez pas avec précaution.

Synonymes au moment de l'indexation ou au moment de la recherche ?

Les synonymes sont utilisés dans des analyseurs qu'on peut exploiter au moment de l'indexation ou au moment de la recherche. L'une des questions les plus récurrentes au sujet des filtres de synonymes dans Elasticsearch est de savoir s'il convient de les utiliser au moment de l'indexation, au moment de la recherche, ou les deux. Commençons par l'application de filtres de synonymes au moment de l'indexation. Cela signifie que les termes contenus dans les documents indexés sont remplacés ou étendus une fois pour toutes, et que le résultat est conservé dans l'index de recherche.

Les synonymes au moment de l'indexation présentent plusieurs inconvénients :

  • L'index risque de devenir volumineux, car tous les synonymes doivent être indexés.
  • Le score de recherche, qui dépend des statistiques terminologiques, risque d'être affecté, car les synonymes sont également comptés, ce qui fausse les statistiques des termes moins courants.
  • Les règles synonymiques ne peuvent pas être modifiées pour les documents existants sans réindexation.

Les deux derniers points sont particulièrement désavantageux. Le seul avantage potentiel des synonymes au moment de l'indexation est la performance : le coût du processus d'extension est payé d'avance et il est inutile de renouveler l'opération au moment de la recherche, ce qui se traduit potentiellement par un plus grand nombre de termes devant être mis en correspondance. Toutefois, en pratique, cela n'est pas vraiment un problème.

En revanche, l'utilisation de synonymes dans les analyseurs au moment de la recherche est moins problématique :

  • La taille de l'index n'est pas impactée.
  • Les statistiques terminologiques du corpus restent inchangées.
  • La modification des règles synonymiques ne nécessite pas la réindexation des documents.

Ces avantages l'emportent en général sur l'unique inconvénient, à savoir la nécessité d'étendre les synonymes à chaque recherche, et donc potentiellement, le plus grand nombre de termes à mettre en correspondance. De plus, l'extension des synonymes au moment de la recherche permet d'exploiter un filtre de tokens plus avancé : synonym_graph. Exclusivement conçu pour être utilisé dans un analyseur de recherche, celui-ci peut correctement traiter les synonymes multimots.

Les avantages qu'il y a à utiliser les synonymes au moment de la recherche l'emportent en général sur les légers gains de performance que qu'on peut obtenir lorsqu'on les utilise au moment de l'indexation.

Notons néanmoins que l'utilisation de synonymes au moment de la recherche présentait jusqu'ici un autre inconvénient. Bien que la modification des règles synonymiques ne nécessite pas la réindexation des documents, on était obligé de fermer temporairement l'index et de le rouvrir. Cela était nécessaire, car les analyseurs sont instanciés au moment de la création de l'index, du redémarrage d'un nœud ou de la réouverture d'un index fermé. Pour que l'index tienne compte des modifications apportées à un fichier de règles synonymiques, on devait d'abord mettre à jour le fichier sur l'ensemble des nœuds, puis fermer et rouvrir l'index. Mais tout cela est devenu inutile.

Les synonymes revisités grâce à l'API de rechargement

Avec Elasticsearch 7.3, plus besoin de rouvrir les index pour actualiser les modifications apportées aux fichiers de synonymes. Grâce au point de terminaison que nous avons ajouté, il est désormais possible de déclencher le rechargement des ressources de l'analyseur à la demande. Il suffit d'appeler ce nouveau point de terminaison pour recharger tous les analyseurs d'un index qui contiennent des éléments marqués comme modifiables. Par conséquent, ces éléments ne sont utilisables qu'au moment de la recherche.

Par ailleurs, grâce au marquage des filtres de synonymes comme modifiables et à l'appel de l'API de rechargement, les modifications apportées au fichier de configuration des synonymes sur chaque nœud deviennent visibles au processus d'analyse. Il n'est pas possible de mettre à jour des règles synonymiques qui font partie de la définition du filtre (via le paramètre synonyms), mais celles-ci sont la plupart du temps réservées aux tests ad hoc. Toujours est-il que la configuration de synonymes via un fichier de configuration présente plusieurs avantages :

  • Ils sont bien plus simples à gérer. Dans un système de production, les règles synonymiques peuvent être nombreuses, et comme elles ont une grand incidence sur la pertinence de la recherche, elles doivent faire partie intégrante de la configuration, faire l'objet de contrôles de version et être testées à chaque mise à jour.
  • Les synonymes sont souvent extraits d'autres sources ou créés par un algorithme exécuté sur vos données. Puisqu'ils sont lus depuis des fichiers, il devient inutile de les intégrer à la configuration des filtres.
  • On peut utiliser le même fichier de synonymes dans différents filtres.
  • Lorsqu'ils sont volumineux, les ensembles de règles synonymiques occupent beaucoup d'espace mémoire dans l'état du cluster Elasticsearch qui stocke les métadonnées relatives aux paramètres des index. Pour éviter un accroissement inutile de la taille du cluster, il est recommandé de stocker les ensembles de règles synonymiques volumineux dans des fichiers de configuration.

Pour illustrer cela, imaginons que vous ayez placé dans le répertoire config de vos nœuds Elasticsearch un premier fichier my_synonyms.txt contenant l'unique règle ci-dessous. Imaginons donc que le fichier ne contienne initialement que cette unique règle :

universe, cosmos

Nous devons ensuite définir un analyseur qui référence ce fichier dans un filtre de synonymes :

PUT /synonym_test
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_analyzer": {
            "tokenizer": "whitespace",
            "filter": ["my_synonyms"]
          }
        },
        "filter": {
          "my_synonyms": {
            "type": "synonym",
            "synonyms_path": "my_synonyms.txt",
            "updateable": true
          }
        }
      }
    }
  }
}

Soulignons que nous avons indiqué que ce filtre de synonymes est updateable (modifiable). Cela est important, car seuls les filtres modifiables sont rechargés lorsque nous appelons le nouveau point de terminaison qui déclenche le rechargement. L'inconvénient, c'est que les analyseurs contenant des filtres modifiables ne peuvent plus êtres utilisés au moment de l'indexation. Mais vérifions tout d'abord que les synonymes sont correctement appliqués.Pour ce faire, nous allons lancer un petit test via le point de terminaison _analyze :

GET /synonym_test/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "cosmos"
}

Nous devons obtenir deux tokens, l'un deux étant "universe" ("univers"), comme attendu. Sur une deuxième ligne, ajoutons maintenant une autre règle au fichier synonyms.txt (en anglais, "lift" et "elevator" signifient tous deux "ascenseur", le premier est employé en Grande-Bretagne, le second, aux États-Unis. Les deux mots sont donc synonymes) :

lift, elevator

C'est à cette étape qu'il était jusqu'ici nécessaire de fermer l'index et de le rouvrir pour afficher les modifications. Il suffit maintenant d'appeler le nouveau point de terminaison :

POST /synonym_test/_reload_search_analyzers

Cette requête ne nécessite pas de corps, mais on peut la limiter à un ou à plusieurs index grâce aux modèles d'indexation à caractères génériques. La réponse indique les analyseurs rechargés et les nœuds concernés :

{
  [...],
  "reload_details": [{
    "index": "synonym_test",
    "reloaded_analyzers": ["synonym_analyzer"],
    "reloaded_node_ids": ["FXbmbgG_SsOrNRssrYcPow"]
  }]
}

Lorsqu'on exécute la requête _analyze ci-dessus sur le terme "lift", on obtient maintenant le token synonyme "elevator".

Quelques points méritent cependant d'être soulignés. Comme mentionné plus haut, un filtre marqué comme updateable (modifiable) doit être utilisé au moment de la recherche. Pour appliquer l'analyseur de synonymes défini plus haut à un champ, il faut donc procéder comme suit :

POST /synonym_test/_mapping
{
  "properties": {
    "text_field": {
      "type": "text",
      "analyzer": "standard",
      "search_analyzer": "synonym_analyzer"
    }
  }
}

Notons aussi que seuls les synonymes chargés depuis des fichiers peuvent être rechargés : la modification des synonymes définis via les paramètres d'un filtre n'est pas prise en charge. Enfin, vous devez vous assurer d'appliquer les mises à jour aux fichiers de synonymes présents sur l'ensemble des nœuds du cluster. Si l'analyseur accède, sur certains nœuds, à des versions de fichier différentes, vous risquez d'obtenir des résultats de recherche hétérogènes, en fonction du nœud utilisé pour la recherche. Si cela se produit pour un synonyme, la première chose à faire sera de vérifier que vos fichiers de synonymes sont identiques sur tous les nœuds, avant de déclencher le rechargement de nouveau.

Récapitulons : le nouveau point de terminaison _reload_search_analyzer vous permet de revoir et de modifier rapidement vos synonymes au moment de la recherche, sans devoir rouvrir vos index. Par exemple, il vous suffit d'examiner vos logs de recherche pour savoir si les utilisateurs saisissent d'autres termes de recherche que ceux de vos documents indexés, puis de les ajouter à chaud. L'ajout de synonymes peut cependant avoir des conséquences imprévues sur les scores de pertinence. Il est donc recommandé de commencer par un test (par exemple, un test A/B ou quelque chose comme l'API d'évaluation des classements) avant d'appliquer les modifications en production.

Chaînes d'analyse et filtres de synonymes

Une autre question fréquemment soulevée au sujet des filtres de synonymes concerne leur comportement dans des chaînes d'analyse complexes. Dans la plupart des cas, on place des filtres de caractère ou de tokens devant le filtre de synonymes, par exemple un filtre lowercase (minuscules). Tous les tokens transmis à la chaîne d'analyse sont donc convertis en minuscules avant l'application du filtre de synonymes. Cela signifie-t-il pour autant que les synonymes en entrée dans les règles synonymiques doivent aussi être saisis en minuscules pour correspondre ? Voyons ce que cela donne dans cet exemple simple :

PUT /test_index
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_analyzer": {
            "tokenizer": "whitespace",
            "filter": ["lowercase", "my_synonyms"]
          }
        },
        "filter": {
          "my_synonyms": {
            "type": "synonym",
            "synonyms": ["Eins, Uno, One", "Cosmos => Universe"]
          }
        }
      }
    }
  }
}
GET /test_index/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "one"
}

Dans l'exemple ci-dessus, nous voyons que le texte d'entrée en minuscules est étendu à trois tokens, ce qui signifie que les minuscules sont aussi appliquées aux règles du filtre de synonymes. De plus, comme illustré par la règle "Cosmos => Universe" ci-dessus, la partie droite des règles de remplacement est réécrite en minuscules dans le résultat :

GET /test_index/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "cosmos"
}

En général, les filtres de synonymes réécrivent les entrées selon le générateur de tokens et les filtres utilisés dans la chaîne d'analyse précédente. Il existe néanmoins quelques exceptions notables : un certains nombre de filtres qui génèrent des tokens empilés en sortie (comme common_grams ou le filtre phonetic) ne sont pas autorisés à précéder les filtres de synonymes et entraînent des erreurs dans le cas contraire. D'autres, comme les filtres de mots composés, voire les filtres de synonymes eux-mêmes, sont ignorés lorsqu'ils précèdent un autre filtre de synonymes de la chaîne. Cette dernière règle est importante pour permettre le chaînage des filtres de synonymes. Nous le verrons en action dans le prochain exemple.

Que se passe-t-il donc si vous placez deux ou plusieurs filtres de synonymes à la suite ? La sortie du premier filtre sera-t-elle l'entrée du dernier ? Le chaînage des filtres de synonymes se rapproche-il ainsi en quelque sorte d'une opération transitive ? Essayons l'exemple suivant :

PUT /synonym_chaining
{
  "settings": {
    "index": {
      "analysis": {
        "filter": {
          "first_synonyms": {
            "type": "synonym",
            "synonyms": ["a => b", "e => f"]
          },
          "second_synonyms": {
            "type": "synonym",
            "synonyms": ["b => c", "d => e"]
          }
        },
        "analyzer": {
          "synonym_analyzer": {
            "filter": [
              "first_synonyms",
              "second_synonyms"
            ],
            "tokenizer": "whitespace"
          }
        }
      }
    }
  }
}
GET /synonym_chaining/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "a"
}

Dans cet exemple, le token de sortie est "c". Cela signifie que les deux filtres sont appliqués l'un à la suite de l'autre : le premier filtre remplace "a" par "b" et le second remplace cette entrée par "c". Si, en lieu et place, "d" est utilisé comme entrée, il est remplacé par "e" (la première règle n'est pas appliquée), mais si vous utilisez "e" comme entrée, le token est remplacé par "f" dans le premier filtre, et le second filtre ne trouve aucune correspondance.

Souvenez-vous des exceptions évoquées plus haut au sujet de la réécriture en fonction des filtres de tokens précédents. Si, dans l'exemple ci-dessus, le filtre second_synonyms avait appliqué les règles du premier filtre à son ensemble de règles, il aurait remplacé sa propre règled => e par d => f (car il aurait appliqué la règle e => f du filtre précédent). Dans les premières versions d'Elasticsearch, ce comportement a pu être source de confusion. C'est pourquoi les filtres de synonymes sont maintenant ignorés lors du traitement des règles synonymiques du filtre suivant, qui fonctionne comme décrit dans la version 6.6 et suivantes.

Retour vers le futur

Dans ce court article de blog, nous n'avons fait qu'effleurer la surface de tout ce que vous pouvez accomplir grâce aux synonymes ; nous avons aussi tenté de répondre à quelques questions fréquentes concernant leur utilisation. Les synonymes sont des outils puissants, que vous pouvez exploiter pour booster le rappel de votre système de recherche. Mais il est important de connaître et d'expérimenter leurs nombreuses subtilités, notamment en les associant à des tests de pertinence systématiques.

Avec la nouvelle API intégrée à Elasticsearch 7.3, qui vous permet de recharger les analyseurs au moment de la recherche, ce type d'expérimentation devient plus simple : il est désormais inutile de fermer et de rouvrir les index, et vous avez la possibilité de mettre à jour des règles synonymiques appliquées au moment de la rechercher sans devoir déconnecter vos index. Il ne s'agit là que d'une amélioration parmi tant d'autres prévues. L'objectif est de vous faciliter la gestion des synonymes dans les grands clusters et de la rendre plus conviviale. N'hésitez pas à nous dire ce que vous en pensez et à nous laisser des commentaires ou des questions sur notre forum de discussion. D'ici-là, je vous souhaite de bonnes analyses !