Multilingual search using language identification in Elasticsearch | Elastic Blog
Technique

Recherche multilingue grâce à la détection de la langue dans Elasticsearch

Nous sommes heureux d'annoncer que, dans le cadre du lancement du processeur d'inférence par ingestion du Machine Learning, nous vous proposons une fonctionnalité de détection de la langue dans Elasticsearch 7.6. Pour vous présenter cette nouveauté, nous souhaitons vous décrire quelques cas d'utilisation et stratégies de recherche dans les corpus multilingues mais aussi vous expliquer le rôle de la détection de la langue. Nous avons abordé certains de ces sujets auparavant. Nous faisons donc référence à ce précédent article dans quelques exemples donnés ci-dessous.

Motivation

Dans notre société actuelle largement interconnectée, les documents et autres sources d'informations sont disponibles dans une multitude de langues, ce qui représente un problème pour bien des applications de recherche. Nous devons comprendre la langue de ces contenus afin de les analyser correctement et de garantir la meilleure expérience de recherche possible. Pour ce faire, nous devons au préalable détecter la langue utilisée.

La détection de la langue permet d'améliorer la pertinence globale des recherches dans ces corpus multilingues. Étant donné qu'ils contiennent un ensemble de documents dont nous ne connaissons pas encore la ou les langues, nous devons mener des recherches efficaces à leur sujet. Les documents peuvent comprendre une seule langue ou plusieurs. Le premier cas est courant dans les domaines comme l'informatique où l'anglais est la principale langue de communication. En revanche, dans les textes médicaux et biologiques, la terminologie latine intervient souvent.

En nous basant sur des analyses linguistiques précises, nous pouvons améliorer la pertinence (soit la précision et le rappel) en garantissant les bonnes compréhension, indexation et recherche des termes des documents. Grâce à une suite d'analyseurs linguistiques d'Elasticsearch (qui sont intégrés ou disponibles via des plug-ins), nous offrons une conversion en tokens ainsi qu'un filtrage des tokens et des termes optimisés :

Pour des motifs similaires, la détection de la langue est l'une des premières étapes des pipelines de traitement automatique des langues (TAL) afin d'utiliser des modèles et des algorithmes linguistiques très précis. Par exemple, l'apprentissage de modèles pré-entraînés, comme BERT et ALBERT de Google ou GPT-2 d'OpenAI, se fonde souvent sur des corpus unilingues ou composés d'une langue principale. En outre, ces modèles sont spécialement conçus pour effectuer certains types de tâches, comme la classification de documents, l'analyse du ressenti ou la reconnaissance d’entités nommées (REN).

Dans les stratégies et exemples présentés ci-dessous, si nous ne fournissons aucune indication précise, nous supposons que les documents contiennent une seule langue ou une langue principale.

Avantages de l'analyse linguistique

Pour accentuer la motivation, étudions quelques avantages des analyseurs linguistiques.

Décomposition : en allemand, des mots sont souvent associés pour en créer d'autres, beaucoup plus longs et difficiles à lire. Par exemple, le mot “Jahr” (qui signifie “année”) se retrouve dans “Jahrhunderts” (“siècle”), “Jahreskalender” (“calendrier”) ou “Schuljahr” (“année scolaire”). Sans un analyseur personnalisé capable de décomposer ces mots, il est impossible d'obtenir des documents sur les années scolaires contenant le mot “Schuljahr” simplement en recherchant le terme “jahr”. En outre, l'allemand utilise des règles différentes des autres langues latines concernant le pluriel et le datif. Par exemple, si vous recherchez le mot “jahr”, vous devez obtenir des résultats contenant “Jahre” (pluriel de “jahr”) et “Jahren” (datif pluriel de “jahr”).

