こんにちは、オウチーノの山本です。この記事はくふうカンパニー Advent Calendar 2018の17日目として書いています。
はじめに
以前の記事 ( 漸進的なシステムリプレイス(STTMeetup システムリプレイスNight))内の資料でも触れているのですが、オウチーノでは物件の検索にElasticsearch
を、Elasticsearch
への登録には、Embulk
を使用しています。
このElasticsearch
に登録している物件の情報ですが、階層構造を持つ必要があるので、現在は Nested datatype を使用しています。
# Embulkのconfigから一部抜粋 estate: _all: enabled: false dynamic: false properties: estate_id: type: keyword traffics: type: nested properties: traffic_id: type: keyword
nested datatype
を使う場合、estate
(物件の基本情報)とtraffics
(最寄駅などの交通機関の情報)は、1つのドキュメント上に存在する必要があるのですが、RDBのテーブル上は別々ですので、
- RDBの物件テーブルから
estate
を登録 - 交通情報テーブルを元に
traffics
を追記
のように、2回Embulk
を実行して作成しています。
しかも追加+更新なので、並列で実行する事ができません。
いやいや、それは普通だろう、と思われる方もおられるかも知れませんが、Elasticsearch
には join datatypeというものが存在するので、別々に登録したドキュメント間で階層構造を作ることが出来ます。
ただ、公式ドキュメントに以下のような記述があります。
The join field shouldn’t be used like joins in a relation database. In Elasticsearch the key to good performance is to de-normalize your data into documents. Each join field, has_child or has_parent query adds a significant tax to your query performance.
このように釘を刺されていたので、今まで使っていませんでした。
それでも、最近、並列で実行したいという思いが高まってきたので、試しにやってみる事にしました。
前提
今回、検証を行なった環境は以下の通りです。
- Embulk
- v0.9.6
- 実行はローカル(MacBook Pro 13-inch 2016 Core i7 3.3GHz 16GB High Sierra)
- input-plugin
- embulk-input-mysql
- output-plugin
- embulk-output-elasticsearch_using_url
- Elasticsearch
- AWS Elasticsearch service 6.3
Embulkでjoin datatypeを登録
まず最初に、少なくとも以下の条件をクリアするプラグインが必要だったのですが、見つかりませんでした。
- 項目の値をJsonでパースする必要がある
{"traffics": "{\"name\": \"traffic\", \"parent\": 1}"}
こうではなく{"traffics": {"name": "traffic", "parent": 1}}
こうしたい
- メタ項目である
_routing
を指定する必要がある
1.
は embulk-output-elasticsearch_using_urlで解決できるのですが、2.
がどうしても解決できなかったので、とりあえず修正しました。
後は、階層構造で記述していた定義を平坦化して、join datatype
の項目を追加すれば、登録できるようになります。
- 平坦化して
join datatype
を追加
# Embulkのconfigから一部抜粋 estate: _all: enabled: false dynamic: false properties: estate_id: type: keyword traffic_id: type: keyword traffics: type: join relations: estate: traffic
- 親ドキュメントは
traffics
にestate
を指定
# estate のconfigから一部抜粋 filters: - type: column add_columns: - {name: traffics, type: string, default: "estate"} out: type: elasticsearch_using_url bulk_actions: 2000 request_timeout: 300 id_format: "%d" id_keys: - "estate_id" routing_format: "%d" routing_keys: - "estate_id"
- 子ドキュメントは
traffics
にtraffic
と、親ドキュメントのIDを設定。
# traffic のconfigから一部抜粋 filters: - type: column add_columns: - {name: traffics, type: string, default: ""} - type: ruby_proc requires: - json columns: - name: traffics proc: | ->(col, record) do { name: "traffic", parent: "#{record['estate_id']}" }.to_json end type: string out: type: elasticsearch_using_url bulk_actions: 2000 request_timeout: 300 id_format: "%d-%d" id_keys: - "estate_id" - "traffic_id" routing_format: "%d" routing_keys: - "estate_id" array_columns: - {name: traffics, parse_json: true, error_skip: true}
実際に、物件を12万件ほど登録してみたところ、結果は以下の通りでした。
nested | join | |
---|---|---|
実行時間(estate + traffic) | 2:05 + 1:52 | 2:02 + 2:24 |
データサイズ | 102.2MB | 85.6MB |
join datatype
の場合、nested datatype
に比べて、交通情報の登録に時間がかかっていますが、並列で動かせるので問題はなさそうです。
データサイズが小さくなるのは意外でした。
検索速度の検証
登録はできたので次は検索を実行します。
まずはnested datatype
のインデックスに対して、駅徒歩10分の物件を検索してみます。
GET nested_estate/_search { "query": { "nested": { "path": "traffics", "query": { "range": { "traffics.walk_time_station": { "lte": 10 } } } } } }
平均6ms程で結果が返ってきます。
{ "took": 6, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 30616, "max_score": 1, "hits": [ {
次にjoin datatype
のインデックスに対して、同じ検索をしてみます。
GET join_estate/_search { "query": { "has_child": { "type": "traffic", "query": { "range": { "walk_time_station": { "lte": 10 } } } } } }
平均13ms程。確かにnested datatype
に比べたら遅いです。倍くらいの時間がかかっています。
{ "took": 13, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 30616,
色々検索条件を変更してみましたが、nested datatype
は1桁msで返ってくるのですが、join datatype
はひどい時だと50msほどかかったりしました。
しかし、Embulk
が並列実行できるのは結構なメリットなので、これくらい目を瞑るかと思っていた所、ある問題に気づきました。
返却値の違い
nested datatype
は1つのドキュメントに全てのデータが入っているので意識していなかったのですが、join datatype
はestate
とtraffic
が別のドキュメントなので、返ってくる値にtraffic
が入っていない事に気づきました。
"hits": [ { "_index": "nested_estate", "_type": "estate", "_id": "1", "_score": 1, "_source": { "estate_id": 1, "traffics": [ <---- この部分が返ってこない { "walk_time_station": 16, "traffic_id": 1 }, { "walk_time_station": 5, "traffic_id": 2 } ]
これはいけない、という事でtraffic
の値も返ってくるようにオプションをつけたのですが、
GET join_estate/_search { "query": { "has_child": { "type": "traffic", "query": { "range": { "walk_time_station": { "lte": 10 } } }, "inner_hits": {} <-- inner_hits option } } }
検索で対象となったものしか返ってきません。
"hits": [ { "_index": "join_estate", "_type": "estate", "_id": "1", "_score": 1, "_source": { "estate_id": 1, "inner_hits": { "traffic": { "hits": { "total": 1, <-- 1件しか返ってこない "max_score": 1, "hits": [ { "_source": { "estate_id": 1, "walk_time_station": 5, "traffic_id": 2, } ]
inner_hits オプションを別の所につけたらいいような気がして、試しにmatch_all
を追加してみた所、
GET join_estate/_search { "query": { "bool": { "must": [ { "has_child": { "type": "traffic", "query": { "range": { "walk_time_station": { "lte": 10 } } } } }, { "has_child": { "type": "traffic", "query": { "match_all": {} <-- match_all を追加 }, "inner_hits": {} } } ] } } }
返ってくるようにはなったのですが、平均で20msほどかかるようになってしまいました。
{ "took": 20, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 30616, "max_score": 2,
まとめ
nested datatype
に比べ、最終的に3倍以上の時間がかかるようになってしまったので、確かにjoin datatype
は遅いです。
並列で実行したいと思っていましたが、今回はちょっと見送る事にしました。
ただ、検索速度の遅延が問題にならず、データ投入の時間を短縮したい場合には非常に有効な手段だと思いますので、今後そういう機会があれば、再チャレンジしたいと思います。