Elasticsearch 6 へのアップグレードに際して Parent-Child を join データタイプへ移行する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Elasticsearch 6 からインデックス当たり作成できるタイプが一つのみとなりました。それにより何ができなくなり、現在複数タイプを使っている場合どうすればいいのか調査したのでブログにまとめます。

概要

現在弊社のプロダクトで利用している Elasticsearch をバージョン 6系へアップグレードするため、調査、検証しています。Elasticsearch 6.0 の Breaking Changes を確認していたところ、中でも影響が大きいのはインデックス当たりのタイプ数が一つのみになることでした。

Elasticsearch でインデックス当たりに複数タイプを作成する代表的なケースは Parent-Child の利用です。今回アップグレードするプロダクトでも複数種類のインデックスにて Parent-Child を利用しています。Parent-Child を使っているシステムはどのように移行すれば良いのでしょうか?

Parent-Child とは?

Elasticsearch は RDB のようにテーブル間で外部キーを貼って、レコードを結合することはできません。ただ似た機能として、タイプ間のドキュメントを結合する Parent-Child リレーションシップを提供していました。Parent-Child は外部キーのような参照整合性を得ることはできませんが、ドキュメントの結合は達成することができます。

イメージとしてはこんな感じ。

その Parent-Child は必要か?

本題とは関係ありませんが、、、
Parent-Child は RDB 利用者からすると非常に便利です。RDB のテーブル設計と同じようにデータを扱うことができます。ただ、Parent-Child はパフォーマンスを犠牲とするケースが多々あります。

RDB で複数テーブルで管理しているデータを Elasticsearch にインデキシングして検索するには大きく 3つの方法があります。

  1. 非正規化してアプリケーション側で制御する
    RDB からテーブル間で結合したデータを取り出し、一つのドキュメント(JSON)にして Elasticsearch に登録します。最も検索パフォーマンスが高いです。ただし、子ドキュメントで複雑な検索や集計ができなくなり、アプリケーションで実装する必要があるケースがあります。

  2. Nested タイプを利用する
    上のやり方と似ていますが、一:多の結合したフィールドを nested データタイプとして登録します。検索パフォーマンスがそこそこ高いです。Nested タイプを利用することで一:多のドキュメントを柔軟に検索や集計することができます。ただ子ドキュメントの量が膨大だったり、更新量が過多の場合に Elasticsearch の負荷が大きくなります。

  3. Parent-Child を利用する
    結合せずに RDB のレコード単位でそれぞれのタイプにドキュメントを登録します。一:多のドキュメントを柔軟に検索、集計、管理することができます。ただし、親子のドキュメントが同じシャードに所属するだけで完全に切り離されているためパフォーマンスが出ないケースがあります。

Parent-Child の移行

閑話休題。
それでは今まで Parent-Child を利用してきたシステムはどのようにすれば、Elasticsearch 6系にアップグレードできるのでしょうか。ほとんどのケースでは Elasticsearch 5.6 からリリースされた join データタイプに移行することになるかと思います。

Parent-Child がタイプ間でドキュメントを結合していたのに対し、join データタイプは同タイプ内のフィールドのデータでドキュメントを結合するイメージです。

データ登録

実際にデータ登録までの流れを見れば分かりやすいかと思います。

検索結果で結合したデータを得るにはインデックス定義、親ドキュメント登録、子ドキュメント登録の三つのステップでデータを登録する必要があります。

Elasticsearch バージョン

Parent-Child : 5.6.7
join データタイプ : 6.2.2

Parent-Child

まずは従来の Parent-Child で親子ドキュメントを登録してみましょう。

### インデックス定義
PUT index
{
  "mappings": {
    "parents": {},
    "children": {
      "_parent": {
        "type": "parents"
      }
    }
  }
}
### 親ドキュメント登録
PUT index/parents/1
{
  "text": "parent data"
}
### 子ドキュメント登録
PUT index/children/1?parent=1
{
  "text": "child data"
}
join データタイプ

次に join データタイプで親子ドキュメントを登録してみましょう。

### インデックス定義
PUT index
{
  "mappings": {
    "type": {
      "properties": {
        "join": {
          "type": "join",
          "relations": {
            "parents": "children"
          }
        }
      }
    }
  }
}
### 親ドキュメント登録
PUT index/type/1
{
  "text": "parent data",
  "join": "parents"
}
### 子ドキュメント登録
PUT index/type/2?routing=1
{
  "text": "child data",
  "join": {
    "name": "children",
    "parent": "1"
  }
}