Terme commun : certaines langues utilisent une terminologie commune ou spécifique à un domaine. Par exemple le mot “computer” est souvent utilisé tel quel dans d'autres langues que l'anglais. Si nous recherchons “computer”, nous pouvons être intéressés par des documents qui ne sont pas rédigés en anglais. Être en mesure de mener des recherches dans un ensemble connu de langues et trouver des termes communs est un cas d'utilisation intéressant. Utilisons à nouveau l'allemand comme exemple. Imaginons que nous disposons d'un corpus multilingue sur la sécurité informatique, soit “Computersicherheit” en allemand (“sicherheit” signifiant sécurité). Seul un analyseur allemand cherche des correspondances de “computer” en anglais et en allemand.

Alphabets non latins : l'analyseur standard est efficace avec la plupart des langues latines (c'est-à-dire de l'Europe de l'Ouest). Cependant, il montre rapidement ses limites avec les alphabets cyrillique ou chinois, japonais et coréen, par exemple. Dans un précédent article, nous avons étudié la formation des langues chinoise, japonaise et coréenne. Nous en avons donc conclu à la nécessité d'utiliser des analyseurs linguistiques. Par exemple, le coréen utilise des postpositions, c'est-à-dire des suffixes qui modifient le sens des noms et des pronoms. Parfois, l'analyseur standard réussit à trouver des correspondances, mais leurs scores ne sont pas très bons. Autrement dit, vous bénéficiez d'un bon rappel, au détriment de la précision. Dans d'autres cas, l'analyseur standard ne trouve aucune correspondance, au détriment de la précision et du rappel.

Prenons un exemple pratique, à savoir “Jeux olympiques d'hiver”, soit “동계올림픽대회는”, en coréen, un terme composé de “동계” (“hiver”), de “올림픽대회” (“Jeux olympiques” ou “compétition olympique”) et de “는”, la postposition ou le suffixe ajouté au mot indiquant le thème. Si vous recherchez cette expression exacte à l'aide de l'analyseur standard, vous obtenez une correspondance parfaite. En revanche, si vous recherchez “올림픽대회” (ce qui signifie seulement “Jeux olympiques”), vous n'obtenez aucun résultat. Toutefois, si vous utilisez l'analyseur coréen nori, vous obtenez une correspondance, car des tokens ont été générés correctement lors de l'indexation pour l'association “동계올림픽대회는”/”Jeux olympiques d'hiver”.

Premiers pas avec la détection de la langue

Projet de démonstration

Nous avons configuré un petit projet de démonstration afin de vous montrer les cas d'utilisation et les stratégies pour la détection de la langue dans les recherches. Ce projet contient tous les exemples cités dans le présent article mais aussi quelques outils d'indexation et de recherche WiLI-2018, un corpus multilingue, que vous pouvez utiliser comme référence, et des exemples pratiques pour tester la recherche multilingue. Nous vous conseillons d'exécuter le projet de démonstration avec ses documents indexés lorsque vous consultez ces exemples afin de bien les comprendre. Cela peut vous être utile même si ce n'est pas obligatoire.

Pour mener ces expériences, vous pouvez installer Elasticsearch 7.6 sur votre ordinateur ou vous inscrire à un essai gratuit d'Elasticsearch Service.

Premières expériences

La détection de la langue se fonde sur un modèle pré-entraîné intégré à la distribution par défaut d'Elasticsearch. Elle est associée au processeur d'inférence par ingestion en paramétrant lang_ident_model_1 comme identifiant de modèle (model_id) lors de la configuration du processeur dans un pipeline d'ingestion.

{ 
  "inference": { 
    "model_id": "lang_ident_model_1", 
    "inference_config": {}, 
    "field_mappings": {} 
  } 
}

Le reste de la configuration est identique à celle d'autres modèles. Vous pouvez configurer des paramètres comme le nombre de catégories principales à obtenir, le champ de sortie qui contient la prédiction et, le plus important pour nos cas d'utilisation, le champ d'entrée à utiliser. Par défaut dans ce modèle, un champ appelé “text” devrait contenir l'entrée. Dans l'exemple ci-dessous, nous utilisons l'API de pipeline _simulate avec des documents contenant un seul champ. Il associe le champ de contenu d'entrée au champ de texte à des fins d'inférence, ce qui n'influe pas sur les autres processeurs du pipeline. Ensuite, il obtient les trois premières catégories pour inspection.

