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

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です