menu icon

Expédition vers Synonym Graph dans Elasticsearch

Dans cet article, nous expliquons comment nous sommes passés des anciens filtres de synonymes d'Elasticsearch aux nouveaux filtres de type graphe, les Synonym Graph Token Filter.

Expédition vers Synonym Graph dans Elasticsearch

Pour améliorer notre moteur e-commerce a2 basé sur Elasticsearch, nous avons décidé de remplacer l’ancien filtre de synonymes par le nouveau filtre synonym_graph. Cet article revient sur les problématiques que nous avons rencontrées lors de ce changement.

Pour améliorer un moteur de recherche, une technique classique consiste à utiliser des synonymes.

“Télé” = “Television” = “Ecran”

En général, les synonymes servent à augmenter le recall du moteur, c.a.d. récupérer des documents qui nous auraient échappé.

Depuis ses toutes premières versions, Elasticsearch propose le filtre synonym, mais des problèmes surviennent lorsque l’on veut utiliser des synonymes avec plusieurs termes, et plus particulièrement lorsque le nombre de ces termes n’est pas égal entre les synonymes.

“USA” = “United States” = “United States of America”

En effet, Lucene ne possède pas la structure de données adéquate pour stocker ces cas particuliers et un synonyme avec plus de mots que l’original va “se ranger” avec d’autres termes dans le texte indexé. Le moteur de recherche va alors produire des résultats inattendus et difficiles à analyser.

Pour pallier à ce problème, Elasticsearch (via les nouvelles versions de Lucene) propose désormais un filtre nommé synonym_graph. Pour les raisons de stockage citées ci-dessus, il est impossible d’utiliser ce filtre à l’indexation. Ces filtres sont donc uniquement utilisables à la recherche.

Le moteur e-commerce a2 d’Adelean, comme beaucoup de moteurs basés sur Lucene, utilisait historiquement les synonymes à l’indexation, il fallait donc à la fois changer de type de filtre et passer ce nouveau filtre à la recherche.

Premiers pas et le paramètre auto_generate_synonyms_phrase_query

Une première surprise fut de voir que l’ajout de certains synonymes multi-termes à la recherche entraînait la baisse du nombre de résultats. Etonnant ! Car comme dit précédemment, lorsque l’on ajoute des synonymes, on s’attend plutôt à ramener plus de documents !

Ainsi, pour trois simples documents dans un index standard :

PUT test_synographs/_doc/1
{
  "name": "jus orange"
}
PUT test_synographs/_doc/2
{
  "name": "jus avec orange"
}
PUT test_synographs/_doc/3
{
  "name": "orangeade"
}

Une simple requête :

GET test_synographs/_search
{
  "query": {
    "multi_match": {
      "query": "jus orange",
      "fields": [
        "name"
      ],
      "operator": "and"
    }
  }
}

donne :

"hits" : [
  {
   "_id" : "2",
   "_score" : 0.5753642,
   "_source" : {
     "name" : "jus avec orange"
    }
  },
  {
   "_id" : "1",
   "_score" : 0.5753642,
   "_source" : {
     "name" : "jus orange"
    }
 }
]

Jusqu’ici, le comportement est celui attendu. Pour récupérer le dernier document, ajoutons maintenant le synonyme

“jus orange” = “orangeade”

DELETE test_synographs
PUT test_synographs
{
  "settings": {
    "analysis": {
      "filter": {
        "synonyms_graph": {
          "type": "synonym_graph",
          "synonyms": [
            "jus orange,orangeade"
          ]
        }
      },
      "analyzer": {
        "simple": {
          "filter": [
          ],
          "tokenizer": "standard"
        },
        "with_synonyms": {
          "filter": [
            "synonyms_graph"
          ],
          "tokenizer": "standard"
        }
      }
    }
  },
  "mappings": {
    "_doc": {
      "dynamic": "false",
      "properties": {
        "name": {
          "type": "text",
          "analyzer": "simple",
          "search_analyzer": "with_synonyms"
        }
      }
    }
  }
}

A notre grande surprise, la même requête que précédemment renvoie toujours deux documents ! Et pas les mêmes !

"hits": [
    {
      "_id": "1",
      "_score": 0.5753642,
      "_source": {
        "name": "jus orange"
      }
    },
    {
      "_id": "3",
      "_score": 0.2876821,
      "_source": {
        "name": "orangeade"
      }
    }
  ]

Nous avons certes récupéré le document “orangeade” mais perdu le document n°2 “jus avec orange” en cours de route.

En fouillant la documentation Elasticsearch, nous trouvons la raison. Une requête multi-match possède une option auto_generate_synonyms_phrase_query à true par défaut. Ce qui veut dire qu’une phrase query est automatiquement ajoutée à notre requête initiale pour les synonymes définis sur le champ requêté. Un choix étonnant de la part d’Elasticsearch !

Il suffit de modifier légèrement notre requête pour enfin obtenir les trois documents :