# simuler une configuration d'inférence de base

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes": 3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents": "Das Leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents": "The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

La sortie indique chaque document et quelques informations supplémentaires dans le champ _ml.lang_ident. Elle comprend la probabilité pour chacune des trois langues principales et la langue principale, qui est stockée dans _ml.lang_ident.predicted_value.

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : "Das Leben ist kein Ponyhof",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" : 0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" : 2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" : 1.130237050226503E-4
                }
              ],
              "predicted_value" : "de",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:38:13.810179Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : "The rain in Spain stays mainly in the plains",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" : 7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 7.968926766495827E-5
                }
              ],
              "predicted_value" : "en",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:38:13.810185Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" : 1.6980752372837307E-5
                }
              ],
              "predicted_value" : "en",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:38:13.810189Z"
        }
      }
    }
  ]
}

Et voilà ! L'allemand est détecté dans le premier document, l'anglais dans le deuxième et le troisième. Même un peu de latin est détecté dans le troisième document.

Stratégies de détection de la langue dans les recherches

Après cet exemple simple de détection de la langue, utilisons nos nouvelles connaissances pour déterminer une stratégie d'indexation et de recherche.

Pour ce faire, nous nous reposons sur deux stratégies d'indexation fondamentales, à savoir par champ linguistique et par index linguistique. Dans la stratégie par champ linguistique, nous créons un seul index à l'aide d'un ensemble de champs linguistiques et utilisons un analyseur adapté à chaque langue concernée. Au moment d'effectuer une recherche, nous pouvons choisir le champ d'une langue connue ou tous les champs, puis choisir celui qui permet d'obtenir les meilleures correspondances. Dans la stratégie par index linguistique, nous créons un ensemble d'index avec différents mappings. Chaque champ indexé est doté d'un analyseur pour la langue concernée. Au moment d'effectuer une recherche, nous pouvons adopter une approche similaire à la stratégie par champ linguistique et choisir un seul index ou plusieurs présentant un modèle d'indexation dans la requête.

Comparez ces deux stratégies à l'aide des données dont vous disposez. Indexez la même chaîne plusieurs fois à l'aide d'un champ ou d'un index avec un analyseur linguistique. Cette approche est susceptible d'être efficace. Toutefois, elle engendre un grand nombre de duplications, ralentit les requêtes et utilise un espace de stockage bien plus important que nécessaire.

Indexation

Étudions en détail chaque stratégie d'indexation séparément étant donné que chacune conditionne les stratégies de recherche que nous pouvons utiliser.

Par champ

Dans la stratégie par champ linguistique, nous utilisons la sortie de la détection de la langue et un éventail de processeurs dans un pipeline d'ingestion afin de stocker le champ d'entrée dans un champ linguistique. Nous prenons en charge un ensemble défini de langues (à savoir l'allemand, l'anglais, le coréen, le japonais et le chinois), car nous devons configurer un analyseur spécifique pour chacune d'entre elles. Tout document qui n'est pas rédigé dans une de ces langues est indexé dans un champ par défaut avec l'analyseur standard.

La définition de l'ensemble du pipeline est disponible dans le projet de démonstration : config/pipelines/lang-per-field.json.

Par conséquent, voici le mapping pour la prise en charge de cette stratégie d'indexation.

{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "contents": {
        "properties": {
          "language": {
            "type": "keyword"
          },
          "supported": {
            "type": "boolean"
          },
          "default": {
            "type": "text",
            "analyzer": "default",
            "fields": {
              "icu": {
                "type": "text",
                "analyzer": "icu_analyzer"
              }
            }
          },
          "en": {
            "type": "text",
            "analyzer": "english"
          },
          "de": {
            "type": "text",
            "analyzer": "german_custom"
          },
          "ja": {
            "type": "text",
            "analyzer": "kuromoji"
          },
          "ko": {
            "type": "text",
            "analyzer": "nori"
          },
          "zh": {
            "type": "text",
            "analyzer": "smartcn"
          }
        }
      }
    }
  }
}

