Elasticsearchの言語特定機能を多言語検索に活用する

Elasticsearch 7.6のリリースで新たに、機械学習推論インジェストプロセッサーと、言語特定機能が加わりました。この機会に、多言語コーパスを検索するいくつかのユースケースや戦略について、またそこで言語特定の機能が果たす役割について、ご紹介したいと思います。以前にもこの領域のトピックについて、いくつかのブログ記事を投稿しました。今回取り上げる事例には、その内容を発展させているものもあります。

背景

グローバルな交流が活発になった現在、私たちが扱うドキュメントやその他の情報ソースもさまざまな種類の言語で書かれています。多くの検索アプリにとってこの現象は、問題を提起するものです。適切な分析を行って可能な限り最高の検索エクスペリエンスを提供するには、ドキュメントがどの言語で書かれているか、極力把握しなくてはなりません。そこで“言語特定”の出番です。

言語特定は、そのような多言語コーパスの全体的な検索関連性を向上させるために使われる機能です。たとえば、どの言語を含むかわからない一連のドキュメントがあるとして、このドキュメントを効率的に検索するにはどうすればよいか、を考えます。またドキュメントに含まれる言語は1つとは限らず、複数である可能性もあります。1種類の言語を含むドキュメントは、たとえば、英語がコミュニケーション言語として優越的に使われるコンピューターサイエンス分野などでよく見られます。他方、ラテン語の語彙が英語の中に散在するドキュメントは、生物学や医学分野のテキストによく見られます。

言語固有の分析を適用すると、ドキュメントの用語を確実に理解し、適切にインデックスと検索を行って、関連性(適合率と再現率の両方)を向上させることができます。Elasticsearchの各種言語固有アナライザー(内蔵アナライザーと、追加して使用できるプラグインのアナライザー)を使えば、言語により適したトークナイゼーション、トークンフィルタリング、用語フィルタリングを適用できます。

  • ストップワードと同義語のリスト
  • 単語形式の正規化:ステミングとレンマ化
  • 分解(例:ドイツ語、オランダ語、韓国語)

上述のようなメリットから、言語特定機能は自然言語処理(NLP)パイプラインの最初の処理ステップでも使われており、言語特定の高精度な、言語固有のアルゴリズムとモデルが活用されています。たとえばGoogleのBERTALBERTや、OpenAIのGPT-2といった事前教育済みのNLPモデルは一般的に、言語別、または特定の言語が大半を占める言語コーパスで教育され、ドキュメント分類や感情分析、固有表現抽出(NER)等のタスク向けに微調整されています。

今回の記事で扱う事例や戦略では、特に記載がない限り、ドキュメントは1種類の言語、または大半が特定の言語で記述されている前提とします。

言語固有の分析を行うメリット

今回ご紹介する手法を推奨する具体的な理由の1つとして、言語固有アナライザーがもたらすいくつかのメリットについてご説明します。

分解:ドイツ語では、複数の名詞を複合させて別の名詞を作り、長く美しい、しかし読みにくい複合語にすることがよくあります。簡単な例を挙げると、“Jahr”(年)という単語と他の単語が組み合わった、“Jahrhunderts”(世紀)や“Jahreskalender”(年間カレンダー)、“Schuljahr”(学年)などの名詞があります。このような複合語を分解するカスタムアナライザーがなければ、“Jahr”(年)を検索して“Schuljahr”(学年)に関するドキュメントを返すことは不可能です。さらにドイツ語は、複数形と与格に関して、他のラテン語と規則が異なります。つまり、“Jahr”(年)を検索する場合は、“Jahre”(複数形)と”Jahren”(複数形の与格)にも一致させる必要があります。

一般的な用語:言語によっては、一般的な用語や、領域専門の外来語をそのまま使うことがあります。たとえば“computer”(コンピューター)という単語は、しばしば他の言語でもそのまま使われます。また、“computer”と入力して検索するとき、英語以外のドキュメントにも関心がある、という可能性もあります。主要な一連の言語にわたって検索可能であり、かつ一般的な単語に一致させるというユースケースには、興味深い側面があります。もう一度、ドイツ語の例で説明しましょう。ここに、複数の言語を含む、コンピューターセキュリティについてのドキュメントがあるとします。ドイツ語でコンピューターセキュリティは“Computersicherheit”(“sicherheit”が“セキュリティ”、あるいは“安全性”の意)ですが、この単語を検索して、英語とドイツ語の双方で“computer”に一致させることができるのは、ドイツ語のアナライザーだけです。