変わったところを簡単に説明します。

インデックス定義

Parent-Child はタイプを複数作成します。join データタイプではタイプは一つしか作成していません。何度も言うようですが、Elasticsearch 6 からは複数タイプを作成するインデックス作成はできません。複数のタイプを持つインデックス作成リクエストはエラーとなり、作成に失敗します。また Parent-Child では子タイプの中で親タイプを関連付けていたところ、join データタイプではリレーションシップ張るための join タイプのフィールドを作成し、親となる名前と子となる名前を KEY:VALUE で定義します。

親ドキュメント登録

Parent-Child は親タイプにドキュメントを登録します。join データタイプでは定義した join タイプのフィールドに relations で指定した KEY の値で登録します。

子ドキュメント登録

Parent-Child は子タイプにドキュメントを登録します。join データタイプでは定義した join タイプのフィールドに relations で指定した VALUE の値で登録します。またクエリストリングの routing 指定(親と同じシャードに所属される必要がある)も必須となりました(勝手にやってほしい

検索

検索は同じクエリで Parent-Child も join データタイプも検索して、子ドキュメントを一緒に取得することができます。

POST index/_search
{
  "query": {
    "has_child": {
      "type": "children",
      "query": {
        "match_all": {}
      },
      "inner_hits": {}
    }
  }
}
Parent-Child のレスポンス
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "index",
        "_type": "parents",
        "_id": "1",
        "_score": 1,
        "_source": {
          "text": "parent data"
        },
        "inner_hits": {
          "children": {
            "hits": {
              "total": 1,
              "max_score": 1,
              "hits": [
                {
                  "_type": "children",
                  "_id": "1",
                  "_score": 1,
                  "_routing": "1",
                  "_parent": "1",
                  "_source": {
                    "text": "child data"
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}
join データタイプのレスポンス
{
  "took": 147,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "index",
        "_type": "type",
        "_id": "1",
        "_score": 1,
        "_source": {
          "text": "parent data",
          "join": "parents"
        },
        "inner_hits": {
          "children": {
            "hits": {
              "total": 1,
              "max_score": 1,
              "hits": [
                {
                  "_index": "index",
                  "_type": "type",
                  "_id": "2",
                  "_score": 1,
                  "_routing": "1",
                  "_source": {
                    "text": "child data",
                    "join": {
                      "name": "children",
                      "parent": "1"
                    }
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}

タイプが違っていたり、join データタイプのデータがあったりぐらいで根本的な違いはありません。

join データタイプのアレコレ

join データタイプのドキュメントページにも記載がありますが押さえておいた方がいいことがいくつかあります。

親に対して複数種類の子の join

配列で指定可能です。

PUT index
{
  "mappings": {
    "type": {
      "properties": {
        "join": {
          "type": "join",
          "relations": {
            "parent": [
              "child1",
              "child2"
            ]
          }
        }
      }
    }
  }
}
親子孫の join

relations に複数定義できます。

PUT index
{
  "mappings": {
    "type": {
      "properties": {
        "join": {
          "type": "join",
          "relations": {
            "parent": "child",
            "child": "grandchild"
          }
        }
      }
    }
  }
}

Parent-Child -> join の注意点

  • タイプが一つのみとなるため、ID の重複が NG
    Parent-Child はタイプが異なっていたため、Parent タイプと Child タイプで同一 ID を割り当てていても別のドキュメントとして扱われていました。join データタイプは同一タイプにドキュメントを配置するため同一 ID を割り当てると上書きになってしまいます。重複しないように ID を設計してください。特に RDB のシーケンスで割り当てたカラムのデータを ID に利用していると注意が必要です。

タイプに関するロードマップ

5.6.0
  • タイプを一つしか利用できなくすることが可能(インデックスオプション指定)
  • join データタイプが追加
6.0.0
  • インデックス当たりタイプは一つのみ
  • _default_ マッピングが非推奨
7.0.0
  • タイプがなくなる。タイプに置き換わり _doc を指定
  • _default_ マッピングは利用不可

まとめ

いかがでしたでしょうか? Elasticsearch 6.0 からインデックス当たりのタイプが一つになることの対応方法を書きました。私は思ったよりも大変ではない印象でした。簡単にまとめです。

  • Parent-Child が本当に必要なのか
  • Parent-Child を使っている場合は、join データタイプで同じようなことができる
  • タイプの今後のロードマップを把握しておきたい

参考URL