(Dans un souci de concision, la configuration de l'analyseur allemand a été raccourcie dans l'exemple ci-dessus. Elle est disponible en entier dans config/mappings/de_analyzer.json.)

À l'instar de l'exemple précédent, nous utilisons l'API de pipeline _simulate dans celui-ci.

# simuler une stratégie par champ linguistique et obtenir les 3 principales catégories de langues pour inspection

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes": 3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      },
      {
        "rename": {
          "field": "contents",
          "target_field": "contents.default"
        }
      },
      {
        "rename": {
          "field": "_ml.lang_ident.predicted_value",
          "target_field": "contents.language"
        }
      },
      {
        "script": {
          "lang": "painless",
          "source": "ctx.contents.supported = (['de', 'en', 'ja', 'ko', 'zh'].contains(ctx.contents.language))"
        }
      },
      {
        "set": {
          "if": "ctx.contents.supported",
          "field": "contents.{{contents.language}}",
          "value": "{{contents.default}}",
          "override": false
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents": "Das Leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents": "The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents": "オリンピック大会"
      }
    },
    {
      "_source": {
        "contents": "로마는 하루아침에 이루어진 것이 아니다"
      }
    },
    {
      "_source": {
        "contents": "授人以鱼不如授人以渔"
      }
    },
    {
      "_source": {
        "contents": "Qui court deux lievres a la fois, n’en prend aucun"
      }
    },
    {
      "_source": {
        "contents": "Lupus non timet canem latrantem"
      }
    },
    {
      "_source": {
        "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

Voici la sortie avec une stratégie par champ.

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "de" : "Das Leben ist kein Ponyhof",
            "default" : "Das Leben ist kein Ponyhof",
            "language" : "de",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" : 0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" : 2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" : 1.130237050226503E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218641Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "en" : "The rain in Spain stays mainly in the plains",
            "default" : "The rain in Spain stays mainly in the plains",
            "language" : "en",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" : 7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 7.968926766495827E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218646Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "オリンピック大会",
            "language" : "ja",
            "ja" : "オリンピック大会",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ja",
                  "class_probability" : 0.9993823252841599
                },
                {
                  "class_name" : "el",
                  "class_probability" : 2.6448654791599055E-4
                },
                {
                  "class_name" : "sd",
                  "class_probability" : 1.4846805271384584E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218648Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "로마는 하루아침에 이루어진 것이 아니다",
            "language" : "ko",
            "ko" : "로마는 하루아침에 이루어진 것이 아니다",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ko",
                  "class_probability" : 0.9999939196272863
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 3.0431805047662344E-6
                },
                {
                  "class_name" : "am",
                  "class_probability" : 1.710514725818281E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218649Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "授人以鱼不如授人以渔",
            "language" : "zh",
            "zh" : "授人以鱼不如授人以渔",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "zh",
                  "class_probability" : 0.9999810103320087
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 1.0390454083183788E-5
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 2.6302271562335787E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.21865Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "Qui court deux lievres a la fois, n’en prend aucun",
            "language" : "fr",
            "supported" : false
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "fr",
                  "class_probability" : 0.9999669852240882
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 2.3485226102079597E-5
                },
                {
                  "class_name" : "ht",
                  "class_probability" : 3.536708810360631E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218652Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "Lupus non timet canem latrantem",
            "language" : "la",
            "supported" : false
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "la",
                  "class_probability" : 0.614050940088811
                },
                {
                  "class_name" : "fr",
                  "class_probability" : 0.32530021315840363
                },
                {
                  "class_name" : "sq",
                  "class_probability" : 0.03353817054854559
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218653Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "en" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",
            "default" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",
            "language" : "en",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" : 1.6980752372837307E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218654Z"
        }
      }
    }
  ]
}

