06 octobre 2016 Cas Utilisateur

Les leviers d'Elasticsearch pour le traitement des spécificités linguistiques

Par Lucian Precup

"Vos données, votre recherche" (Your Data, Your Search) : c'était l’une des idées à l'origine d'Elasticsearch. Nous partons toujours de ce postulat lors de la mise en place de nos projets Elasticsearch. Notre interprétation est que chaque type de donnée, chaque cas d'utilisation doit être traité de manière spécifique pour avoir la meilleure pertinence et performance possibles. Dans cette série d’articles nous allons nous concentrer sur le traitement du langage humain. Dans le traitement d'une langue, nous ne gérons pas de la même manière les fautes de saisie et les formes des mots (singulier, pluriel, masculin, féminin) tout comme nous ne gérons pas de la même manière les noms de personnes et les contenus éditoriaux. A travers cette série d'articles, nous allons passer en revue les nombreux leviers qu'offre Elasticsearch pour traiter tous les cas d'utilisation virtuellement possibles. Le livre Elasticsearch: The Definitive Guide consacre déjà un chapitre entier aux problématiques du langage humain. Nous prenons un angle de vue différent avec des retours d'expérience et exemples qui finiront avec une implémentation d'extension sur mesure (plugin).

Le traitement du texte

Les trois rôles principaux d'un moteur de recherche sont l'indexation, la recherche mais aussi, et surtout, le traitement du texte. Avant d'être mis dans l'index, un texte est analysé et transformé. Dans le cadre d'une requête, le même traitement (ou un autre) peut être appliqué avant l'envoi de la requête technique à l'index. Nous parlons ici d'un texte destiné à être recherché par saisie de mots clés (autrement dit pour la "recherche full text") car le texte destiné au calcul d'analytiques ne peut pas être transformé avant son indexation (voir le type keyword ou "index": "not analyzed").

L'analyse du texte pour l'auto-complétion

Prenons deux documents en entrée d'Elasticsearch :

{
    "id": 1,
    "nom": "Céline"
}

et

{
    "id": 2,
    "nom": "Celia"
}

Si nous préparons un index utilisé pour des services d'auto-complétion et que nous souhaitons être agnostiques aux accents et aux caractères majuscules éventuellement saisis par l'utilisateur, alors nous pouvons appliquer les transformations suivantes : asciifolding, lowercase et edgeNGram. Pour edgeNGram nous commençons à deux caractères (min_gram: 2, max_gram: 10 par exemple).

Suite à ces transformations, les tokens suivants seront en réalité indexés :

  1. Pour le document 1 : ce cel celi celin celine
  2. Pour le document 2 : ce cel celi celia

Lors d'une recherche, si nous utilisons les requêtes "full text" de type match, multi_match ou simple_query_string, les mêmes transformations s'appliquent en général pour le texte saisi par l'utilisateur. Ce qui fait qu'une recherche Célin sera en réalité une recherche ce cel celi celin. Et ce qui fait que, selon les configurations et les paramètres des requêtes, la recherche Célin remontera les deux documents avec un ordre et un highlighting adéquat :

{
    "id": 1,
    "nom": "Céline"
},
{
    "id": 1,
    "nom": "Celia"
}

Schématiquement ceci se présente de la manière suivante :

analyse_auto_completion.png

Une analyse différente peut être appliquée à la recherche. Si nous enlevons la transformation edgeNGram lors de la recherche (en spécifiant un search_analyzer), le document Celia ne remontera plus lors de la saisie Célin :

analyse_auto_completion_search.png

Analyse du texte avec stemmer

Voici un deuxième exemple d'analyse du texte : le French stemmer. Imaginons que nous avons le document suivant :

{
    "id": 1,
    "metier": "Développeuse"
}

et que nous souhaitons que ce document remonte lors de la recherche "développeur". Le French stemmer nous permet de ramener le mot à sa racine et, par conséquent, de retrouver la forme féminine lors d'une recherche de la forme masculine et vice-versa.

analyse_french_stemmer.png

L'API _analyze

Un excellent outil pour comprendre comment fonctionne l’analyse du texte dans Elasticsearch est l'API _analyze. Attention, si vous utilisez une version plus ancienne d'Elasticsearch, la syntaxe de cette API a (légèrement) changé : voici le lien pour la documentation de la version 1.7.

En utilisant cet outil vous saurez, par exemple, qu'avec l'analyseur french, "pain" et "pains", "développeur" et "développeuse" mais aussi les plus improbables "cheval" et "chevaux" ou "La Croix" et "croissant" sont en fait ramenés à la même forme. Les commandes :

GET _analyze
{
  "analyzer" : "french",
  "text" : "développeur"
}
GET _analyze
{
  "analyzer" : "french",
  "text" : "développeuse"
}

donnent comme résultat

{
  "tokens": [
    {
      "token": "developeu",
      "start_offset": 0,
      "end_offset": 11,
      "type": "<ALPHANUM>",
      "position": 0
    }
  ]
}

L’analyseur « french »

