見出し画像

Solr 9 日本語オートコンプリートを試してみた

こんにちは、ネコ派メタラーです。ナビタイムジャパンで地点検索基盤の開発・運用を担当しています。

この記事では、Solr 9 で使用可能になった日本語オートコンプリート機能について、使い方と性質を調査したことをお話しします。Solr を使ったことがある方を対象としています。

背景

地点検索チームでは、フリーワード入力に対して地点を返却する機能を提供しています。できるだけ少ない入力操作で簡単に地点を選択いただくため、オートコンプリート技術は特に重要視して開発しています。

当社ではオートコンプリートシステムとして Apache Solr を使用する機会が多いのですが、Lucene 9.0.0 で日本語オートコンプリート向けの機能が取り込まれ、Solr でもバージョン 9 から使用できるようです。

当社では Solr と内製のプログラムの組み合わせでオートコンプリートを実装していますが、この新機能によって内製プログラムから脱却できる可能性があります。新しいオートコンプリート機能のアイデアにも使えると思いましたので、当社での活用に期待して試してみることにしました。

今回は「東京」「東京タワー」「東京ドーム」など地点に関連する表現を登録し、それを入力している途中の文字列 (「とうきょ」など) に対して表現を補完できるかどうか調べてみました。

設定

この検証は「日本語用オートコンプリートのためのAnalyzer」を参考にさせていただきました。参考記事は Elasticsearch に向けたものですが、本稿は同等の内容を Solr で実現するものになります。

今回の検証では『Amazon ECR Public Gallery』より Solr 9.1.0 イメージを使用しました。

フィールド設計

日本語オートコンプリート機能として、JapaneseCompletionFilter というクラスが追加されたとのことです。このフィルターを設定した field type を作成しました。

{
  "add-field-type": {
    "name": "completion_ja",
    "positionIncrementGap": "100",
    "class": "solr.TextField",
    "indexAnalyzer": {
      "charFilters": [],
      "tokenizer": {
        "class": "solr.JapaneseTokenizerFactory",
        "mode": "normal"
      },
      "filters": [
        {
          "class": "solr.CJKWidthFilterFactory"
        },
        {
          "class": "solr.LowerCaseFilterFactory"
        },
        {
          "class": "solr.JapaneseCompletionFilterFactory",
          "mode": "INDEX"
        }
      ]
    },
    "queryAnalyzer": {
      "charFilters": [],
      "tokenizer": {
        "class": "solr.JapaneseTokenizerFactory",
        "mode": "normal"
      },
      "filters": [
        {
          "class": "solr.CJKWidthFilterFactory"
        },
        {
          "class": "solr.LowerCaseFilterFactory"
        },
        {
          "class": "solr.JapaneseCompletionFilterFactory",
          "mode": "QUERY"
        }
      ]
    }
  }
}

JapaneseCompletionFilter にはインデックスモードとクエリモードがありますので、それぞれ設定しました。クエリモードでは入力途中のひらがなや英字を直前のトークンと結合する動きをするようです。たとえば「東京タワー」を入力する途中に生じる「東京たわ」という文字列を解析してみると、「たわ」の部分が「た」「わ」の形態素に分かれますが、クエリ側では入力途中部分「たわ」をまとめて 1 トークンとする動きがみられました。

インデックスモードとクエリモードの違い

補完対象の語句を登録するフィールドも作成しておきます。なお、先程作成した field type はこのフィールドには設定せず、後述する検索コンポーネントから呼び出します。

{
  "add-field": {
    "name": "terms_ja",
    "type": "strings",
    "multiValued": true
  }
}

検索設定

オートコンプリートでは入力途中の文字列で検索することから、トークンに完全一致させる通常の全文検索でヒットさせることは困難です。今回はトークンへの前方一致で検索できる SuggestComponent を用いることにしました。

検索コンポーネントを以下のように作成しました。

{
  "add-searchcomponent": {
    "name": "suggest",
    "class": "solr.SuggestComponent",
    "suggester": {
      "name": "japaneseSuggester",
      "lookupImpl": "AnalyzingLookupFactory",
      "field": "terms_ja",
      "preserveSep": "false",
      "dictionaryImpl": "DocumentDictionaryFactory",
      "payloadField": "id",
      "suggestAnalyzerFieldType": "completion_ja"
    }
  }
}