Comme prévu, des champs en allemand sont stockés dans contents.de, en anglais dans contents.en, en coréen dans contents.ko et ainsi de suite. Nous obtenons aussi quelques exemples dans des langues non prises en charge, comme le français et le latin. Aucun avertissement lié à la prise en charge n'apparaît. Il est possible de mener des recherches dans ces langues dans le champ par défaut uniquement. Vérifiez les principales catégories prévues ainsi que l'exemple en latin. Étudiez la manière dont le modèle détecte, à raison, le latin. Cependant, le modèle n'est pas certain et classe le français en deuxième position.

Il s'agit d'un exemple de base d'un pipeline d'ingestion concernant la détection de la langue. Nous espérons toutefois qu'il vous laisse entrevoir d'autres possibilités. Grâce à la flexibilité de ces pipelines, nous pouvons créer de nombreux autres scénarios. Nous en abordons quelques-uns en détail à la fin de l'article. Dans cet exemple, certaines étapes peuvent être fusionnées ou exclues d'un pipeline de production. Cependant, n'oubliez pas qu'un bon pipeline de traitement des données est celui que vous pouvez facilement lire et comprendre, pas celui comprenant le moins de lignes possible.

Par index

Notre stratégie par index linguistique utilise les mêmes composantes fondamentales que le pipeline de l'autre stratégie. Toutefois, nous utilisons un index différent au lieu de stocker les données dans un champ linguistique. Au moment de l'ingestion, nous pouvons configurer le champ _index d'un document, ce qui nous permet de remplacer la valeur par défaut en configurant un nom d'index linguistique de notre choix. Si la langue concernée n'est pas prise en charge, cette étape est sautée et le document est indexé dans l'index par défaut. Quand on vous dit que c'est simple...

La définition de l'ensemble du pipeline est disponible dans le projet de démonstration : config/pipelines/lang-per-index.json.

Par conséquent, voici le mapping pour la prise en charge de cette stratégie d'indexation.

{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "contents": {
        "properties": {
          "language": {
            "type": "keyword"
          },
          "text": {
            "type": "text",
            "analyzer": "default"
          }
        }
      }
    }
  }
}

Dans ce mapping, nous n'avons précisé aucun analyseur personnalisé. À la place, nous avons utilisé ce fichier comme modèle. Quand nous créons chaque index linguistique, nous configurons l'analyseur pour la langue concernée.

Voici une simulation de ce pipeline.

# simuler une stratégie par index linguistique et obtenir les 3 principales catégories de langues pour inspection

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes": 3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      },
      {
        "rename": {
          "field": "contents",
          "target_field": "contents.text"
        }
      },
      {
        "rename": {
          "field": "_ml.lang_ident.predicted_value",
          "target_field": "contents.language"
        }
      },
      {
        "set": {
          "if": "['de', 'en', 'ja', 'ko', 'zh'].contains(ctx.contents.language)",
          "field": "_index",
          "value": "{{_index}}_{{contents.language}}",
          "override": true
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents": "Das Leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents": "The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents": "オリンピック大会"
      }
    },
    {
      "_source": {
        "contents": "로마는 하루아침에 이루어진 것이 아니다"
      }
    },
    {
      "_source": {
        "contents": "授人以鱼不如授人以渔"
      }
    },
    {
      "_source": {
        "contents": "Qui court deux lievres a la fois, n’en prend aucun"
      }
    },
    {
      "_source": {
        "contents": "Lupus non timet canem latrantem"
      }
    },
    {
      "_source": {
        "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

Voici la sortie avec une stratégie par index.

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index_de",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "de",
            "text" : "Das Leben ist kein Ponyhof"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" : 0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" : 2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" : 1.130237050226503E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486009Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_en",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "en",
            "text" : "The rain in Spain stays mainly in the plains"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" : 7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 7.968926766495827E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486037Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_ja",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "ja",
            "text" : "オリンピック大会"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ja",
                  "class_probability" : 0.9993823252841599
                },
                {
                  "class_name" : "el",
                  "class_probability" : 2.6448654791599055E-4
                },
                {
                  "class_name" : "sd",
                  "class_probability" : 1.4846805271384584E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486039Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_ko",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "ko",
            "text" : "로마는 하루아침에 이루어진 것이 아니다"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ko",
                  "class_probability" : 0.9999939196272863
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 3.0431805047662344E-6
                },
                {
                  "class_name" : "am",
                  "class_probability" : 1.710514725818281E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486041Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_zh",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "zh",
            "text" : "授人以鱼不如授人以渔"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "zh",
                  "class_probability" : 0.9999810103320087
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 1.0390454083183788E-5
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 2.6302271562335787E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486043Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "fr",
            "text" : "Qui court deux lievres a la fois, n’en prend aucun"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "fr",
                  "class_probability" : 0.9999669852240882
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 2.3485226102079597E-5
                },
                {
                  "class_name" : "ht",
                  "class_probability" : 3.536708810360631E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486044Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "la",
            "text" : "Lupus non timet canem latrantem"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "la",
                  "class_probability" : 0.614050940088811
                },
                {
                  "class_name" : "fr",
                  "class_probability" : 0.32530021315840363
                },
                {
                  "class_name" : "sq",
                  "class_probability" : 0.03353817054854559
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486046Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_en",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "en",
            "text" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" : 1.6980752372837307E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.48605Z"
        }
      }
    }
  ]
}