ラテン文字を使わない言語:Elasticsearchのスタンダードアナライザーは、ラテン文字を使う言語(西ヨーロッパ言語)のほとんどで非常にうまく動作します。ところがキリル文字CJK(中国語、日本語、韓国語)など、ラテン文字を使わない言語では、とたんに調子が出なくなります。以前のブログ記事のシリーズでは、CJK言語の形式と、言語固有のアナライザーの必要性についてご説明しました。たとえば韓国語には後置詞があり、名詞や代名詞に付け足された接尾辞によって言葉の意味が変わります。そのためスタンダードアナライザーを使って検索した単語を一致させることができても、一致のスコアリングがうまくいかないことがあります。つまり、ドキュメント上の再現率が高いのに適合率は低い、という結果になることがあります。他のケースでは、スタンダードアナライザーがどの単語も一致させることができず、適合率も再現率も低いという結果になることもあります。

“Winter Olympics”(冬季オリンピック)という単語の動作例を見てみましょう。韓国語で“Winter Olympics”は“동계올림픽대회는”、“동계”が“冬季”で“올림픽대회”が“オリンピック”、または“オリンピックの試合”、最後の“는”はこのトピックの後置詞です。つまりこの単語に付加された接尾辞であり、トピックを示します。スタンダードアナライザーの場合、この文字列をそのまま検索すると完全一致しますが、“올림픽대회”、つまり“オリンピック”だけで検索すると何もヒットしません。ところが、韓国語アナライザーのnoriを使うと、“オリンピック”の検索で一致を得ることができます。これは、インデックス時に“동계올림픽대회는”、つまり“冬季オリンピック”という語が適切にトークナイズされていたためです。

言語特定機能を使いはじめる

デモプロジェクト

検索に言語特定機能を使うユースケースと戦略をわかりやすく説明するために、今回、小規模なデモプロジェクトをセットアップしました。こちらに本ブログ記事で取り上げるすべての事例を掲載したほか、多言語コーパスWiLI-2018をインデックス、および検索するためのツール類も置いています。多言語検索に関する実験の参考資料および、動作例としてお使いいただけます。以下の例を参照する際は、ドキュメントをインデックスして、デモプロジェクトを立ち上げておくと、ご自身で再現しながら進められるので便利です(必須というわけではありません)。

Elasticsearchを使った検索の実験は、Elasticsearch 7.6をローカルにインストールして、あるいはElasticsearch Serviceで無料のトライアルを利用して、行っていただくことができます。

実験その1

言語特定は、Elasticsearchのデフォルト配布パッケージに含まれる事前教育済みモデルであり、推論インジェストプロセッサーとともに使用します。インジェストパイプラインで推論プロセッサーを立ち上げる際、model_idlang_ident_model_1に指定することで、使用できます。

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

設定の残りの部分は他のモデルと同じです。出力するトップクラスの数や、推定を含むアウトプットフィールド、そしてこのユースケースで最も重要となる、使用するインプットフィールドなどを指定できます。このモデルはデフォルトで、textというフィールドをインプットとして利用します。以下は、いくつかの単フィールドドキュメントにpipeline _simulate APIを使用した例です。このAPIは推論のために、インプットのcontentsフィールドをtextフィールドにマッピングします。このマッピングによって、パイプライン中の他のプロセッサーが影響を受けることはありません。次に、APIは調査用に上位3つのクラスを出力します。

# 基本的な推論のセットアップをシミュレーションする
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"
      }
    }
  ]
}

アウトプットは各ドキュメントを表示しているほか、_ml.lang_identフィールドについていくつか付加的な情報を提供しています。情報には上位3つの各言語の確率と最上位の言語が含まれます。最上位の言語は_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"
        }
      }
    }
  ]
}

成功したようです。1つ目のドキュメントはドイツ語、2つ目と3つ目のドキュメントは英語で、3つ目のドキュメントにはラテン語の要素も含まれていましたが、英語と特定できました。

検索に言語特定を活用する戦略

ここまで基礎的な言語特定の例を確認してきました。次に、インデックスと検索に言語特定を活用する戦略について考えてみましょう。

今回は、基本的な2つの戦略を使用します。1つが“フィールド別言語”、もう1つが“インデックス別言語”戦略です。フィールド別言語戦略では、一連の言語固有フィールドを持つ単一のインデックスを作成し、各言語向けにカスタマイズされたアナライザーを使用します。検索を行う際は、既知の言語フィールドを検索するよう指定することも、すべての言語フィールドを検索するよう指定した後、最も一致するフィールドを選ぶこともできます。インデックス別言語戦略では、マッピングの異なる言語固有のインデックスを複数作成し、インデックスされたフィールドにその言語のアナライザーを設定します。検索時のアプローチは、フィールド別言語戦略の場合と似ています。単一の言語インデックスを検索するか、あるいはインデックスパターンを持つ検索リクエストで、複数のインデックスを検索するよう指定します。