GET test_synographs/_search
{
  "query": {
    "multi_match": {
      "query": "galettes de chaise",
      "fields": [
        "name"
      ],
      "operator": "and",
      "auto_generate_synonyms_phrase_query": "false"
    }
  }
}

Cohabitation avec les autres filtres

Si l’on veut utiliser le Synonym Graph Token avec d’autres filtres dans notre chaîne d’analyse, il est conseillé de le mettre en dernier dans la liste. De cette manière les filtres précédents seront également appliqués aux synonymes.

Par exemple, si vous utilisez la lemmatisation pour que tomates devienne tomate, mettre le filtre de synonymes en dernier vous permettra d’écrire ce genre de règles :

“fruit => orange, pomme, poire”

au lieu de

“fruit, fruits => orange, oranges, pomme, pommes, poire, poires”

C’est également valable pour les minuscules, supprimer les accents, etc..

C’est particulièrement utile dans notre moteur e-commerce, car la liste des synonymes est éditée par des équipes métiers qui ne connaissent pas le fonctionnement technique d’Elasticsearch. De plus, mettre toutes les formes d’un mot (avec et sans majuscule, avec et sans accent, au pluriel, au singulier) serait un travail de titan extrêmement rébarbatif.

En revanche, nous rencontrons rapidement un problème avec les stopwords: Elasticsearch ne nous laisse pas générer un tel index :

PUT test_synographs
{
  "settings": {
    "analysis": {
      "filter": {
        "stopword_filter": {
          "type": "stop",
          "ignore_case": true,
          "stopwords": [
            "un",
            "une"
          ]
        },
        "synonyms_graph": {
          "type": "synonym_graph",
          "synonyms": [
            "jus orange,une orangeade"
          ]
        }
      },
      "analyzer": {
        "simple": {
          "filter": [],
          "tokenizer": "standard"
        },
        "with_synonyms": {
          "filter": [
            "stopword_filter",
            "synonyms_graph"
          ],
          "tokenizer": "standard"
        }
      }
    }
  },
  "mappings": {
    "_doc": {
      "dynamic": "false",
      "properties": {
        "name": {
          "type": "text",
          "analyzer": "simple",
          "search_analyzer": "with_synonyms"
        }
      }
    }
  }
}

En effet, un message d’erreur apparaît dès qu’un synonyme contient un des stopwords de la liste. Le bug n’a toujours pas trouvé de solution aujourd’hui (avril 2021). La solution consistant à inverser l’ordre ne génère pas d’erreurs, mais les synonymes multi termes se remettent à générer des résultats incohérents.

Pour ce problème, nous avons donc été contraints de passer à la moulinette cette liste de synonymes pour y supprimer les stopwords.

Crossfield et Search analyzers

A ce moment-là, notre moteur de recherche commence à fonctionner relativement correctement. Seul souci, certains documents censés remonter, sont toujours absents.

Notre moteur a2 utilise des requêtes dites crossfield : si je cherche “jus orange", le moteur peut renvoyer un document possédant “jus” dans un champ et “orange” dans l’autre. Or, crossfield ne semble pas fonctionner correctement. Il ne s’agit pas, cette fois-ci, d’un bug d’Elasticsearch. En effet, crossfield s’applique uniquement entre des champs possédant le même search analyzer. Et nous avons défini des listes de synonymes différentes selon les champs. Ce qui veut dire des search analyzers différents !

Gardons l’exemple de notre document avec “jus” dans un champ et “orange” dans l’autre. Si j’ai une liste de synonymes pour le champ contenant “jus", et une autre pour le champ contenant “orange", ma requête multi-match crossfield avec “jus orange” ne sera pas capable de retrouver ce document !

La solution a été de rassembler tous les synonymes et de tous les appliquer sur tous les champs. Une petite perte fonctionnelle, mais moins dommageable que la perte de nos documents.

Notre moteur fonctionne désormais correctement pour tout type de synonymes !

Des requêtes plus complexes

Il faut quand même rajouter une dernière petite remarque sur les performances : si mettre les synonymes à l’indexation peut entraîner une augmentation de la taille des index, les mettre à la recherche peut générer des requêtes plus complexes. C’est particulièrement vrai pour les recherches avec plusieurs termes si ces derniers ont chacun un très grand nombre de synonymes définis.

Il nous est ainsi arrivé de dépasser le nombre maximal de requêtes booléennes Lucene sous-jacentes (dont le maximum par défaut est 1024), nous obligeant à repérer ces requêtes complexes (très rares heureusement) et à les simplifier dynamiquement.

Nous n’avons pas noté de ralentissement sensible pour la très grande majorité des requêtes, en passant aux synonymes à la recherche.

Nous espérons que cet article vous permettra de passer plus facilement au Synonym Graph Token Filter et de résoudre un problème classique sur tous les moteurs de recherche basés sur Lucene : les synonymes multi-termes. Si toutefois vous avez besoin d’assistance n’hésitez pas à nous contacter.