Comme prévu, les résultats en matière de détection de la langue sont identiques à ceux de la stratégie par champ linguistique. La seule différence réside dans la manière dont nous utilisons ces informations dans le pipeline afin de transférer un document dans l'index approprié.

Recherche

Sur la base de ces deux stratégies d'indexation, quelle est la meilleure méthode de recherche ? Comme nous l'avons expliqué, nous avons plusieurs possibilités pour chaque stratégie d'indexation. Une question reste en suspens : comment pouvons-nous indiquer un analyseur linguistique pour la chaîne de recherche afin qu'il corresponde au champ indexé ? Ne vous inquiétez pas. Vous n'avez pas besoin d'indiquer un analyseur particulier au moment de la recherche. Sauf si vous précisez un analyseur de recherche (search_analyzer) dans votre DSL de recherche, la chaîne de recherche est analysée par le même analyseur que le champ de correspondance. À l'instar des exemples par champ linguistique, si des champs sont en anglais et en allemand, la chaîne de recherche sera analysée avec l'analyseur english pour les correspondances avec les champs en anglais et avec l'analyseur german_custom pour les correspondances avec les champs en allemand.

Langue de recherche

Avant d'étudier les stratégies de recherche, il est important de dessiner le contexte de détection de la langue dans la chaîne de recherche de l'utilisateur. Vous vous demandez peut-être pourquoi ne pas détecter la langue dans la chaîne de recherche, puis effectuer une recherche normale dans le champ ou l'index correspondant étant donné que la langue (principale) des documents indexés est désormais connue. Malheureusement, les requêtes de recherche ont tendance à être très très courtes. En 2001, une étude[1] du moteur de recherche Excite a révélé que les recherches moyennes contiennent seulement 2,4 mots. Le temps a passé. Les requêtes en langage naturel et les recherches conversationnelles ont beaucoup changé (lire par exemple l'article consacré à l'utilisation d'Elasticsearch pour mener des recherches dans les corpus multilingues). Toutefois, les requêtes de recherche tendent à rester trop courtes, ce qui ne permet pas de détecter la langue. La plupart des algorithmes de détection de la langue sont le plus efficaces avec plus de 50 caractères[2]. En outre, les requêtes de recherche concernent souvent des noms propres, des noms d'entités ou des noms scientifiques, comme “Justin Trudeau”, “Foo Fighters” ou “plantar fasciitis”. L'utilisateur peut être également intéressé par des documents rédigés dans une langue arbitraire. Or, cette exigence est impossible à connaître simplement en analysant ce type de chaînes de requête.

