Elasticsearch の nested 型のハイライト
本投稿は、一つ前の「Elasticsearch多言語化その2」の補足。
ゴール
やりたい事は、以下のようなフィールドに対する検索結果のハイライトをすること。
- nested 型で、中身は複数の “attachment” 型
- attachment 型の “content” フィールドには、content.{ja,en,…} というサブフィールドを作成し、各言語毎の analyzer で処理する
マッピング定義は以下の通り。(elastic4s の DSL をそのままコピペしたが、まぁ大体理解してもらえるかと。)
val messageMapping = mapping("message").fields( "title_v3" typed StringType fields( "en" typed StringType analyzer "english", "ja" typed StringType analyzer "ja_kuromoji_neologd", ), "body_v3" typed StringType fields( "en" typed StringType analyzer "english", "ja" typed StringType analyzer "ja_kuromoji_neologd", ), "attached_files_v3" nested ( "attached_file" typed AttachmentType fields ( "content" typed StringType fields( "en" typed StringType analyzer "english" termVector ("with_positions_offsets") store (true), "ja" typed StringType analyzer "ja_kuromoji_neologd" termVector ("with_positions_offsets") store (true), ) ) ) includeInRoot (true) )
環境は以下の通り
- Elasticsearch 2.3.5
- Elasticsearch Mapper Attachments プラグインを使用
- elastic4s というライブラリ経由で Scala から使用
TL; DR
先に結論を書いておくと、以下の2つがうまくいった。
- nested 型のフィールドに include_in_root を指定し、検索時には通常のクエリーを実行
- 通常の nested 型のフィールドに対し、nested query を実行し、inner hits でハイライト対象フィールドを指定
どちらの場合も、 “term_vector”: “with_positions_offsets” と “store”: “true” は、attached_file.content.{ja,en} につける。
上手く行かなかったのは以下の方法。
- nested query を実行し、highlight query でも nested query を実行
以下、詳細に説明していく。
実際に試してみた結果
事前準備
テスト用に新規でインデックスを作成する(本番のものとは若干異なる)。マッピング定義としてはこんな感じ。
- test_title フィールド
- staring 型
- ja, en のサブフィールド付き
- test_attached_files_1 フィールド
- nested 型
- include_in_root: true
- test_attached_files_2 フィールド
- nested 型
- include_in_root: false (デフォルト値)
test_attached_files_1 と 2 の下に、attachment 型の attached_file という名前のフィールドがくる。 attached_file.content も ja, en のサブフィールドを定義する。
ちょっと長いが、マッピング定義の JSON は以下の通り。
PUT /test_attachment { "mappings": { "test_message": { "properties": { "test_title": { "type": "string", "fields": { "ja": { "type": "string", "analyzer": "ja_kuromoji", "term_vector": "with_positions_offsets", "store": true }, "en": { "type": "string", "analyzer": "english", "term_vector": "with_positions_offsets", "store": true } } }, "test_attached_files_1": { "type": "nested", "include_in_root": true, "properties": { "attached_file": { "type": "attachment", "fields": { "content": { "type": "string", "fields": { "ja": { "type": "string", "analyzer": "ja_kuromoji", "term_vector": "with_positions_offsets", "store": true }, "en": { "type": "string", "analyzer": "english", "term_vector": "with_positions_offsets", "store": true } } } } } } }, "test_attached_files_2": { "type": "nested", "properties": { "attached_file": { "type": "attachment", "fields": { "content": { "type": "string", "fields": { "ja": { "type": "string", "analyzer": "ja_kuromoji", "term_vector": "with_positions_offsets", "store": true }, "en": { "type": "string", "analyzer": "english", "term_vector": "with_positions_offsets", "store": true } } } } } } } } } } }
テストデータを投入。
POST /test_attachment/test_message { "test_title": "テストタイトル for testing", "test_attached_files_1": [ { "attached_file": { "_name": "test.txt", "_content": "44OG44K544OI5aSq6YOO44Gu5YaS6Zm6" } } ], "test_attached_files_2": [ { "attached_file": { "_name": "test.txt", "_content": "44OG44K544OI5aSq6YOO44Gu5YaS6Zm6" } } ] }
include_in_root + 通常のクエリー
test_attached_files_1 は、 include_in_root が true になっていて、test_attached_files_2 は、false になっているが、 test_attached_files_1 に通常の(nested ではない)クエリーを投げると、ハイライトも期待通りになる。
クエリーは以下の通り。
{ "from" : 0, "size" : 10, "query" : { "multi_match" : { "query" : "テスト", "fields" : [ "test_title*", "test_attached_files_1.attached_file.content.*" ] } }, "explain" : true, "highlight" : { "fields" : { "test_title.*": {}, "test_attached_files_1.attached_file.content.*" : { } } } }
結果は以下の通り。
{ "took": 10, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 1, "max_score": 0.47062817, "hits": [ { "_shard": 1, "_node": "lfXU5hJeQ_iwtuEwZgbhMg", "_index": "test_attachment", "_type": "test_message", "_id": "AVjKLMBxQVf672jImhPa", "_score": 0.47062817, "_source": { "test_title": "テストタイトル for testing", "test_attached_files_1": [ { "attached_file": { "_name": "test.txt", "_content": "44OG44K544OI5aSq6YOO44Gu5YaS6Zm6" } } ], "test_attached_files_2": [ { "attached_file": { "_name": "test.txt", "_content": "44OG44K544OI5aSq6YOO44Gu5YaS6Zm6" } } ] }, "highlight": { "test_attached_files_1.attached_file.content.ja": [ "<em>テスト</em>太郎の冒険\n" ], "test_title.ja": [ "<em>テスト</em>タイトル for testing" ], "test_attached_files_1.attached_file.content.en": [ "<em>テスト</em>太郎の冒険\n" ] } } ] } }
nested query + inner hits
この方法は、include_in_root の値に関わらず(今回の例だと test_attached_files_1 と 2 の両方で)上手くいった。
クエリーはこちら。nested query に inner hits でハイライトを指定している。
{ "from" : 0, "size" : 10, "query" : { "nested" : { "query" : { "multi_match" : { "query" : "テスト", "fields" : [ "test_attached_files_2.attached_file.content.*" ] } }, "path" : "test_attached_files_2", "inner_hits": { "highlight": { "fields": { "test_attached_files_2.attached_file.content.*": {} } } } } } }
結果
{ "took": 7, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 1, "max_score": 0.70273256, "hits": [ { "_index": "test_attachment", "_type": "test_message", "_id": "AVjKLMBxQVf672jImhPa", "_score": 0.70273256, "_source": { "test_title": "テストタイトル for testing", "test_attached_files_1": [ { "attached_file": { "_name": "test.txt", "_content": "44OG44K544OI5aSq6YOO44Gu5YaS6Zm6" } } ], "test_attached_files_2": [ { "attached_file": { "_name": "test.txt", "_content": "44OG44K544OI5aSq6YOO44Gu5YaS6Zm6" } } ] }, "inner_hits": { "test_attached_files_2": { "hits": { "total": 1, "max_score": 0.70273256, "hits": [ { "_index": "test_attachment", "_type": "test_message", "_id": "AVjKLMBxQVf672jImhPa", "_nested": { "field": "test_attached_files_2", "offset": 0 }, "_score": 0.70273256, "_source": { "attached_file": { "_name": "test.txt", "_content": "44OG44K544OI5aSq6YOO44Gu5YaS6Zm6" } }, "highlight": { "test_attached_files_2.attached_file.content.ja": [ "<em>テスト</em>太郎の冒険\n" ], "test_attached_files_2.attached_file.content.en": [ "<em>テスト</em>太郎の冒険\n" ] } } ] } } } } ] } }
nested query + highlight query は駄目?
attached_files.attached_file.content が、ja, en のサブフィールドを持たない通常の string 型だった時は、この方法で出来てたんだけど、今回は上手くいかなかった。
クエリーはこちら。highlight query でも nested query を指定している。
{ "from" : 0, "size" : 10, "query" : { "nested" : { "query" : { "multi_match" : { "query" : "テスト", "fields" : [ "test_attached_files_1.attached_file.content.*" ] } }, "path" : "test_attached_files_1" } }, "highlight" : { "fields" : { "test_attached_files_1.attached_file.content.*" : { "highlight_query" : { "nested" : { "query" : { "multi_match" : { "query" : "テスト", "fields" : [ "test_attached_files_1.attached_file.content.*" ] } }, "path" : "test_attached_files_1" } } } } } }
結果には highlight が含まれない。
{ "took": 7, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 1, "max_score": 0.5, "hits": [ { "_index": "test_attachment", "_type": "test_message", "_id": "AVjKLMBxQVf672jImhPa", "_score": 0.5, "_source": { "test_title": "テストタイトル for testing", "test_attached_files_1": [ { "attached_file": { "_name": "test.txt", "_content": "44OG44K544OI5aSq6YOO44Gu5YaS6Zm6" } } ], "test_attached_files_2": [ { "attached_file": { "_name": "test.txt", "_content": "44OG44K544OI5aSq6YOO44Gu5YaS6Zm6" } } ] } } ] } }
上述の通り、うまくいくパターンが2つ見つかったので、これがなぜ上手くいかないかは詳しくは調べていない。
技術的な説明、補足
nested 型
まずは nested 型が何かというと、詳しくはドキュメントを見て欲しい。
Nested datatype | Elasticsearch Reference [5.0] | Elastic
かなり大雑把に言うと、Elasticsearch で、配列的なデータをもたせる時に使うべきデータ型。これを使わずに、object 型に配列的なデータを格納すると、ちょっと分かりづらい結果になる(詳細はドキュメント参照)。
Elasticsearch の内部的には、nested 型のフィールドのデータは別の独立したドキュメントとして保存される。ただし、その独立して保存されたドキュメントを(親オブジェクト経由でない)通常の方法で検索する事はできない。
内部的には別の場所に保存されているため、nested 型のフィールドに対する検索には、nested query というものが必要となる。
Nested Query | Elasticsearch Reference [5.0] | Elastic
include_in_root
nested 型のフィールドに対するオプションの一つに include_in_root というオプションがある。このオプションを true にすると、nested 型のデータは、独立したドキュメントとして保存されると共に、大元の親ドキュメントにも保存される。
include_in_root を true に設定すると、nested 型の値は親ドキュメントにも保存されるため、nested query を使う必要が無くなる。
ちなみに、前述のドキュメントには、それと似た名前の include_in_all しか記載がないが、以前のドキュメントには記載がある。
Nested Type | Elasticsearch Reference [1.4] | Elastic
この理由としては、include_in_root と include_in_parent は deprecated になるっぽいので、ドキュメントから削除されているのだと思う。
Deprecate `include_in_root` and `include_in_parent`? · Issue #12461 · elastic/elasticsearch
inner hits
inner hits は Elasticsearch 1.5 から追加された機能で、nested 型やドキュメント間の親子関係がある場合に使える機能。ドキュメントはこちら。
Inner hits | Elasticsearch Reference [5.0] | Elastic
以下、今回のパターンである、 「nested 型かつ include_in_root を使っていない場合」に絞って述べる。
nested query を使って検索した場合、nest された子供(今回の例で言えば、添付ファイル)の内容を検索する事が出来るが、検索結果として返されるのは親ドキュメントのみとなる。だが、今回のようなハイライト目的だったり、その他の場合に、どの子供が検索クエリーにヒットしたか知りたい場合がある。そういう時に使える機能。
内部的には、検索結果として返された内容1件1件について、再度クエリーを投げるみたいなイメージ。検索結果が10件とか100件とかであれば問題にならないが、10000件の検索結果に対して inner hits を使用すると、パフォーマンス上問題になると思う。以下の issue で、内部的な話とかも説明されていて参考になった。
Including inner hits drastically slows down query results · Issue #14229 · elastic/elasticsearch
まとめ
Elasticsearch は、結構簡単に使えて便利だけど、内部的な構造が意外に複雑なので(というか、簡単に使えるように内部的に色々頑張っている)、その辺も理解しないと意外なところで詰まるって気がする。
あと、毎回書いてるけど、進化が速すぎて、ついてくのに大変。
お願い
Elasticsearch を使って、以下のようなサービスをちびちび作ってるので、使ってみてフィードバックとかバグ報告とかもらえると嬉しいです。
GitHub も、Slack も、まとめて検索 | Commet
GitHub, Slack, Google Drive 等、色んなツールをまとめて検索します。