Elasticsearch vient par défaut avec un nombre impressionnant d'analyseurs linguistiques déjà préconfigurés. Ils sont bien documentés et à portée de main pour démarrer un projet ou monter un prototype. Ils sont également configurables et leur documentation vous permet de commencer votre configuration sur mesure car, rappelez-vous, "Vos données, votre recherche". En pratique vous avez tout intérêt à faire votre propre configuration et les différentes fonctionnalités d'Elasticsearch vous permettent de le faire très facilement, presque comme dans un jeu de construction.

L’option stem_exclusion

Prenons l'exemple d'un e-commerçant proposant des produits alimentaires. Si vous êtes une supérette, un supermarché ou un hypermarché vous avez souvent aussi un rayon "Produits d'entretien de la maison". Rappelez vous, le French stemmer va ramener des mots comme "La Croix" et "croissant" à leur "racine" :

GET _analyze
{
  "analyzer" : "french",
  "text" : "La Croix"
}
GET _analyze
{
  "analyzer" : "french",
  "text" : "croissant"
}
{
  "tokens": [
    {
      "token": "croi",
      "start_offset": 0,
      "end_offset": 9,
      "type": "<ALPHANUM>",
      "position": 0
    }
  ]
}

Pour éviter que lors d'une recherche "croissant" les produits d'entretien de la marque "La Croix" remontent (et vice-versa), votre configuration sur mesure consistera à positionner la marque La Croix dans la liste stem_exclusion :

DELETE bzh
PUT bzh
{
  "settings": {
    "analysis": {
      "analyzer": {
        "bzh_french": {
          "type": "french",
          "stem_exclusion": [ "croix" ]
        }
      }
    }
  }
}
GET bzh/_analyze
{
  "analyzer" : "bzh_french",
  "text" : "La Croix"
}
{
  "tokens": [
    {
      "token": "croix",
      "start_offset": 3,
      "end_offset": 8,
      "type": "<ALPHANUM>",
      "position": 1
    }
  ]
}

Dans d'autres cas d'utilisation, nous positionnerons bien entendu d'autres paramètres car la meilleure configuration pour vous dépend de vos données.

Les détails de l'analyseur « french »

L'analyseur _french_ proposé par Elasticsearch par défaut est, en réalité, la combinaison d'une série de Token Filters et d'un Tokenizer. La description détaillée de cet analyseur est documentée ici.

A partir de cet analyseur par défaut, nous pouvons très facilement implémenter le notre. Nous pouvons par exemple configurer le filtre keyword_marker pour traiter la marque "La Croix" et le filtre stemmer_override pour donner notre propre interprétation de la manière dont un mot devrait être ramené à une racine quelconque :

DELETE bzh
PUT bzh
{
  "settings": {
    "analysis": {
      "filter": {
        "bzh_french_elision": {
          "type":         "elision",
          "articles_case": true,
            "articles": [
              "l", "m", "t", "qu", "n", "s",
              "j", "d", "c", "jusqu", "quoiqu",
              "lorsqu", "puisqu"
            ]
        },
        "bzh_french_stop": {
          "type":       "stop",
          "stopwords":  "_french_" 
        },
        "bzh_french_keywords": {
          "type":       "keyword_marker",
          "keywords":   ["Croix"] <img src='/fr/assets/bltfe0c05b31164acaa/1.png' data-sys-asset-uid="bltfe0c05b31164acaa" alt="1.png" style="display:inline;">
        },
        "bzh_french_override": {
          "type":       "stemmer_override",
          "rules": [
            "croissant=>croisan" <img src='/fr/assets/blt22f6a68e53c6b7af/2.png' data-sys-asset-uid="blt22f6a68e53c6b7af" alt="2.png" style="display:inline;">
          ]
        },
        "bzh_french_stemmer": {
          "type":       "stemmer",
          "language":   "light_french"
        }
      },
      "analyzer": {
        "bzh_french": {
          "tokenizer":  "standard",
          "filter": [
            "bzh_french_elision",
            "bzh_french_keywords",
            "lowercase",
            "bzh_french_stop",
            "bzh_french_override",
            "bzh_french_stemmer"
          ]
        }
      }
    }
  }
}

1.png "Protection" d'un mot (épelé avec majuscule) du stemming


2.png Version personnalisée pour le stemming de certains mots

Trois stemmers sont disponibles pour le français : french, light_french, minimal_french. Le stemmer choisi par Elasticsearch pour l'analyseur french par défaut est le light_french. Selon vos données, vous pourriez préférer l'un ou l'autre. Le mieux est de tester vos cas d'utilisation. Cette configuration vous permet de tester tous les stemmers :