Par conséquent, nous recommandons de n'utiliser aucune détection de la langue avec les chaînes de requête seules. Si vous voulez vraiment utiliser le langage de requête de l'utilisateur pour sélectionner l'index ou le champ de recherche, il est préférable d'envisager d'autres approches utilisant des informations implicites ou explicites au sujet de l'utilisateur. Par exemple, le contexte implicite peut consister à utiliser le domaine du site web (par exemple, “.com” ou “.de”) ou le lieu d'un App Store où vous avez téléchargé votre application (notamment un App Store américain ou allemand). Dans la plupart des cas, il est plus judicieux de simplement poser la question à l'utilisateur. Sur nombre de sites, il est possible d'en choisir le lieu dès la première consultation. Vous pouvez également envisager de recourir à la recherche à facettes (avec une agrégation de termes) pour les langues des documents afin que l'utilisateur vous indique celles qui l'intéressent.

Par champ

Avec la stratégie par champ, nous obtenons plusieurs sous-champs linguistiques. Nous devons mener la recherche dans tous ces sous-champs simultanément et choisir celui avec le meilleur score. Cette méthode est relativement simple. En effet, dans le pipeline d'indexation, nous configurons uniquement un champ de langue. Ainsi, lorsque nous menons une recherche dans plusieurs champs, un seul est réellement renseigné. Dans ce cas, nous utilisons une requête multi_match avec le paramètre best_fields (par défaut). Cette combinaison est exécutée en tant que requête dis_max. Nous l'utilisons, car nous sommes intéressés par tous les termes correspondant à un seul champ uniquement.

GET lang-per-field/_search 
{ 
  "query": { 
    "multi_match": { 
      "query": "jahr", 
      "type": "best_fields", 
      "fields": [ 
        "contents.de", 
        "contents.en", 
        "contents.ja", 
        "contents.ko", 
        "contents.zh" 
      ] 
    } 
  } 
}

