ElasticsearchでNGram Tokenizerを試す
ElasticsearchでNGram Tokenizerを試してみたメモです。
ElasticsearchのアナライザーでNGram Tokenizerを試してみました。
Ubuntu上でElasticsearch5.4.0で試してみます。
N-Gram
日本語の文章は英文と違い空白による単語の分割が出来ないので、
何らかの方法で検索できる形に単語を分解する必要があります。
N-Gramは文字数単位(N文字単位)で文章を分割する手法です。
トークナイザ
トークナイザは文字をトークン(単語)に分割して出力します。
デフォルトで色々なトークナイザがあり、NGram Tokenizerはデフォルトで入っています。
Tokenizers | Elasticsearch Reference [5.4] | Elastic
インストール・起動
Elasticsearch5.4.0をインストールします。
NGram Tokenizerはデフォルトで入っているのでインストール不要です。
インストール方法は下記と同様。
Elasticsearch5とKibana5をインストールしてCRUDを試す - abcdefg.....
ホストOSからゲストOS上のElasticsearchにアクセスできるように、
elasticsearch.ymlにゲストOSのIPを設定します。
$ vi /config/elasticsearch.yml network.host : ["10.0.2.15", _local_]
起動。
$ ./elasticsearch
アナライザ・トークナイザ設定
アナライザとトークナイザを設定するためのjsonファイルを用意します。
analyzer
analyzerの項目にはmy_analyzerという名前を設定し、
typeにcustomを設定し、tokenizerにはmy_tokenizerを設定します。
tokenizer
tokenizerの項目にはmy_tokenizerという名前を設定し、
typeにngramを指定します。
min_gramとmax_gramは文字を分割する最小と最大の単位です。
どうやら日本語は2gramか3gramらしいので、それぞれ2と3に設定します。
token_charsにはトークンとして含める対象を配列で指定します。
指定できるのは下記のキャラクタークラスです。
今回はletterとdigitを指定します。
キャラクタークラス | 例 |
---|---|
letter | a, b, ï or 京 |
digit | 3, 7 |
whitespace | " ", "\n" |
punctuation | !, " |
symbol | $, √ |
analysis.jsonというファイル名で保存。
analysis.json
{ "settings": { "analysis": { "analyzer": { "my_analyzer": { "type": "custom", "tokenizer": "my_tokenizer" } }, "tokenizer": { "my_tokenizer": { "type": "ngram", "min_gram": 2, "max_gram": 3, "token_chars": [ "letter", "digit" ] } } } } }
analysis.jsonを指定してmy_indexというインデックスを作成。
curl -X PUT 'localhost:9200/my_index' -d @analysis.json
インデックスを確認してみると、analyzerとtokenizerの項目に
指定したアナライザとトークナイザが設定されていることが分かります。
curl -X GET 'localhost:9200/my_index/?pretty' { "my_index" : { "aliases" : { }, "mappings" : { }, "settings" : { "index" : { "number_of_shards" : "5", "provided_name" : "my_index", "creation_date" : "1495360973104", "analysis" : { "analyzer" : { "my_analyzer" : { "type" : "custom", "tokenizer" : "my_tokenizer" } }, "tokenizer" : { "my_tokenizer" : { "token_chars" : [ "letter", "digit" ], "min_gram" : "2", "type" : "ngram", "max_gram" : "3" } } }, "number_of_replicas" : "1", "uuid" : "HjcLX2GrSo2uteIkNeGCOQ", "version" : { "created" : "5040099" } } } } }
デフォルトのアナライザを設定したい場合は、下記の様に
setting.analysis.analyzer.defaultにアナライザを設定します。
default_analysis.json
{ "settings": { "analysis": { "analyzer": { "default": { "tokenizer": "my_tokenizer" } }, "tokenizer": { "my_tokenizer": { "type": "ngram", "min_gram": 2, "max_gram": 3, "token_chars": [ "letter", "digit" ] } } } } }
確認
アナライザを確認するには/_analyzeでanalyze APIのエンドポイントを指定します。
analyzerでmy_analyzerを指定し、textにアナライズ対象の文章を指定します。
実行してみると、2文字と3文字の単語に分割されていることが分かります。
curl -X POST 'localhost:9200/my_index/_analyze?pretty' -d '{"analyzer": "my_analyzer", "text": "吾輩は猫である。"}' { "tokens" : [ { "token" : "吾輩", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "吾輩は", "start_offset" : 0, "end_offset" : 3, "type" : "word", "position" : 1 }, { "token" : "輩は", "start_offset" : 1, "end_offset" : 3, "type" : "word", "position" : 2 }, { "token" : "輩は猫", "start_offset" : 1, "end_offset" : 4, "type" : "word", "position" : 3 }, { "token" : "は猫", "start_offset" : 2, "end_offset" : 4, "type" : "word", "position" : 4 }, { "token" : "は猫で", "start_offset" : 2, "end_offset" : 5, "type" : "word", "position" : 5 }, { "token" : "猫で", "start_offset" : 3, "end_offset" : 5, "type" : "word", "position" : 6 }, { "token" : "猫であ", "start_offset" : 3, "end_offset" : 6, "type" : "word", "position" : 7 }, { "token" : "であ", "start_offset" : 4, "end_offset" : 6, "type" : "word", "position" : 8 }, { "token" : "である", "start_offset" : 4, "end_offset" : 7, "type" : "word", "position" : 9 }, { "token" : "ある", "start_offset" : 5, "end_offset" : 7, "type" : "word", "position" : 10 } ] }
mapping定義
マッピングでフィールドごとにアナライザを設定できます。
nameとaddressというフィールドを持つfriendsタイプを定義します。
nameとaddressフィールドにはそれぞれanalyzerにmy_analyzerを指定します。
mapping.json
{ "settings": { "analysis": { "analyzer": { "my_analyzer": { "type": "custom", "tokenizer": "my_tokenizer" } }, "tokenizer": { "my_tokenizer": { "type": "ngram", "min_gram": 2, "max_gram": 3, "token_chars": [ "letter", "digit" ] } } } }, "mappings": { "friends": { "properties": { "name": { "type": "string", "index": "analyzed", "analyzer": "my_analyzer" }, "address": { "type": "string", "index": "analyzed", "analyzer": "my_analyzer" } } } } }
先ほど作成したインデックスを削除
curl -X DELETE 'localhost:9200/my_index'
mapping.jsonを指定してmy_indexを再作成。
curl -X PUT 'localhost:9200/my_index/' -d @mapping.json
インデックスを確認してみるとnameとaddressフィールドにmy_analyzerが設定されてるのが分かります。
curl -X GET 'localhost:9200/my_index/?pretty' { "my_index" : { "aliases" : { }, "mappings" : { "friends" : { "properties" : { "address" : { "type" : "text", "analyzer" : "my_analyzer" }, "name" : { "type" : "text", "analyzer" : "my_analyzer" } } } }, "settings" : { "index" : { "number_of_shards" : "5", "provided_name" : "my_index", "creation_date" : "1495365420134", "analysis" : { "analyzer" : { "my_analyzer" : { "type" : "custom", "tokenizer" : "my_tokenizer" } }, "tokenizer" : { "my_tokenizer" : { "token_chars" : [ "letter", "digit" ], "min_gram" : "2", "type" : "ngram", "max_gram" : "3" } } }, "number_of_replicas" : "1", "uuid" : "wcSsZQ0PRey5ayaiAcWZ3A", "version" : { "created" : "5040099" } } } } }
確認
確認用のドキュメントを登録。
"name": "山本太郎"
"address": "東京都港区赤坂1-12-32"
で登録します。
curl -X PUT 'localhost:9200/my_index/friends/1?pretty' -d '{"name": "山本太郎", "address": "東京都港区赤坂1-12-32"}'
simple_query_stringでaddressフィールドを指定して検索してみます。
"東京"で検索。
curl -X GET 'localhost:9200/my_index/_search?pretty' -d ' { "query": { "simple_query_string": { "fields": ["address"], "query": "東京" } } } '
2gramで分割しているので当然ヒットします。
{ "took" : 6, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 1, "max_score" : 0.28004453, "hits" : [ { "_index" : "my_index", "_type" : "friends", "_id" : "1", "_score" : 0.28004453, "_source" : { "name" : "山本太郎", "address" : "東京都港区赤坂1-12-32" } } ] } }
"京都"で検索。
curl -X GET 'localhost:9200/my_index/_search?pretty' -d ' { "query": { "simple_query_string": { "fields": ["address"], "query": "京都" } } } '
こちらもヒットしました。
min_gramで2gramで分割しているので、"東京都"の"京都"がヒットしてしまいます。
{ "took" : 3, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 1, "max_score" : 0.28004453, "hits" : [ { "_index" : "my_index", "_type" : "friends", "_id" : "1", "_score" : 0.28004453, "_source" : { "name" : "山本太郎", "address" : "東京都港区赤坂1-12-32" } } ] } }
"京"で検索。
curl -X GET 'localhost:9200/my_index/_search?pretty' -d ' { "query": { "simple_query_string": { "fields": ["address"], "query": "京" } } } '
min_gramで最小を2gramで分割しているので、1文字の"京"はヒットしません。
{ "took" : 14, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 0, "max_score" : null, "hits" : [ ] } }
"東京都"で検索。
curl -X GET 'localhost:9200/my_index/_search?pretty' -d ' { "query": { "simple_query_string": { "fields": ["address"], "query": "東京都" } } } '
max_gramで3gramで分割しているので、3文字の"東京都"もヒットします。
{ "took" : 12, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }, "hits" : { "total" : 1, "max_score" : 0.84013355, "hits" : [ { "_index" : "my_index", "_type" : "friends", "_id" : "1", "_score" : 0.84013355, "_source" : { "name" : "山本太郎", "address" : "東京都港区赤坂1-12-32" } } ] } }
終わり。