elasticsearch6.xの親子関係を整理してみた
この記事はElastic stack Advent Calendar 2017 の17日目の記事となります。
概要
elasticsearchで、親子関係を持ったデータを扱うには、幾つかの方法があります。
例えば
- 親の情報を冗長に保持して、親子を1対1の関係にする
- object配列として子供を保持する
- nested data typeを用いる
- parent-childの関係を用いる
等があると思います。
elasticsearch6.0から、_typeがindexに対して単一になりました。(ver 7では_typeが無くなる予定)
それに伴って、parent-childの使い方も変わったので、改めて整理しておきます。
目次
- サンプルデータの概要
- 親の情報を冗長に保持して、親子を1対1の関係にする
- object data type
- nested data type
- join data type
- まとめ
サンプルデータの概要
ブログサービスのデータを扱うことを想定します。
(ダミーデータはhttp://www.databasetestdata.com/で生成させてもらいました)
親子関係にある2つのindexがあり、片方はユーザーの情報を表すuser index、
もう片方は、ブログの投稿単位でindexしたblog_post indexです。
ブログは一人のユーザーが複数回投稿しうるので、userとblog_postは1対Nの親子関係にあると言えます。
user情報を示すuser table。
fieldとしては、FullName,Id,Country,registered_time,Emailなどがあります。
1ドキュメント取り出してみると下記のような構造になっています。
{ "_index": "user", "_type": "doc", "_id": "QLwKYGABLuVBi7nej178", "_score": 1, "_source": { "Email": "Felicita@eliane.co.uk", "host": "Macintosh.local", "Country": "Gambia", "registered_time": "12/17/1985", "Id": "81", "@version": "1", "Full Name": "Ms. Judd Tromp", "@timestamp": "2017-12-16T15:56:32.767Z" } }
それに対して、blog_postの内容は下記のようになっています。
{ "_index": "blog_post", "_type": "doc", "_id": "jr09YGABLuVBi7neyjL2", "_score": 1, "_source": { "@version": "1", "Summary": """ facere nobis eum minima perspiciatis sit cum debitis commodi est quia distinctio molestiae ut praesentium autem occaecati nemo et asperiores itaque quis optio harum ratione ex vitae dignissimos consequatur vel aliquam accusantium doloribus optio sint provident praesentium sunt laboriosam ullam quo tempore ratione vel dolorum accusantium suscipit blanditiis distinctio unde dolores commodi recusandae numquam perspiciatis ducimus dicta blanditiis sed sint dolore unde eos distinctio facere veritatis vitae veniam qui harum voluptatem dolorem nam quam """, "Id": 3, "@timestamp": "2017-12-16T16:52:30.135Z", "Title": """ eius a quam sequi nulla unde et laboriosam perferendis ut id accusamus corporis ipsam quisquam vero """, "Created At": "2008-09-12T13:14:46.235Z", "host": "Macintosh.local", "Body": """ quia unde quod distinctio odit commodi odio non qui libero perferendis quos expedita voluptatem id odio ut consequatur minus sit odio est aspernatur vel numquam accusamus laborum eligendi dolore corporis voluptate itaque vel aut repudiandae enim saepe fuga voluptatem sed quis quia voluptatem eos in commodi nisi mollitia occaecati qui sint amet unde iusto enim qui voluptate recusandae dolores quia quam eum at ut et cupiditate odit dolores nulla voluptas """ } }
これらの二つのindexはId fieldで紐付いていて、user:blog_postsが1対Nになっています。
親の情報を冗長に保持して、親子を1対1の関係にする
ドキュメントの構造という観点で、この方法は非常にシンプルになります。
ただし、親ドキュメントの情報が冗長になるので、データサイズの増加や、検索時の結果重複などのデメリットが考えられます。
(今回はuserに比べてblog_postが大きかったので、ほとんどデータサイズは増えませんでした)
つまりデータ構造としては、
親ドキュメント
{"Id" : 10, "user" : "A"}
子ドキュメント
{"Id" : 10, "Title" : "aaa"}, {"Id" : 10, "Title" : "bbb"}, {"Id" : 10, "Title" : "ccc"}
という2つのindexをjoinした結果として、
{"user" : "A", "Id" : 10, "Title" : "aaa"}, {"user" : "A", "Id" : 10, "Title" : "bbb"}, {"user" : "A", "Id" : 10, "Title" : "ccc"}
という1つのindexに統合します。
一部のフィールドを省略していますが、userというfieldが冗長であることがわかります。
(user indexのサイズが大きくなるほどデータサイズなどへの影響が顕著です。)
検索時の結果重複という面でのデメリットは、次のような状況が想像できます。
例えば「Edytheという名前のユーザーが何人いるかを検索したい場合」です。
queryはこんな感じでしょうか
GET join/_search { "query": { "match": { "Full Name": "Edythe" } } }
レスポンスはこうです↓
"hits": { "total": 20, "max_score": 5.1217155, "hits":[...] }
これだけ見ると、Edytheさんは20人登録されているように見えますが、実際は1人しか登録されていません。
Edytheさんのブログ投稿が20件あったため、20件のドキュメントが"Full Name":"Edythe"という条件に当てはまりヒットしてしまいます。
object data type
上記を改善するために思いつく方法として、
ブログ投稿のデータであるblog_postを、配列形式で1つのドキュメントに詰め込む方法があります。
つまり下記のようなデータ構造です。
{ "_index": "object", "_type": "doc", "_id": "jL20YGABLuVBi7neE7yx", "_score": 1, "_source": { "Email": "Kendall@reymundo.me", "@timestamp": "2017-12-16T15:56:32.759Z", "registered_time": "6/24/2012", "host": "Macintosh.local", "@version": "1", "Full Name": "Kale Pouros", "Country": "Sudan", "Id": "19", "posts": [ {blog_post 1},{blog_post 2},{blog_post 3}....{blog_post n} ] } }
ある程度見通しが良くなりました。
検索結果が重複する問題も解決します。
しかし、この構造にも欠点があります。
elasticsearchのobject型は、内部的には、「各object要素の配列」が保存されます。
つまり
{ "posts" : [ { "Id" : "1", "Title" : "AAA" }, { "Id" : "2", "Title" : "BBB" } ] }
というドキュメントがあったとしても....
{ "posts.Id" : [ "1", "2" ], "posts.Title" : [ "AAA", "BBB" ] }
というように保持されているのです。
結果として、このドキュメントは
"Title":"AAA" AND "Id":"2"
という条件で検索してもヒットしてしまいます。
nested data type
上で述べた、object datatypeの欠点を解消するためのものがnested datatypeです。
ユーザーから見ると、object data typeを使っている場合とドキュメント構造は全く同じに見えます。
しかし内部的にnested部分は、別のドキュメントとしてindexされます。
使い方としては、事前にデータ型の定義をしておくだけです。
PUT nested { "mappings": { "doc":{ "properties": { "posts":{ "type": "nested" } } } } }
こうすることで、nestした個々のオブジェクトに対して検索をかけることができます。
見かけ上は100ドキュメントでも、cat APIで確認すると、10100件保持していることがわかります↓
green open object aFeBCUA0SI6xAQAgwFspdQ 5 0 100 0 11.7mb 11.7mb green open nested KMCi8GTQS6eLmofmlRgBcQ 5 0 10100 0 12.4mb 12.4mb
強いてデメリットを挙げるとすれば、nested data typeを使っている場合は、
query, aggregation, sort, highlightにおいて、nested専用のものを使う必要があるというところでしょうか。
(慣れていないと書き方が少し難しい&面倒)
join data type
ここまで挙げてきた方法はいずれも、スキーマが異なる情報を、1つのドキュメントとして統合するものでした。
(nested data typeは内部的には別ドキュメントですが)
それに対して、join data typeは同一index内の異なるドキュメント同士の親子関係を定義するためのものです。
以前は複数の_typeを用いて実現していたparent-childの関係が、
バージョン6以降ではjoin data typeのフィールドを用いて実現するように変更されました。
使い方としては、
事前に親子関係の識別子となるフィールドを定義しておきます。
PUT parent-child { "mappings": { "doc": { "properties": { "user_or_post": { "type": "join", "relations": { "user": "post" } } } } } }
あとはdocument登録の際に、親には "user_or_post":"user"というフィールドを追加し....
{ "_index": "parent-child", "_type": "doc", "_id": "-b3jYGABLuVBi7neE9XR", "_score": 1, "_source": { "@version": "1", "user_or_post": "user", #<= ここ "@timestamp": "2017-12-16T19:53:02.295Z", "host": "Macintosh.local", "Full Name": "Charity Pollich V", "registered_time": "11/22/2011", "Id": "2", "Country": "Svalbard and Jan Mayen", "Email": "Demetrius@estel.com" } }
子供側には
"user_or_post": {"name": "post", "parent": "親のドキュメントID"}
というフィールドを追加します。
{ "user_or_post": { "name": "post", #<= ここ "parent": "98" #<= ここ }, "Body": "xxxxx", "Summary": "xxxxx", "@version": "1", "Title": "xxxxxx", "Id": 98, "@timestamp": "2017-12-16T20:12:12.361Z", "host": "Macintosh.local", "Created At": "1987-12-11T14:58:59.947Z" }
これで親子関係を持つことができます。
検索時には、has_child queryやhas_parent queryを使います。
例えば「1996-08-12T15:28:25.327Z」にブログを投稿したユーザーを調べたければ下記のように検索します。
(要するにchildの条件で絞り込んで、parentのdocumentを返したい)
GET parent-child/_search { "query": { "has_child": { "type": "post", "query": { "term": { "Created At": { "value": "1996-08-12T15:28:25.327Z" } } } } } }
"hits": { "total": 1, "max_score": 1, "hits": [ { "_index": "parent-child", "_type": "doc", "_id": "93", "_score": 1, "_source": { "host": "Macintosh.local", "@timestamp": "2017-12-16T19:57:38.872Z", "registered_time": "5/1/1998", "user_or_post": "user", "Country": "Indonesia", "Email": "Kylie_Kiehn@ernestina.com", "@version": "1", "Full Name": "Dannie Schiller", "Id": "93" } } ] }
無事検索できました。
まとめ
ここまで、親子関係を持つデータの構造について何パターンか整理しました。
各方法とも一長一短あると思うので、状況によって使い分けたいものです。
以上です。読んでいただきありがとうございました。
その他
- 何かツッコミがあればコメントお願いします
- 検索パフォーマンスとかをしっかり検証していないので、データ増やしたり階層増やすとどうなるか、そのうち試してみたい(やってみた人が既にいたら是非教えてください)