この2つの戦略と、現在多くのユーザーが行っている手順は対照的です。同じ文字列を何度もインデックスし、そのたびにフィールドやインデックスに言語固有のアナライザーを適用するというやり方です。このアプローチが成立しないわけではありませんが、恐ろしく大量の複製が生じ、クエリが遅くなる上、使用するストレージ容量も大幅に増えます。

インデックス

2つのインデックス戦略を詳しく見てみましょう。インデックス戦略を説明すると、今回使用する検索の戦略も説明することができます。

フィールド別

フィールド別言語戦略を採用する場合、インジェストパイプラインで言語特定のアウトプットと一連のプロセッサーを使用し、インプットフィールドを言語固有のフィールドに格納します。各言語に固有のアナライザーをセットアップする必要があるため、サポートできる言語は限られています(ドイツ語、英語、韓国語、日本語、中国語をサポート)。サポート対象外の言語のドキュメントは、スタンダードアナライザーでデフォルトのフィールドにインデックスされます。

パイプラインの完全な定義は、デモプロジェクト(config/pipelines/lang-per-field.json)で確認いただけます。

このインデックス戦略をサポートするマッピングは次のようになります。

{
  "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"
          }
        }
      }
    }
  }
}

(簡潔に説明するため、上の例ではドイツ語アナライザーの設定が省略されています。設定はconfig/mappings/de_analyzer.jsonをご覧ください)

前回同様、パイプラインの_simulate APIを使って探索します。

# フィールド別言語をシミュレーションし、上位3つの言語クラスをアウトプットして調べます
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"
      }
    }
  ]
}

フィールド別言語のアウトプットはこのようになります。

{
  "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"
        }
      }
    }
  ]
}

期待通りの結果になりました。ドイツ語フィールドがcontents.deに、英語がcontents.enに、韓国語がcontents.koに格納されました。サポート対象外の言語である、フランス語とラテン語の例もいくつか混ざっています。フランス語とラテン語にはサポートのフラグがなく、デフォルトのフィールドでしか検索できないことがわかります。ラテン語の例で、上位の予測クラスも確認しましょう。このモデルは、この言語をラテン語と判断したようです。正しい判断ですが、モデルは確信を持つことができず、2番目に強力な予測としてフランス語を挙げています。

これは言語特定機能をインジェストパイプラインで使用する基本的な事例ですが、この事例から、言語特定機能で“できること”のイメージをつかんでいただけたのではないかと思います。インジェストパイプラインはフレキシブルに設定でき、多様なシナリオに対応します。本記事の最後で、いくつか別のシナリオをご紹介します。運用パイプラインでは、この事例の手順を組み合わせたり、省略することができます。ただし、簡単に読めて、理解できることがすぐれたデータ処理パイプラインの条件です。行数を可能な限り最小にすることではありませんので、その点に留意してください。

インデックス別

インデックス別言語戦略も、フィールド別言語のパイプラインと同じ基本的な構成の記述を使用します。先ほどと大きく異なるのが、言語固有のフィールドに格納するのではなく、別のインデックスを使用する点です。別のインデックスへの格納が可能になるのは、投入時にドキュメントに_indexフィールドを設定することでデフォルト値の上書きが可能になり、言語固有のインデックス名を設定できるためです。サポート対象外の言語の場合はこの手順をスキップし、ドキュメントはデフォルトのインデックスにインデックスされます。とてもシンプルです。

パイプラインの完全な定義は、デモプロジェクトconfig/pipelines/lang-per-index.json)で確認いただけます。

このインデックス戦略をサポートするマッピングは次のようになります。

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

マッピングではカスタムアナライザーが指定されておらず、このファイルをテンプレートとして使用する点に留意してください。言語固有の各インデックスを作成する際、 その言語に対応するアナライザーを設定します。

パイプラインをシミュレーションします。

# インデックス別言語をシミュレーションし、上位3つの言語クラスをアウトプットして調べます
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"
      }
    }
  ]
}

インデックス別言語のアウトプットはこのようになります。

{
  "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"
        }
      }
    }
  ]
}

言語特定の結果は期待した通り、フィールド別戦略と同じになりました。違いは、ドキュメントを正しいインデックスに送る際、パイプラインで情報を使う方法だけです。

検索する