DELETE bzh
PUT bzh
{
  "settings": {
    "analysis": {
      "filter": {
        "bzh_french_elision": {
          "type":         "elision",
          "articles_case": true,
            "articles": [
              "l", "m", "t", "qu", "n", "s",
              "j", "d", "c", "jusqu", "quoiqu",
              "lorsqu", "puisqu"
            ]
        },
        "bzh_french_stop": {
          "type":       "stop",
          "stopwords":  "_french_" 
        },
        "bzh_french_keywords": {
          "type":       "keyword_marker",
          "keywords":   ["Croix"]
        },
        "bzh_french_override": {
          "type":       "stemmer_override",
          "rules": [
            "croissant=>croisan"
          ]
        },
        "bzh_french_stemmer": {
          "type":       "stemmer",
          "language":   "light_french" <img src='/fr/assets/bltfe0c05b31164acaa/1.png' data-sys-asset-uid="bltfe0c05b31164acaa" alt="1.png" style="display:inline;">
        },
        "bzh_french_stemmer_min": {
          "type":       "stemmer",
          "language":   "minimal_french" <img src='/fr/assets/blt22f6a68e53c6b7af/2.png' data-sys-asset-uid="blt22f6a68e53c6b7af" alt="2.png" style="display:inline;">
        },
        "bzh_french_stemmer_max": {
          "type":       "stemmer",
          "language":   "french" <img src='/fr/assets/bltf9cec29532f22fa9/3.png' data-sys-asset-uid="bltf9cec29532f22fa9" alt="3.png" style="display:inline;">
        }
      },
      "analyzer": {
        "bzh_french": { <img src='/fr/assets/bltade37c04b2d9ade6/4.png' data-sys-asset-uid="bltade37c04b2d9ade6" alt="4.png" style="display:inline;">
          "tokenizer":  "standard",
          "filter": [
            "bzh_french_elision",
            "bzh_french_keywords",
            "lowercase",
            "bzh_french_stop",
            "bzh_french_override",
            "bzh_french_stemmer"
          ]
        },
        "bzh_french_min": { <img src='/fr/assets/blt89e1f6bc116552f0/5.png' data-sys-asset-uid="blt89e1f6bc116552f0" alt="5.png" style="display:inline;">
          "tokenizer":  "standard",
          "filter": [
            "bzh_french_elision",
            "bzh_french_keywords",
            "lowercase",
            "bzh_french_stop",
            "bzh_french_override",
            "bzh_french_stemmer_min"
          ]
        },
        "bzh_french_max": { <img src='/fr/assets/bltf90c42e70b9d2899/6.png' data-sys-asset-uid="bltf90c42e70b9d2899" alt="6.png" style="display:inline;">
          "tokenizer":  "standard",
          "filter": [
            "bzh_french_elision",
            "bzh_french_keywords",
            "lowercase",
            "bzh_french_stop",
            "bzh_french_override",
            "bzh_french_stemmer_max"
          ]
        }
      }
    }
  }
}

1.png Définition du stemmer light_french 


2.png Définition du stemmer minimal_french


3.png Définition du stemmer french


4.png Analyseur basé sur light_french


5.png Analyseur basé sur minimal_french


6.png Analyseur basé sur le stemmer french

Voici quelques requêtes permettant de tester les différents analyseurs et stemmers ainsi que leur retour :

GET bzh/_analyze
{
  "analyzer" : "bzh_french",
  "text" : "majestueux"
}
{
  "tokens": [
    {
      "token": "majestueu",
      "start_offset": 0,
      "end_offset": 10,
      "type": "<ALPHANUM>",
      "position": 0
    }
  ]
}
GET bzh/_analyze
{
  "analyzer" : "bzh_french_min",
  "text" : "majestueux"
}
{
  "tokens": [
    {
      "token": "majestueu",
      "start_offset": 0,
      "end_offset": 10,
      "type": "<ALPHANUM>",
      "position": 0
    }
  ]
}
GET bzh/_analyze
{
  "analyzer" : "bzh_french_max",
  "text" : "majestueux"
}
{
  "tokens": [
    {
      "token": "majestu",
      "start_offset": 0,
      "end_offset": 10,
      "type": "<ALPHANUM>",
      "position": 0
    }
  ]
}

Conclusion

Nous avons vu à travers de cet article quelques cas d'utilisations de type "full text" et leur configuration la mieux adaptée pour un maximum de pertinence et performance. Elasticsearch dispose d'un grand nombre de fonctionnalités et options permettant le traitement du langage humain et la création d'index spécialisés pour un domaine métier en particulier. La composition et configuration de ces fonctionnalités et options est aussi simple qu'un jeu de construction. Après l'analyseur french, nous détaillerons d'autres fonctionnalités comme l'analyse phonétique et la recherche approximative. Nous verrons aussi comment Elasticsearch est une technologie ouverte permettant la construction de se propres extensions (plugins) pour l'analyse du texte.


Lucian_Precup_420x420_z.png

Lucian Precup est CTO d'Adelean et développe des solutions d'entreprise basées sur la Stack Elastic depuis 2011. Adelean est partenaire intégrateur d'Elastic depuis 2013. Auparavant et avec une équipe de chercheurs de l’INRIA, Lucian a développé des logiciels pour l'intégration de données en temps réel chez Business Objects et SAP.

Cet article est inspiré d'une conférence donnée à Breizhcamp en mars 2016.