電子書籍ランキングで収集しているデータの構造をちょっと変更。
いままで複数の著者がいる場合、名前(author_name
)と ID(author_id
)と URL(author_url
)を別々に格納していたんだけど「1著者で1つのオブジェクトにした方が都合がいいのでは?」と思ったので変えてみた。
Before
{"author_id": ["1", "2", "3" ], "author_name": ["一郎", "二郎", "三郎" ], "author_url": ["https://example.com/1", "https://example.com/2", "https://example.com/3" ]}
After
{"authors": [{"author_id": "1", "author_name": "一郎", "author_url": "https://example.com/1" }, {"author_id": "2", "author_name": "二郎", "author_url": "https://example.com/2" }, {"author_id": "3", "author_name": "三郎", "author_url": "https://example.com/3" }]}
Elasticsearch で配列の中にオブジェクトを入れたことはなかったのでどうなるんだろうなと思ってやってみたらそのまま入った。
{"_shards": {"failed": 0, "skipped": 0, "successful": 2, "total": 2}, "hits": {"hits": [{"_id": "YwMcpGgBCkW6nWKC_myU", "_index": "bookwalker-2019.01.31", "_score": 1.6931472, "_source": {"@timestamp": "2019-01-31T22:33:00+09:00", "authors": [{"author_id": "22719", "author_name": "小宮利公", "author_url": "https://bookwalker.jp/author/22719/" }, {"author_id": "19960", "author_name": "笑うヤカン", "author_url": "https://bookwalker.jp/author/19960/" }], "book_id": "de95d1f812-2aff-416a-aceb-e21e1178e5ef", "book_name": "魔王の始め方 THE COMIC5", "book_url": "https://bookwalker.jp/de95d1f812-2aff-416a-aceb-e21e1178e5ef/", "categories": [{"category_id": "2", "category_name": "マンガ", "category_url": "https://bookwalker.jp/category/2/" }], "genres": [{"genre_id": "2", "genre_name": "男性向け", "genre_url": "https://bookwalker.jp/tag/2/" }, {"genre_id": "1491", "genre_name": "青年マンガ", "genre_url": "https://bookwalker.jp/tag/1491/" }, {"genre_id": "556", "genre_name": "限定特典", "genre_url": "https://bookwalker.jp/tag/556/" }], "price_amount": 680, "price_raw": "680円", "rank": 1, "ranking_category_id": null, "ranking_category_name": null, "ranking_genre_id": "tcl22", "ranking_genre_name": "青年マンガ", "store_id": "bookwalker.jp", "store_name": "BOOK☆WALKER" }, "_type": "_doc" }], "max_score": 1.6931472, "total": 20}, "timed_out": false, "took": 15}
一応マッピングでは事前に下記のように設定しているが普通にデータ投げれば多分勝手にやってると思う(多分)。
{"mappings": {"_doc": {"properties": {"authors": {"type": "nested" }, "categories": {"type": "nested" }, "genres": {"type": "nested" }}}}}
実際に投入したデータを Kibana の Discover で見てみると Objects in arrays are not well supported.
となっており「配列の中のオブジェクトはうまくサポートしてないよ」ということらしい。
次に、検索が効くのかどうか試すのにとりあえず Luceneで genres.genre_id:2
なんてやってみるものの結果は返ってこない。これがサポートしてないよってことなんだろう。
Nested Query なんてものがあるからできそうな気がするんだけど今度は Dev Tools から JSONを投げてみる。
Dev Tools の補間で出てくる nested のテンプレートは下記のようになっている。nested には path
が必須だそうだ。
{"query": {"nested": {"path": "path_to_nested_doc", "query": {}}}}
(bool
だと面倒なので)query_string
を使う場合はこんな感じの書式かな。
{"query": {"nested": {"path": "<ネストされているフィールドの名前>", "query": {"query_string": {"query": "<クエリ>" }}}}}
<親>.<子>
(ここでは genres.genre_id
)に対して検索をかけたい場合はフルパス(完全修飾)で記述する。
{"query": {"nested": {"path": "genres", "query": {"query_string": {"query": "genres.genre_id:2" }}}}}
nested じゃないフィールドと組み合わせたい場合(例えば @timestamp
)、nested ではないフィールドの条件は nested
の外に書かなければいけないっぽい。nested
するとその下の階層が対象になるということなのかな。nested した先で sort するならソート条件も nested の中、ということになりそう。
インデックス | +-- @timestamp ❶ | +-- genres | +-- genre_id ❷ | +-- genre_name | +-- genre_url
非 nested、nested、sort を組み合わせるとこんな感じか。
{"query": {"bool": {"must": [{"query_string": {"query": "@timestamp:[now-1h TOnow]" ❶ }}, {"nested": {"path": "genres", "query": {"query_string": {"query": "genres.genre_id:2" ❷ }}}}]}}, "sort": {"rank": {"order": "asc" }}}
うーん、これを Kibana で出来ないのかなぁと Kibana のドキュメント(Nested Objects | Kibana User Guide [6.6] | Elastic)を見てみたらこう書いてあった。
Kibana cannot perform aggregations across fields that contain nested objects. It also cannot search on nested objects when Lucene Query Syntax is used in the query bar. Kibanaは入れ子になったオブジェクトを含むフィールドをまたがって集約を実行することはできません。 クエリバーでLucene Query Syntaxが使用されている場合も、入れ子になったオブジェクトを検索できません。 Using include_in_parent or copy_to as a workaround is not supported and may stop functioning in future releases. 回避策としてinclude_in_parentまたはcopy_toを使用することはサポートされておらず、将来のリリースで機能を停止する可能性があります。
Oh...
まぁ Elasticsearch 自体にクエリは投げられるので問題無いとして、最大の問題は Grafana である。Grafana の Query エリアで genres.genre_id:2
みたいに書いても返ってこない。クエリの結果を見てみると nested せずに query_string
しているので Kibana とやっていることが同じだからか。
{"xhrStatus": "complete", "request": {"method": "POST", "url": "api/datasources/proxy/6/_msearch", "data": "{\"search_type\":\"query_then_fetch\",\"ignore_unavailable\":true,\"index\":\"bookwalker-*\",\"max_concurrent_shard_requests\":256}\n{\"size\":0,\"query\":{\"bool\":{\"filter\":[{\"range\":{\"@timestamp\":{\"gte\":\"1548846787202\",\"lte\":\"1549019587202\",\"format\":\"epoch_millis\"}}},{\"query_string\":{\"analyze_wildcard\":true,\"query\":\"genres.genre_id:2\"}}]}},\"aggs\":{\"3\":{\"terms\":{\"field\":\"book_name.keyword\",\"size\":50,\"order\":{\"1\":\"asc\"},\"min_doc_count\":1},\"aggs\":{\"1\":{\"min\":{\"field\":\"rank\"}},\"2\":{\"date_histogram\":{\"interval\":\"1h\",\"field\":\"@timestamp\",\"min_doc_count\":0,\"extended_bounds\":{\"min\":\"1548846787202\",\"max\":\"1549019587202\"},\"format\":\"epoch_millis\"},\"aggs\":{\"1\":{\"min\":{\"field\":\"rank\",\"script\":{\"inline\":\"_value * -1\"}}}}}}}}}\n" }, "response": {"responses": [{"took": 28, "timed_out": false, "_shards": {"total": 2, "successful": 2, "skipped": 0, "failed": 0}, "hits": {"total": 0, "max_score": 0, "hits": []}, "aggregations": {"3": {"doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": []}}, "status": 200}]}}
うーん、困ったなぁと思ったらちょうど https://github.com/grafana/grafana/pull/7863でやりとりしていて近々マージされそう?な感じ。いまのところ nested のフィールドを使わなくてもいいようにしているので大丈夫なんだけど将来的には対応して欲しいなと思う。
ただ、Kibana もサポートしていないところを見るとそもそも自分のデータの作り方に問題があるようにも思える。こういう場合どういう風に構成するのがいいだろうなぁ。闇雲に フィールド名_{n}
みたいにフィールド増やしても意味は無いし。やはり配列のインデックスで紐付けるのが正しいんだろうか?
まだまだ Elasticsearch についてわからないことが多い。