この2つのインデックス戦略を採用したとき、ベストな検索方法はどのようなものになるでしょうか。ご説明したように、各インデックス戦略にはいくつかのオプションがあります。よくある質問の1つに、「どのようにクエリの文字列に言語固有のアナライザーを指定して、インデックス済みフィールドと一致させればよいか」というものがあります。この心配は不要です。検索時に、特別なアナライザーを指定する必要はありません。クエリDSLでsearch_analyzerを指定しない限り、クエリ文字列を一致させるフィールドと同じアナライザーで分析されます。フィールド別言語の事例で見たように、たとえばenとdeのフィールドがある場合、クエリの文字列をenのフィールドと一致させるときに英語のアナライザーが、deのフィールドと一致させるときにgerman_custom analyzerで分析されます。

クエリ言語

検索の戦略について詳しく検討する前に、ユーザーのクエリ文字列における言語特定の背景を理解しておくことが大切です。この記事をお読みいただいている方はいま、「インデックスしたドキュメントの(優勢な)言語をすでに把握しているのだから、クエリの文字列についても言語特定を行って、対応するフィールドやインデックスに普通の検索をかければよいのでは?」と思っているかもしれません。残念ながら、検索クエリの文字列は短いものとなる傾向があります。かなりの短さです。話は2001年にさかのぼりますが、古き良きWeb検索エンジンのExciteが行った調査[1]によれば、平均的なユーザークエリに含まれる単語数はわずか2.4でした。これは古い調査であり、また会話式の検索と自然言語クエリ(例:「多言語コーパスの検索にElasticsearchを使う方法」)とではかなり相違があります。しかしそうした違いを考慮してもなお、検索クエリは言語を特定するには短すぎる傾向にあります。実際のところ、多くの言語特定アルゴリズムは、50文字以上ある場合に最適に動作します[2]。この問題に加え、検索はしばしば固有名詞やエンティティ名、学術名で行われます。たとえば“Justin Trudeau”(ジャスティン・トルドー)とか、“Foo Fighters”(フー・ファイターズ)、“plantar fasciitis”(足底筋膜炎)といった単語で検索されます。ユーザーはある特定の言語のドキュメントを求めている可能性がありますが、このようなクエリの文字列を分析しても、それが何の言語なのか把握することは不可能です。

こうした理由から、(あらゆるタイプの)言語特定機能をクエリ文字列にのみ使用することは推奨されません。どうしてもユーザーのクエリ言語を使って検索フィールドやインデックスを選択したいという場合、ユーザーに関する黙示的な、または明示的な情報を活用するという別のアプローチを検討することが最善です。黙示的なコンテクストとして、たとえばWebサイトのドメイン(.comや.jpなど)、アプリがダウンロードされたストアの地域(米国のストアや、日本のストアなど)を使うことがあります。しかし多くの場合に最適なやり方は、ユーザーに直接たずねることです。多くのサイトは、はじめてサイトを閲覧する新規ユーザー向けに地域選択メニューを用意しています。Termsアグリゲーションを使ったドキュメント言語のファセッティングで、ユーザーが関心を持つ言語にナビゲートできるようにするやり方もあります。

フィールド別

フィールド別言語戦略では、複数の言語のサブフィールドを使用します。したがってサブフィールドをすべて同時に検索し、スコアリングが最上位のフィールドを選出する必要があります。これは比較的簡単に実施できます。インデキシングパイプラインには、単一の言語フィールドしか設定されていないためです。つまり、複数のフィールドに対して検索をかけているときも、実際にはそのうちの1つのフィールドしか使われていません。これを実行するには、multi_matchクエリをbest_fieldsタイプ(デフォルト)で使用します。この組み合わせはdis_maxクエリとして実行されます。目的は、すべてのフィールドではなく、単一のフィールドにすべての用語を一致させることだからです。

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

すべての言語を検索したい場合、contents.defaultフィールドにmulti_matchクエリを追加することもできます。フィールド別戦略の長所の1つは、特定した言語を使ってドキュメントをブーストできることです。たとえば上述のような方法でユーザーの言語や地域を特定し、それに一致するドキュメントをブーストすることができます。言語や地域は関連性に直接的な影響を与えうることから、このようなブーストは適合率と再現率の双方を向上させることができます。単一の言語に対して検索をかけたい場合も同様です。たとえば、ユーザーのクエリ言語が判明している場合、その言語の言語フィールド(contents.deなど)でシンプルにmatchクエリを使用することができます。

インデックス別

インデックス別戦略を採用する場合は、複数の言語のインデックスを用意して、各インデックスに同じフィールド名を持たせます。つまり検索リクエストを行うときは、単一のシンプルなクエリを使用し、インデックスパターンを指定するだけです。

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

