ファセットに続いて、サジェストをElasticsearchで試してみようと思います。
Elasticsearchでは、Suggesterというものを使うことになるみたいです。
Suggesters | Elasticsearch Reference [6.4] | Elastic
参考になりそうなエントリも、ちらほらと。
RailsでElasticsearch: サジェスト (Suggest) 機能でオートコンプリート - Rails Webook
Elasticsearchで、漢字データでも平仮名でサジェスト取得
elasticsearchのCompletion Suggesterをさわる
Elasticsearchで日本語人名検索を実装した時のまとめ - 炎と硝煙にむせる開発現場から
Suggesterのうち、Completion Suggesterというものがサジェストとしてはよく使われているようです。
こちらを含めて、いくつかSuggersterを使ってみましょう。
準備
インデックス/マッピングの定義は、以下のようにしています。
{ "settings": { "index": { "number_of_shards" : 5, "number_of_replicas" : 1, "analysis": { "tokenizer": { "kuromoji_tokenizer_search": { "type": "kuromoji_tokenizer", "mode": "search", "discard_punctuation" : "true", "user_dictionary" : "userdict_ja.txt" } }, "filter": { "kuromoji_no_romaji_readingform": { "type": "kuromoji_readingform", "use_romaji": false }, "kana_filter": { "type" : "icu_transform", "id": "Katakana-Hiragana" } }, "analyzer": { "kuromoji_analyzer": { "type": "custom", "tokenizer": "kuromoji_tokenizer_search", "filter": ["kuromoji_baseform", "kuromoji_part_of_speech", "cjk_width", "stop", "ja_stop", "kuromoji_stemmer", "lowercase"] }, "kuromoji_reading_analyzer": { "type": "custom", "tokenizer": "kuromoji_tokenizer_search", "filter": ["kuromoji_baseform", "kuromoji_part_of_speech", "cjk_width", "stop", "ja_stop", "kuromoji_stemmer", "kuromoji_no_romaji_readingform", "kana_filter", "lowercase"] } } } } }, "mappings": { "mytype": { "_source": { "enabled": true }, "_all": { "enabled": true }, "properties": { "content": { "type": "string", "store": "yes", "index": "not_analyzed", "copy_to": [ "content_ja", "content_completion_ja", "content_reading_ja", "content_reading_completion_ja" ] }, "content_ja": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_analyzer" }, "content_completion_ja": { "type": "completion", "analyzer": "kuromoji_analyzer" }, "content_reading_ja": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_reading_analyzer", "search_analyzer": "simple" }, "content_reading_completion_ja": { "type": "completion", "analyzer": "kuromoji_reading_analyzer", "search_analyzer": "simple" } } } } }
いくつかサジェスト用にフィールドを用意していますが、そちらはcopy_toでまずは値を入れるようにしています。
フィールドは、それぞれ
- content … 登録した値をそのまま登録
- content_ja … 形態素解析した結果を登録
- content_completion_ja … 形態素解析した結果を登録するが、Completion Suggester用に使う
- content_reading_ja … 登録した値の読み仮名を登録
- content_reading_completion_ja … 登録した値の読み仮名を登録するが、Completion Suggester用に使う
という使い方をします。
なお、ICUのフィルターを使っているので、事前にICU Analysis Pluginをインストールしておきます。今回は、カタカナ→ひらがな変換に使用しています。
# sudo -u elasticsearch /usr/share/elasticsearch/bin/plugin install analysis-icu
こういうデータを放り込みます。
[ { "content": "吾輩は猫である" }, { "content": "我が名は青春のエッセイドラゴン" }, { "content": "下町ロケット" }, { "content": "北斗の拳" }, { "content": "進撃の巨人" } ]
なんでこのデータかなのですが、過去にApache Solrで書いたこちらのエントリのElasticsearch版として書いています。
Apache Solr 5.xのSuggesterを使って、サジェストとDid You Mean? - CLOVER
では、ここから試していってみます。
Term Suggester
最初は、Term Suggester。
Term suggester | Elasticsearch Reference [6.4] | Elastic
単語(term)と編集距離(edit distance)による、シンプルなSuggesterです。
こんな感じで、微妙に誤字ったクエリを用意。
{ "suggest": { "my-suggestion-1": { "text": "ドラコン", "term": { "field": "content_ja", "size": 10 } }, "my-suggestion-2": { "text": "ろけって", "term": { "field": "content_reading_ja", "size": 10 } } } }
この形式の場合は、Search APIを使って実行します。
$ curl -XGET 'http://localhost:9200/myindex/mytype/_search?pretty' -d @[クエリを書いたファイル]
結果。
{ "took" : 698, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 5, "max_score" : 1.0, "hits" : [ { "_index" : "myindex", "_type" : "mytype", "_id" : "AVLaDrunMoVksaujBZ9v", "_score" : 1.0, "_source" : { "content" : "吾輩は猫である" } }, { 〜省略〜 "suggest" : { "my-suggestion-2" : [ { "text" : "ろけって", "offset" : 0, "length" : 4, "options" : [ { "text" : "ろけっと", "score" : 0.75, "freq" : 1 } ] } ], "my-suggestion-1" : [ { "text" : "ドラコン", "offset" : 0, "length" : 4, "options" : [ { "text" : "ドラゴン", "score" : 0.75, "freq" : 1 } ] } ] } }
正しい単語になって戻って来ていますね。
また、以下のような形式にした場合は、Suggest APIを使います。
{ "my-suggestion-1": { "text": "ドラコン", "term": { "field": "content_ja", "size": 10 } }, "my-suggestion-2": { "text": "ろけって", "term": { "field": "content_reading_ja", "size": 10 } } }
こういうコマンドになります。typeはいらないみたいです。
$ curl -XGET 'http://localhost:9200/myindex/_suggest?pretty' -d @[クエリを書いたファイル]
結果。
{ "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "my-suggestion-2" : [ { "text" : "ろけって", "offset" : 0, "length" : 4, "options" : [ { "text" : "ろけっと", "score" : 0.75, "freq" : 1 } ] } ], "my-suggestion-1" : [ { "text" : "ドラコン", "offset" : 0, "length" : 4, "options" : [ { "text" : "ドラゴン", "score" : 0.75, "freq" : 1 } ] } ] }
こちらの場合は、検索結果がレスポンスに含まれません。
Search APIを使う場合でも、sizeを0にしたり
{ "size": 0, "suggest": { "my-suggestion-1": { "text": "ドラコン", "term": { "field": "content_ja", "size": 10 } }, "my-suggestion-2": { "text": "ろけって", "term": { "field": "content_reading_ja", "size": 10 } } } }
search_type=countを使えばよさそう?
$ curl -XGET 'http://localhost:9200/myindex/mytype/_search?pretty&search_type=count'
Completion Suggester
続いて、Completion Suggesterです。前方一致によるSuggesterだそうな。
Completion Suggester | Elasticsearch Reference [6.4] | Elastic
こちらを使う場合は、準備としてフィールドのtypeに「completion」としておく必要があるようです。
先ほどのマッピングで示したCompletion Suggester用に用意したフィールドのtypeも、「completion」としてあります。
"content_completion_ja": { "type": "completion", "analyzer": "kuromoji_analyzer" }, "content_reading_completion_ja": { "type": "completion", "analyzer": "kuromoji_reading_analyzer", "search_analyzer": "simple" }
で、実行するクエリとしてはこんなのを用意。
{ "my-suggest-1": { "text": "我", "completion": { "field": "content_completion_ja" } }, "my-suggest-2": { "text": "吾", "completion": { "field": "content_completion_ja" } }, "my-suggest-3": { "text": "下", "completion": { "field": "content_completion_ja" } }, "my-suggest-4": { "text": "わが", "completion": { "field": "content_reading_completion_ja" } } }
普通に形態素解析した単語と読みに対して、前方一致でヒットさせることを狙います。
結果。
{ "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "my-suggest-1" : [ { "text" : "我", "offset" : 0, "length" : 1, "options" : [ { "text" : "我が名は青春のエッセイドラゴン", "score" : 1.0 } ] } ], "my-suggest-4" : [ { "text" : "わが", "offset" : 0, "length" : 2, "options" : [ { "text" : "吾輩は猫である", "score" : 1.0 }, { "text" : "我が名は青春のエッセイドラゴン", "score" : 1.0 } ] } ], "my-suggest-2" : [ { "text" : "吾", "offset" : 0, "length" : 1, "options" : [ { "text" : "吾輩は猫である", "score" : 1.0 } ] } ], "my-suggest-3" : [ { "text" : "下", "offset" : 0, "length" : 1, "options" : [ { "text" : "下町ロケット", "score" : 1.0 } ] } ] }
うまくいってそうな感じですね。
ここで、ちょっとマッピングの定義を変更。
"mappings": { "mytype": { "_source": { "enabled": true }, "_all": { "enabled": true }, "properties": { "content": { "type": "string", "store": "yes", "index": "not_analyzed", "copy_to": [ "content_ja", "content_reading_ja", "content_reading_completion_ja" ] }, "content_ja": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_analyzer" }, "content_completion_ja": { "type": "completion", "analyzer": "kuromoji_analyzer" }, "content_reading_ja": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_reading_analyzer", "search_analyzer": "simple" }, "content_reading_completion_ja": { "type": "completion", "analyzer": "kuromoji_reading_analyzer", "search_analyzer": "simple" } } } } }
「content_completion_ja」フィールドへのcopy_toを解除して、データを登録し直します。
"copy_to": [ "content_ja", "content_reading_ja", "content_reading_completion_ja" ]
登録するデータ。
[ { "content": "吾輩は猫である", "content_completion_ja": { "input": "吾輩は猫である", "output": "夏目漱石" } } ]
ちょっと形式が変わりました。inputとoutputを取ることができ、inputは登録する値ですが、サジェスト時に得られる値をoutputで指定することができます。
この状態で、こういうクエリを発行すると。
{ "my-suggest-1": { "text": "吾", "completion": { "field": "content_completion_ja" } } }
「吾」で「夏目漱石」がサジェストされます。
{ "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "my-suggest-1" : [ { "text" : "吾", "offset" : 0, "length" : 1, "options" : [ { "text" : "夏目漱石", "score" : 1.0 } ] } ] }
とりあえず、マッピング定義とデータは戻しておきます。
Phrase Suggester
最後は、Phrase Suggester。
Phrase Suggester | Elasticsearch Reference [6.4] | Elastic
こちらも編集距離を使ったSuggesterのようなのですが、Did You Mean?を実現するのに使われるようです。
なんですが…ちょっと今回はうまく使えませんでした。これ系、Solrの時もブログエントリを書く時には失敗したんですよねぇ…。
どこかでデータをたくさん入れて、試してみますか。
一応、こういうクエリを作って
{ "size": 0, "suggest": { "my-suggest-1": { "text": "吾輩は犬である", "phrase": { "field": "content", "size": 10 } } } }
Search APIに投げると動くのですが
"suggest" : { "my-suggest-1" : [ { "text" : "吾輩は犬である", "offset" : 0, "length" : 7, "options" : [ { "text" : "吾輩は猫である", "score" : 0.7842417 } ] } ] } }
これだとTerm Suggesterと変わらないですからねぇ…。
まとめ
ElasticsearchのSuggesterを使って、サジェスト機能をちょっと試してみました。
サジェストというものはけっこう悩ましいイメージがあって、Suggesterみたいな機能を使って実現することもあれば、専用のインデックスを定義して作り込むケースもあるかと思います。
…自分は、Solrでは後者を最近やりました。
Apache Solr 5.xで、サジェストを実装することを考える - CLOVER
こういうエントリも参考になりそうですね。
Elasticsearch キーワードサジェスト日本語のための設計 – Hello! Elasticsearch. – Medium