はじめに
Elasticsearchで日本語をうまい具合に検索しようとした場合、通常はN-Gramかkuromojiを使うかと思います。
今回は、このkuromoji、特に辞書のメンテナンスについて書いてみます。
といっても、目新しいことはないですかね・・・
(2019.04.23 _update_by_query
すれば既存のドキュメントも改めてアナライズされる旨を追記)
辞書の読み込みタイミング
辞書っていつ読み込まれるのでしょう。
- Index作成時
- Index作成後の再起動時
- Index作成後にクローズして、再度オープンした時
- Index作成後にフリーズしたあとで、アンフリーズした時
一つ目は、まぁそうですね。二つ目も。三つ目は、二つ目と同じ感じでしょうか。
四つ目は意外でした。フリーズしても検索はできるのですが、その段階では、もし辞書ファイルが書き換わっていても新しい辞書は読み込まれていません。
ですが、アンフリーズすると新しい辞書が読み込まれます。
ま、だから何だって話ですが・・・
辞書を配置しなければならないノード
Elasticsearchは、役割として
- マスターノード
- データノード
- コーディネーターノード
があります(MLノードは無視します)。
この中で辞書を置かなければならないのは、
- マスターノード
- データノード
の二つとなります。
データノードはわかるんですが、マスターノード、特に「マスター」として選出されたノードにも必要です。中身はからでも良いのですが、マスターノードにも配置されていないと、そもそもIndexが作成できません。
ちなみに、データノードに辞書ファイルがない場合は、Indexは作成できますが、IndexのステータスはRedのままとなります(マスターノードでは、エラーが出まくります)。
辞書更新に伴う注意点
運用中に辞書を更新すると、思いがけない検索結果になる可能性があります。
以下のようなIndexを考えて見ます(ここにあるのをちょっと修正したものです)。
PUT kuromoji_sample
{
"settings": {
"index": {
"analysis": {
"tokenizer": {
"kuromoji_user_dict": {
"type": "kuromoji_tokenizer",
"mode": "extended",
"discard_punctuation": "false",
"user_dictionary": "userdict_ja.txt"
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"tokenizer": "kuromoji_user_dict"
}
}
}
}
},
"mappings": {
"_doc": {
"properties": {
"text": {
"type": "text",
"analyzer": "my_analyzer"
}
}
}
}
}
辞書ファイル(userdict_ja.txt
)の中身は空のままとします。
テスト
この状態でテストして見ます。
GET kuromoji_sample/_analyze
{
"analyzer": "my_analyzer",
"text": "東京スカイツリー"
}
{
"tokens" : [
{
"token" : "東京",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "スカイ",
"start_offset" : 2,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "ツリー",
"start_offset" : 5,
"end_offset" : 8,
"type" : "word",
"position" : 2
}
]
}
3つのトークン「東京」「スカイ」「ツリー」に分割されることがわかります。
データ投入
ではデータを投入します。
POST kuromoji_sample/_doc
{
"text": "東京スカイツリー"
}
それでは検索して見ましょう。
検索1
GET kuromoji_sample/_search
{
"query": {
"match": {
"text": "スカイ"
}
}
}
match
クエリなので、クエリに指定された文字列である「スカイ」もanalyzeされますが、結果は「スカイ」となります。そして、Index内には「スカイ」が登録されているので、このクエリの結果は1件となります。
検索2
GET kuromoji_sample/_search
{
"query": {
"match": {
"text": "ツリー"
}
}
}
これは検索1と同様に「ツリー」がIndex内にもあるので結果は1件となります。
検索3
GET kuromoji_sample/_search
{
"query": {
"match": {
"text": "スカイツリー"
}
}
}
「スカイツリー」はanalyzeにより「スカイ」と「ツリー」に分割されます。match
クエリでは、この分割された「スカイ」と「ツリー」はor
条件でIndex内を検索します。
Index内には「スカイ」も「ツリー」もありますので、これも検索結果は1件です。
検索4
GET kuromoji_sample/_search
{
"query": {
"term": {
"text": "スカイ"
}
}
}
これは、まんま検索1と同じです。
検索5
GET kuromoji_sample/_search
{
"query": {
"term": {
"text": "スカイツリー"
}
}
}
これは検索結果が0件となります。
term
クエリはクエリに指定された文字列はanalyzeされませんので、「スカイツリー」がIndex内にあるか検索しますが、テストで見た通り、Index内には「スカイツリー」はありません。
辞書の更新
では、辞書ファイルを以下の内容で更新します。
東京スカイツリー,東京 スカイツリー,トウキョウ スカイツリー,カスタム名詞
このファイルを保存して、kuromoji_sample
をクローズ&オープンします。
そしてテストして見ます。
GET kuromoji_sample/_analyze
{
"analyzer": "my_analyzer",
"text": "東京スカイツリー"
}
{
"tokens" : [
{
"token" : "東京",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "スカイツリー",
"start_offset" : 2,
"end_offset" : 8,
"type" : "word",
"position" : 1
}
]
}
前回のテストとは違い、辞書通りに「東京」と「スカイツリー」という2つに分割されます。
では検索して見ましょう。
新しい辞書を使った検索
といっても、新しい辞書が影響を受けるのはクエリ、もしくは、更新された辞書が適用されてからIndexに登録されたものだけです。
ですが、検索1〜5は結果は以前と同じとなります。
では、以下のような検索ではどうでしょうか。
GET kuromoji_sample/_search
{
"query": {
"match": {
"text": {
"query": "東京スカイツリー",
"operator": "and"
}
}
}
}
これは検索結果が0件となります。新しい辞書のもとではquery
の値である「東京スカイツリー」は「東京」と「スカイツリー」となり、operator
がand
なので、Index内に「東京」と「スカイツリー」があるものを検索します。
しかし、古い辞書のもとでは「東京」「スカイ」「ツリー」がIndexに格納されているのでマッチしません。
マッチさせるには、ここの一番最後に書かれているとおり、_update_by_query
を実行すると改めてアナライズが走るようで、ちゃんと検索結果が1件となります(知らんかった・・・)。
最後に
辞書は途中でメンテナンスすることが必要になるかと思いますが、変な動作を起こさないようクエリには気をつけましょう。
もしくは、ちゃんとreindex
か_update_by_query
しましょう(どっちにしろ、件数が多ければ時間がかかりますが)。