すべての言語で検索をかけたい場合は、デフォルトのインデックスにもマッチするインデックスパターン、lang-per-index*を使用します(下線がつかないところがポイントです)。単一の言語で検索をかけたい場合は、シンプルに、その言語のインデックス(lang-per-index_deなど)を使用します。

実践例

この記事の冒頭、「背景」のセクションで説明した例を使って、WiLI-2018コーパスを検索してみます。ここまでに登場したコマンドをデモプロジェクトで使い、結果を確認してみましょう。

分解:

# "jahr"という用語への厳密な一致のみ 
bin/search --strategy default jahr
# "jahr"、"jahre"、"jahren"、"jahrhunderts"などに一致させる
bin/search --strategy per-field jahr

一般的な用語:

# "computer"という用語に厳密に一致させ、結果に複数の言語を含める 
bin/search --strategy default computer
# "Computersicherheit"(コンピューターセキュリティ)のような、ドイツ語の複合語に一致させる 
bin/search --strategy per-field computer

ラテン文字を使わない言語:

# "网络"("ネットワーク"または"インターネット")にスタンダードアナライザーを使うと、適合率は低く、無関係ないし無一致の結果が返される 
bin/search --strategy default 网络
# ICUと言語固有分析で適切な結果になるが、スコアは異なる 
bin/search --strategy icu 网络 
bin/search --strategy per-field 网络

比較

実際のところ、この2つの戦略のどちらを使うべきでしょうか。その答えは、状況によります。以下に、各アプローチのメリットとデメリットをまとめています。判断の参考にしてみてください。

メリットデメリット
フィールド別
  • インデックスが1つで、管理が簡単
  • 1つのドキュメントに含まれる複数の言語をサポート
  • ドキュメントに複数の言語が含まれる場合も、信頼できる唯一の情報源となる
  • 言語に基づいてドキュメントをブーストできる
  • マッピングとクエリが複雑
  • サポート言語とフィールドの数が増えるほどパフォーマンスが低速化する
インデックス別
  • クエリがシンプル
  • 各インデックスでクエリを受け取るため、検索スピードが速い
  • 使用言語に基づいてインデックスをここにスケールできる
  • 複数のインデックスを管理する必要がある
  • ドキュメントで複数の言語が併用されている場合、単一のドキュメントを複数のインデックスにインデックスする簡単な手順がない

この表を見ても判断がつかない場合、両方のアプローチを試し、各戦略でデータセットがどのように処理されるか確認されることをお勧めします。お手元に関連性ラベルのデータセットがある場合は、順位付け評価APIを使って各戦略の関連性の違いを確認することができます。

その他のアプローチ

本記事は、言語特定機能を使う2つの戦略を紹介し、また多言語コーパスをインデックス、および検索しました。インジェストパイプラインの能力を活用して、他にもさまざまなアプローチを実践したり、アプローチを応用したりできます。以下は、その一部の例です。

  • script-common言語を単一のフィールドにマッピングする(例:中国語、日本語、韓国語をcjkフィールドにマッピングしてcjkアナライザーを使い、enfrlatinフィールドにマッピングしてstandardアナライザーを使う。詳しくはexamples/olympics.txtを参照。)
  • 未知の言語、またはラテン文字を使わない言語はicuフィールドにマッピングし、icuアナライザーを使用する。(詳しくは、config/mappings/lang-per-field.jsonを参照。)
  • プロセッサー条件またはスクリプトプロセッサーを使用し、閾値を上回る上位の言語を(ファセッティング/フィルタリング用に)フィールドに設定する。
  • ドキュメントの複数のフィールドを1つのフィールドに結合して言語を特定し、検索のオプションにも使用する(例:all_contentsフィールド)か、あるいは言語を特定した後、引き続き"フィールド別言語"戦略を採用する(詳しくは、examples/simulate-concatenation.txtおよびexamples/simulate-concatenation.out.jsonを参照。)
  • スクリプトプロセッサーを使い、最上位のクラスが閾値を超える(例:60%、または50%の)場合、あるいは予測された2番目のクラスに比較して顕著に値が大きい(例:50%以上かつ2番目のクラスより10%以上大きい)場合にのみ優越的な言語を指定する。

まとめ

本記事を、多言語検索で言語特定機能をうまく活用するきっかけや、ヒントとして役立てていただければ幸いです。Elasticは何よりユーザーの意見を重視しています。ぜひディスカッションフォーラムをご活用ください。言語特定がうまくいったユースケースや、お困りの点などについてお聞かせください。お待ちしております。

参照資料

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