先ほど作成した field type を suggestAnalyzerFieldType に設定しています。公式ドキュメントに従い、サジェストに使うフィールドの解析は最低限にとどめ、AnalyzingLookupFactory で詳細な解析方法を設定しました。

You usually want a minimal amount of analysis on the field (no stemming, no synonyms, etc.), so an option is to create a field type in your schema that only uses basic tokenizers or filters. (中略)
If using the AnalyzingLookupFactory as your lookupImpl, however, you have the option of defining the field type rules to use for index and query time analysis.

Suggester - Apache Solr Reference Guide

検索用のリクエストハンドラは以下のように作成しました。

{
  "add-requesthandler": {
    "name": "/suggest",
    "class": "solr.SearchHandler",
    "defaults": {
      "suggest": true,
      "suggest.count": 10,
      "suggest.dictionary": "japaneseSuggester"
    },
    "components": ["suggest"]
  }
}

実験結果

以下のデータセットを投入し、どのような補完ができるかを確認しました。

[
  {"id": "1", "terms_ja": ["東京タワー", "東京", "タワー"]},
  {"id": "2", "terms_ja": ["京都タワー", "京都", "タワー"]},
  {"id": "3", "terms_ja": ["東京ドーム", "東京", "ドーム"]},
  {"id": "4", "terms_ja": ["横浜スタジアム", "横浜", "スタジアム"]},
  {"id": "5", "terms_ja": ["阪神甲子園球場", "甲子園", "球場"]}
]

できたこと

入力途中として想定される文字列をクエリとして渡し、補完候補として想定するものが得られるか調べてみました。コア名は example とし、事前に /suggest?suggest.build=true でデータをビルドしてあります。

$ curl -sf "http://localhost:8983/solr/example/suggest" --data-urlencode suggest.q=とうきょ 
{
  "responseHeader":{
    "status":0,
    "QTime":19},
  "suggest":{
    "japaneseSuggester":{
      "とうきょ":{
        "numFound":3,
        "suggestions":[{
            "term":"東京",
            "weight":0,
            "payload":"1"},
          {
            "term":"東京ドーム",
            "weight":0,
            "payload":"3"},
          {
            "term":"東京タワー",
            "weight":0,
            "payload":"1"}]}}}}
  • 「とうきょ」→「東京」「東京ドーム」「東京タワー」を補完

  • 「とうきょうど」→「東京ドーム」

  • 「東京ど」→「東京ドーム」

  • 「東京d」→「東京ドーム」

  • 「きょう」→「京都」「京都タワー」

  • 「たわ」→ 「タワー」

  • 「sutaj」→「スタジアム」

  • 「sutaz」→「スタジアム」

  • 「阪神こうし」→「阪神甲子園球場」

  • 「はんしんこうし」→「阪神甲子園球場」

上記の例では入力途中の文字列に対して表現を補完することができました。「sutaj」「sutaz」の表記ゆれを補完できる点、「阪神こうし」「はんしんこうし」のように途中変換の表記ゆれを補完できる点が、日本語オートコンプリートならではの機能で便利だと思いました。

できなかったこと

モバイルアプリケーションではフリック入力が用いられることがありますが、「スタジアム」のフリック入力中に現れる「すたし」では補完することができませんでした。 当社サービスはモバイルアプリケーションが多いので、このケースとどう向き合うかは引き続き検討したいところです。

まとめ

日本語オートコンプリートは考慮すべきことが多く難易度が高いですが、特別なプログラムを書かなくても Solr だけで実現できる点は魅力的だと思いました。PC で利用することが多いサービスであれば、本機能だけでもクエリオートコンプリートを実現できそうです。当社の地点オートコンプリート機能を Solr に委譲することも引き続き検討していきたいと思います。

Suggester は今回初めて使用しました。今回はシンプルな使い方を確認しましたが、lookupImpl や dictionaryImpl のオプションを変更したり、重み付けを調整するなど発展の余地がありそうです。

今後もユーザーがストレスなく地点を選択できる検索基盤を提供できるよう、新しい技術の調査を続けていきたいと思います。