Engineer's Way

主にソフトウェア関連について色々書くブログです。

ElasticSearchで親-子-孫の3世代ドキュメントを作って、子や孫の内容で検索する

 

f:id:matsnow:20171204030753p:plain

前職の時、AWSのElasticSearchService (Ver5.3) を使っていましたが、日本語どころか英語でもあまり情報がなかったりするので苦労しました。
RDBでよくある、テーブル結合して検索ということがしたかったのですが、手間取ったので方法を残しておきます。

ElasticSearchで3世代ドキュメント構造を持ったindexを作る

たとえば、「商品カテゴリ - 商品 - 商品のレビュアー」みたいに、それぞれのデータに1:nの関係性を持たせたい場合、RDBなら外部キーを使ってJOINすればOKです。 ElasticSearchで、そういった構造を表現したい場合、_typeを使います。

親の方から1,2,3世代とするとき、1世代目、2世代目、3世代目に該当する_typeの名前を決めて、インデックスを設計します。 上の例の場合、名前をcategoryproductreviewerとするなら、以下のようなmappingのJSONをインデックスに適用しておきます。

PUT /market

{
  "mappings": {
    "category": {},
    "product": {
      "_parent": {
        "type": "category" 
      }
    },
    "reviewer": {
      "_parent": {
        "type": "product" 
      }
    }
  }
}

RDBで各テーブルのレコードに外部キーを持たせるのと違う点は、「あくまで1つのindexの中にフラットなデータとして入っている」ということです。 要は_idの重複などを気にしないといけません。

3世代ドキュメント構造を持ったindexにデータを放り込む

  1. 2世代目のドキュメントを放り込む時、_parent: 1世代目の_id(例:_parent: "c1")を追加で渡します。
  2. 3世代目のドキュメントを放り込む時、_parent: 2世代目の_id, _routing: 1世代目の_id(例:_parent: "p1", _routing: "c1")を追加で渡します。

これを_bulkAPIに渡すJSONとして表現するなら

{ "index": { "_index": "market", "_type": "category", "_id": "c1" } }
{"category_name": "clock" }
{ "index": { "_index": "market", "_type": "product", "parent": "c1", "_id": "p1" } }
{"product_name": "clock1" }
{ "index": { "_index": "market", "_type": "reviewer", "parent": "p1", "routing": "c1", "_id": "r1" } }
{"reviewer": "hoge" }

みたいになります。

こうすることで、index内が、図で表すと以下のようなドキュメント構造になります。 f:id:matsnow:20171204030054j:plain

子ドキュメントで検索し、親・孫ドキュメントを含めた一連のドキュメントを取得する

これがあまり情報がなかったのですが、以下のようなクエリを書けば子ドキュメントのデータで検索した上で、3世代分のドキュメントを結合して一気に返してもらうことができます。

{
    "query": {
        "has_child" : {
            "type": "product",
            "query": {
                "bool": {
                    "must": [
                      {
                         "term": {"product_id": "123"}
                      },
                      {
                         "has_child": {
                            "type": "reviewer",
                            "inner_hits": {},
                            "query": {"match_all": {} }
                         }
                      }
                   ]
                }
            },
            "inner_hits": {}
        }
    }
}

boolクエリで、2世代目への検索クエリ(term)と、3世代目の全件取得(has_child.query.match_all)をまとめるのがポイントです。 ※ もちろん、3世代目で更に絞り込みをしたければ、match_allの代わりに別のクエリを使います。

それと、inner_hitsを書くことで、2、3世代目のドキュメント一式を取得できます。 書き忘れると1世代目のドキュメントしか返ってきません。

注意点

公式見解として、今後は1つのindexに対して1つのtypeが推奨されています。
また、親子関係をもたせたいときは5.6から追加されたjoinフィールドを使って欲しいとのこと。
今後のロードマップによると、バージョン8.xあたりでtypeが事実上消滅するようです。(まだまだ先の話でしょうが。。)

www.elastic.co

AWSだと、12/6時点でもまだ5.5までしか選択できないですが、5.6以降がサポートされたら単一type & joinフィールドを使う方向で設計した方が良さそうですね。