Si vous voulez mener une recherche dans toutes les langues, vous pouvez aussi ajouter le champ contents.default à la requête multi_match. Un avantage de la stratégie par champ est également la possibilité d'utiliser la langue détectée afin d'optimiser les documents, notamment ceux correspondant au lieu ou à la langue de l'utilisateur, comme mentionné précédemment. Cela permet d'améliorer la précision et le rappel, car la stratégie peut être utilisée pour influer directement sur la pertinence. De même, si vous voulez mener une recherche dans une seule langue (lorsque vous connaissez celle de l'utilisateur pour sa requête), nous pouvons simplement utiliser une requête match dans le champ linguistique de cette langue, par exemple contents.de.

Par index

Avec la stratégie par index, nous disposons de plusieurs index linguistiques. Or, chacun d'entre eux a le même nom de champ. Par conséquent, nous pouvons utiliser une seule requête simple et simplement préciser un modèle d'indexation au moment de la recherche.

GET lang-per-index_*/_search 
{ 
  "query": { 
    "match": { 
      "contents.text": "jahr" 
    } 
  } 
}

Si nous voulons mener une recherche dans toutes les langues, nous utilisons un modèle d'indexation correspondant également à l'index par défaut : lang-per-index*. (Remarquez l'absence du tiret de soulignement.) Si nous voulons mener une recherche dans une seule langue, nous pouvons simplement utiliser l'index de cette langue, comme lang-per-index_de.

Exemples

En utilisant les exemples décrits dans la section “Motivation”, nous pouvons tester la recherche dans notre corpus WiLI-2018. Essayez ces commandes dans le projet de démonstration et observez les résultats que vous obtenez.

Décomposition

# trouver des correspondances exactes uniquement du mot “jahr” 
bin/search --strategy default jahr
# correspondances : “jahr”, “jahre”, “jahren”, “jahrhunderts”, etc. 
bin/search --strategy per-field jahr

Terme commun

# trouver des correspondances exactes uniquement du mot “computer”, avec plusieurs langues dans les résultats 
bin/search --strategy default computer
# correspondances contenant des mots allemands composés également : “Computersicherheit” (sécurité informatique) 
bin/search --strategy per-field computer

Alphabets non latins

# analyseur standard obtenant une mauvaise précision et des résultats non pertinents avec “réseau”/”internet” : "网络" 
bin/search --strategy default 网络
# ICU et analyse linguistique obtenant de bons résultats, avec toutefois des scores différents 
bin/search --strategy icu 网络 
bin/search --strategy per-field 网络

Comparatif

Quelle stratégie devriez-vous utiliser ? Il n'existe pas de réponse universelle. Pour vous aider à choisir, voici quelques avantages et inconvénients de chaque approche.

AvantagesInconvénients
Par champ
  • Gestion d'un seul index facile
  • Prise en charge de plusieurs langues par document
  • Source de confiance unique même avec des documents contenant plusieurs langues
  • Capacité d'optimisation des documents selon la langue concernée
  • Requête et mappings plus complexes
  • Performances ralenties par l'augmentation du nombre de langues et de champs pris en charge
Par index
  • Requête simple
  • Recherche rapide (une requête pour chaque index)
  • Possibilité de scaler les index individuellement selon l'usage de la langue
  • Plusieurs index à gérer
  • Prise en charge de l'indexation d'un seul document dans plusieurs index pas facile si plusieurs langues sont utilisées dans le document

S'il est toujours difficile de faire un choix, nous vous recommandons d'essayer les deux stratégies afin d'observer les résultats obtenus avec votre ensemble de données. Si votre ensemble de données comprend des étiquettes de pertinence, vous pouvez également utiliser l'API d'évaluation des classements pour repérer toute différence de pertinence entre les stratégies.

Approches supplémentaires

Nous avons passé en revue deux stratégies de base pour l'utilisation de détection de la langue, l'indexation et la recherche dans un corpus multilingue. En nous appuyant sur la puissance des pipelines d'ingestion, nous pouvons obtenir un vaste éventail de modifications et d'approches supplémentaires. En voici quelques exemples :

  • Mapping de langues à l'alphabet commun dans un seul champ, comme le mapping du chinois, du japonais et du coréen dans un champ cjk, puis utilisation de l'analyseur cjk et mapping des champs en et fr dans un champ latin avec l'analyseur standard (voir examples/olympics.txt).
  • Mapping de langues inconnues ou d'alphabets non latins dans un champ icu et utilisation de l'analyseur icu (voir config/mappings/lang-per-field.json).
  • Utilisation d'une condition de processeur ou d'un processeur de script, puis configuration des diverses langues principales supérieure à un seuil donné dans un champ (à des fins de filtrage/recherche à facettes).
  • Concaténation de plusieurs champs d'un document dans un seul champ afin de détecter la langue et utilisation facultative pour mener une recherche (par exemple, dans un champ all_contents) ou utilisation continue de la stratégie par champ linguistique après détection de la langue (voir examples/simulate-concatenation.txt et examples/simulate-concatenation.out.json).
  • Utilisation d'un processeur de script, sélection de la langue principale uniquement si la première catégorie est supérieure à un seuil donné (comme 60 % ou 50 %) ou est considérablement supérieure à la deuxième catégorie prévue (par exemple, supérieure à 50 % et plus de 10 % supérieure à la deuxième catégorie).

Conclusion

Nous espérons que cet article constitue un bon point de départ pour vos réflexions et vous donne des idées sur la manière dont vous pouvez utiliser la détection de la langue pour vos recherches multilingues. Nous sommes curieux d'avoir votre avis sur ce sujet. Nous vous invitons donc à participer à notre forum de discussion. N'hésitez pas à nous raconter vos utilisations réussies de la détection de la langue ou, au contraire, à nous expliquer les problèmes que vous rencontrez.

Références

  1. Amanda Spink, Dietmar Wolfram, Major B. J. Jansen et Tefko Saracevic, 2001, Searching the Web: The Public and Their Queries, Journal of the American Society for Information Science and Technology, volume 52, numéro 3, p. 226-234.
  2. A. Putsma, 2001, Applying Monte Carlo Techniques to Language Identification.