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 等、色んなツールをまとめて検索します。