🌟

OpenSearch 2.13 で 日本語のオートコンプリートが実装しやすくなりました

2024/05/21に公開

まえがき

OpenSearch 2.13 のリリースノートを読んでいたところ、以下の記述を見つけました。

  • Add documentation for kuromoji_completion filter #6699

OpenSearch ドキュメント によると、日本語をローマ字のタームに変換することでオートコンプリートの仕組みを実装しているようです。

Adds Japanese romanized terms to the token stream (in addition to the original tokens). Usually used to support autocomplete on Japanese search terms. Note that the filter has a mode parameter, which should be set to index when used in an index analyzer and query when used in a search analyzer. Requires the analysis-kuromoji plugin. For information about installing the plugin, see Additional plugins.

動作確認

では実際に試していきましょう。本記事では、OSS 版の OpenSearch 2.13 に Kuromoji プラグインをインストールして動作を確認しています。

まず、テスト用のインデックスを作成します。

PUT kuromoji_completion_sample
{
  "mappings": {
    "properties": {
      "suggestion": {
        "type": "completion",
        "analyzer": "kuromoji_completion_index",
        "search_analyzer": "kuromoji_completion_query",
        "preserve_separators": false,
        "preserve_position_increments": true,
        "max_input_length": 20
      }
    }
  },
  "settings": {
    "index": {
      "number_of_shards": "1",
      "analysis": {
        "analyzer": {
          "kuromoji_completion_index": {
            "mode": "index",
            "type": "kuromoji_completion"
          },
          "kuromoji_completion_query": {
            "mode": "query",
            "type": "kuromoji_completion"
          }
        }
      }
    }
  }
}

次に、サジェスト機能を試すためのドキュメントを追加していきます。

PUT kuromoji_completion_sample/_doc/1
{
  "suggestion": ["東京都"]
}

PUT kuromoji_completion_sample/_doc/2
{
  "suggestion": ["鳥取県"]
}

PUT kuromoji_completion_sample/_doc/3
{
  "suggestion": ["北海道"]
}

PUT kuromoji_completion_sample/_doc/4
{
  "suggestion": ["千葉県"]
}

ではサジェストを行ってみましょう。"と" と入力した場合の結果を見ていきます。

GET kuromoji_completion_sample/_search
{
  "suggest": {
    "suggest": {
      "prefix": "と",
      "completion": {
        "field": "suggestion"
      }
    }
  }
}
{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 0,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "suggest": {
    "suggest": [
      {
        "text": "と",
        "offset": 0,
        "length": 1,
        "options": [
          {
            "text": "東京都",
            "_index": "kuromoji_completion_sample",
            "_id": "1",
            "_score": 1,
            "_source": {
              "suggestion": [
                "東京都"
              ]
            }
          },
          {
            "text": "鳥取県",
            "_index": "kuromoji_completion_sample",
            "_id": "2",
            "_score": 1,
            "_source": {
              "suggestion": [
                "鳥取県"
              ]
            }
          }
        ]
      }
    ]
  }
}

東京都と鳥取が出てきました。では、t とだけ入力した場合はどうでしょうか?

GET kuromoji_completion_sample/_search
{
  "suggest": {
    "suggest": {
      "prefix": "t",
      "completion": {
        "field": "suggestion"
      }
    }
  }
}
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 0,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "suggest": {
    "suggest": [
      {
        "text": "t",
        "offset": 0,
        "length": 1,
        "options": [
          {
            "text": "千葉県",
            "_index": "kuromoji_completion_sample",
            "_id": "4",
            "_score": 1,
            "_source": {
              "suggestion": [
                "千葉県"
              ]
            }
          },
          {
            "text": "東京都",
            "_index": "kuromoji_completion_sample",
            "_id": "1",
            "_score": 1,
            "_source": {
              "suggestion": [
                "東京都"
              ]
            }
          },
          {
            "text": "鳥取県",
            "_index": "kuromoji_completion_sample",
            "_id": "2",
            "_score": 1,
            "_source": {
              "suggestion": [
                "鳥取県"
              ]
            }
          }
        ]
      }
    ]
  }
}

今度は千葉県も出てきましたね。
では "トtt" で検索してみます。

