こんにちは。WEBサービス開発グループの中野です。
Elasticsearchでは、今後のバージョンアップで typeの段階的な廃止 が予定されていますが、従来 joinクエリ(has_child や has_parent)は複数のtypeを利用する前提の機能となっていました。
今回は、このtypeの仕様変更後にjoinクエリを利用する方法を確認したいと思います。
typeの段階的な廃止
ドキュメントでアナウンスされている内容を確認すると、今後の変更は次のような流れで進められていくようです。
5.6
- index.mapping.single_type パラメータに true を設定することで、一つのindexに一つのtypeのみの制限をかけることができる
- join field が利用可能になる
6.x
- 5.xで作成したindexでは引き続き複数のtypeが利用可能
- 6.xで作成するindexは単一のtypeのみ
7.x
- クエリ時のtypeパラメータが必須ではなくなる
以下略
Elasticsearch5.xでのjoin利用方法
Elasticsearch5.x までは、親子関係にあるデータをそれぞれ別のtypeに定義し、_parent field というメタフィールドで結びつけることでjoinが実現されていました。
青空文庫さんの作品リストcsv を使用させて頂き、著者データと作品データをjoinする例をみてみましょう。
データ形式
著者を親として authorタイプ、作品を子として bookタイプを定義し、サンプルデータを投入します。
“_parent”: { “type”: “author” } のように子のマッピング定義に親のtypeを指定し、子のデータをインデクシングする際に紐付ける親の _id を指定する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
PUT join_old { "mappings": { "author": { "properties": { "name": { "type": "text" } } }, "book": { "_parent": { "type": "author" }, "properties": { "title": { "type": "text" } } } } } PUT join_old/author/1 { "name": "太宰 治" } PUT join_old/author/2 { "name": "宮沢 賢治" } PUT join_old/book/1?parent=1 { "title": "人間失格" } PUT join_old/book/2?parent=1 { "title": "走れメロス" } PUT join_old/book/3?parent=2 { "title": "銀河鉄道の夜" } |
クエリ
検索条件にマッチする子に紐付く親を取得するクエリとして Has Child Query、逆に検索条件にマッチする親に紐付く子を取得するクエリとして Has Parent Query が用意されています。
Has Child Query:
titleに「夜」が含まれる作品の著者を検索しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
GET join_old/author/_search { "query": { "has_child": { "type": "book", "query": { "match": { "title": "夜" } }, "inner_hits": {} } } } |
期待通りに「宮沢 賢治」がヒットしていることがわかります。
また、クエリに inner_hits を指定したことで、子の情報もjoinされた状態で検索結果が返却されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
{ "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 1, "hits": [ { "_index": "join_old", "_type": "author", "_id": "2", "_score": 1, "_source": { "name": "宮沢 賢治" }, "inner_hits": { "book": { "hits": { "total": 1, "max_score": 0.26742277, "hits": [ { "_type": "book", "_id": "3", "_score": 0.26742277, "_routing": "2", "_parent": "2", "_source": { "title": "銀河鉄道の夜" } } ] } } } } ] } } |
Has Parent Query:
nameに「太宰」が含まれる著者の作品を検索しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
GET join_old/book/_search { "query": { "has_parent": { "type": "author", "query": { "match": { "name": "太宰" } }, "inner_hits": {} } } } |
期待通りに「人間失格」と「走れメロス」がヒットしていることがわかります。
また、クエリに inner_hits を指定したことで、親の情報もjoinされた状態で検索結果が返却されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
{ "took": 3, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 2, "max_score": 1, "hits": [ { "_index": "join_old", "_type": "book", "_id": "1", "_score": 1, "_routing": "1", "_parent": "1", "_source": { "title": "人間失格" }, "inner_hits": { "author": { "hits": { "total": 1, "max_score": 0.5063205, "hits": [ { "_type": "author", "_id": "1", "_score": 0.5063205, "_source": { "name": "太宰 治" } } ] } } } }, { "_index": "join_old", "_type": "book", "_id": "2", "_score": 1, "_routing": "1", "_parent": "1", "_source": { "title": "走れメロス" }, "inner_hits": { "author": { "hits": { "total": 1, "max_score": 0.5063205, "hits": [ { "_type": "author", "_id": "1", "_score": 0.5063205, "_source": { "name": "太宰 治" } } ] } } } } ] } } |
Elasticsearch6.xでのjoin利用方法
Elasticsearch6.x 以降では前述の通りtypeの廃止が予定されているため、従来の形式では Has Child や Has Parent で join を利用することができなくなります。
その代替として新たに join datatype が用意されましたので、使い方を確認してみましょう。
2017年9月末現在、6.xは rc版がリリース されており本リリースが間近の模様ですが、5.6でもバックポートとして join datatype が利用可能になっていますので、今回は5.6で動作確認してみたいと思います。
データ形式
6.xからは一つのindexに複数のtypeを指定することはできなくなりますので、docという名前で単一のtypeを定義し、この単一のtypeに対して、親の著者情報と子の作品情報を格納するフィールドを定義してデータを投入していきます。(5.6ではもともと複数typeが指定可能なため、join datatypeを利用するために “mapping.single_type”: true を設定する必要があります。)
あわせて、ドキュメントにあるように、任意の名前で join datatype のフィールドを定義し、”relations”: { “author”: “book” } のように親子関係を指定しましょう。
インデクシングの際にはこの join datatype のフィールドに対して親と子を区別する relations の値を指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
PUT join_new { "settings": { "mapping.single_type": true }, "mappings": { "doc": { "properties": { "author_name": { "type": "text" }, "book_title": { "type": "text" }, "join_key": { "type": "join", "relations": { "author": "book" } } } } } } PUT join_new/doc/1 { "author_name": "太宰 治", "join_key": { "name": "author" } } PUT join_new/doc/2 { "author_name": "宮沢 賢治", "join_key": { "name": "author" } } PUT join_new/doc/3?routing=1 { "book_title": "人間失格", "join_key": { "name": "book", "parent": 1 } } PUT join_new/doc/4?routing=1 { "book_title": "走れメロス", "join_key": { "name": "book", "parent": 1 } } PUT join_new/doc/5?routing=2 { "book_title": "銀河鉄道の夜", "join_key": { "name": "book", "parent": 2 } } |
クエリ
クエリについては従来の Has Child Query と Has Parent Query がそのまま利用可能です。
Has Child Query:
titleに「夜」が含まれる作品の著者を検索しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
GET join_new/doc/_search { "query": { "has_child": { "type": "book", "query": { "match": { "book_title": "夜" } }, "inner_hits": {} } } } |
期待通りに「宮沢 賢治」がヒットしていることがわかります。
また、クエリに inner_hits を指定したことで、子の情報もjoinされた状態で検索結果が返却されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
{ "took": 5, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 1, "hits": [ { "_index": "join_new", "_type": "doc", "_id": "2", "_score": 1, "_source": { "author_name": "宮沢 賢治", "join_key": { "name": "author" } }, "inner_hits": { "book": { "hits": { "total": 1, "max_score": 0.26742277, "hits": [ { "_type": "doc", "_id": "5", "_score": 0.26742277, "_routing": "2", "_source": { "book_title": "銀河鉄道の夜", "join_key": { "name": "book", "parent": 2 } } } ] } } } } ] } } |
Has Parent Query:
nameに「太宰」が含まれる著者の作品を検索しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
GET join_new/doc/_search { "query": { "has_parent": { "type": "author", "query": { "match": { "author_name": "太宰" } }, "inner_hits": {} } } } |
期待通りに「人間失格」と「走れメロス」がヒットしていることがわかります。
また、クエリに inner_hits を指定したことで、親の情報もjoinされた状態で検索結果が返却されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
{ "took": 3, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 2, "max_score": 1, "hits": [ { "_index": "join_new", "_type": "doc", "_id": "3", "_score": 1, "_routing": "1", "_source": { "book_title": "人間失格", "join_key": { "name": "book", "parent": 1 } }, "inner_hits": { "author": { "hits": { "total": 1, "max_score": 0.5063205, "hits": [ { "_type": "doc", "_id": "1", "_score": 0.5063205, "_source": { "author_name": "太宰 治", "join_key": { "name": "author" } } } ] } } } }, { "_index": "join_new", "_type": "doc", "_id": "4", "_score": 1, "_routing": "1", "_source": { "book_title": "走れメロス", "join_key": { "name": "book", "parent": 1 } }, "inner_hits": { "author": { "hits": { "total": 1, "max_score": 0.5063205, "hits": [ { "_type": "doc", "_id": "1", "_score": 0.5063205, "_source": { "author_name": "太宰 治", "join_key": { "name": "author" } } } ] } } } } ] } } |
Elasticsearch5.6や6.xでjoinを利用する方法ついては、以上となります。
まとめ
typeが単一になったり廃止されることで今回のように利用方法が変わる機能がいくつかありますが、一つのindexに一つのデータ形式というシンプルな形になることによる今後のさらなる機能・性能拡張に期待したいと思います。
余談となりますが、Has Child や Has Parent などのjoin機能は便利な一方、データや状況によっては非常に重いクエリとなる場合がありますので注意して利用しましょう。