GET kuromoji_completion_sample/_search
{
  "suggest": {
    "suggest": {
      "prefix": "トtt",
      "completion": {
        "field": "suggestion"
      }
    }
  }
}
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 0,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "suggest": {
    "suggest": [
      {
        "text": "トtt",
        "offset": 0,
        "length": 3,
        "options": [
          {
            "text": "鳥取県",
            "_index": "kuromoji_completion_sample",
            "_id": "2",
            "_score": 1,
            "_source": {
              "suggestion": [
                "鳥取県"
              ]
            }
          }
        ]
      }
    ]
  }
}

鳥取県だけがヒットしました。このようにかな/カナと英字が織り交じっていてもサジェストが機能することが分かります。

Analyze API を使ってみてみると、各トークンがローマ字に変換されてインデクシングされることが分かります。サジェスト時にクエリテキストに対しても同様の処理を実行することで、前方一致によるサジェストを実現していることがうかがえます。

POST _analyze
{
 "text": ["鳥取県"],
 "analyzer": "kuromoji_completion"
}
{
  "tokens": [
    {
      "token": "鳥取",
      "start_offset": 0,
      "end_offset": 2,
      "type": "word",
      "position": 0
    },
    {
      "token": "tottori",
      "start_offset": 0,
      "end_offset": 2,
      "type": "word",
      "position": 0
    },
    {
      "token": "県",
      "start_offset": 2,
      "end_offset": 3,
      "type": "word",
      "position": 1
    },
    {
      "token": "ken",
      "start_offset": 2,
      "end_offset": 3,
      "type": "word",
      "position": 1
    },
    {
      "token": "kenn",
      "start_offset": 2,
      "end_offset": 3,
      "type": "word",
      "position": 1
    }
  ]
}

注意事項

簡単にオートコンプリート機能が実装できて楽ですが、ローマ字変換の精度は Kuromoji 自体の辞書に依存します。Kuromoji がデフォルトで参照している辞書は 2007 年でメンテナンスが止まっており、新しい固有名詞を処理することはできません。
このため、"鬼滅の刃" などは正しくサジェストすることができません。

POST _analyze
{
 "text": ["鬼滅の刃"],
 "analyzer": "kuromoji_completion"
}
{
  "tokens": [
    {
      "token": "鬼",
      "start_offset": 0,
      "end_offset": 1,
      "type": "word",
      "position": 0
    },
    {
      "token": "oni",
      "start_offset": 0,
      "end_offset": 1,
      "type": "word",
      "position": 0
    },
    {
      "token": "滅",
      "start_offset": 1,
      "end_offset": 2,
      "type": "word",
      "position": 1
    },
    {
      "token": "metu",
      "start_offset": 1,
      "end_offset": 2,
      "type": "word",
      "position": 1
    },
    {
      "token": "metsu",
      "start_offset": 1,
      "end_offset": 2,
      "type": "word",
      "position": 1
    },
    {
      "token": "の",
      "start_offset": 2,
      "end_offset": 3,
      "type": "word",
      "position": 2
    },
    {
      "token": "no",
      "start_offset": 2,
      "end_offset": 3,
      "type": "word",
      "position": 2
    },
    {
      "token": "刃",
      "start_offset": 3,
      "end_offset": 4,
      "type": "word",
      "position": 3
    },
    {
      "token": "ha",
      "start_offset": 3,
      "end_offset": 4,
      "type": "word",
      "position": 3
    }
  ]
}

対策としては、Kuromoji のカスタム辞書を登録することです。
辞書の方で適切にカナを指定することで、正しいサジェストが可能になります。

POST _analyze
{
 "text": ["鬼滅の刃"],
  "tokenizer": {
      "type": "kuromoji_tokenizer",
      "mode": "extended",
      "user_dictionary_rules": [
        "鬼滅の刃,鬼滅の刃,キメツノヤイバ,カスタム名詞"
      ]
  },
  "filter": [
    {
      "type": "kuromoji_completion"
    }
  ]
}
{
  "tokens": [
    {
      "token": "鬼滅の刃",
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 0
    },
    {
      "token": "kimetunoyaiba",
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 0
    },
    {
      "token": "kimetsunoyaiba",
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 0
    }
  ]
}

Discussion