Elasticsearch 入門。join フィールドについて

2022.06.13

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

Elasticsearch 初学者の中村です。 今回は Elasticsearch のフィールドタイプ join について書いていこうと思います。

join フィールドとは

join フィールドは、インデックス内のドキュメント間で関連性(relation)を定義するフィールドで、 RDBMS のようにドキュメント間の関連付けが可能です。

先に注意点

join フィールドを利用することで RDBMS のようなドキュメント間の関連性を持たせられますが、 Elasticsearch では join フィールドを利用した検索クエリ時にメモリや計算にオーバーヘッドが生じます。

RDBMS みたく使いたいからと無闇に利用すると検索性能に影響が出ますので、不要であればデータを非正規化して保存することが推奨されています。

マッピング

親子関係は以下のように定義が出来ます。

PUT family
{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword"
      },
      "family_relation": {   # ①
        "type": "join",
        "relations": {
          "parent": "child"  # ②
        }
      }
    }
  }
}
  • ① : join フィールド名を指定します
  • ② : ドキュメントの関連性を定義します。今回は parentchild の親だと定義しています。

複数定義

一つの親に対して、複数の子を配列で定義することも可能です。

PUT family
{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword"
      },
      "family_relation": {  
        "type": "join",
        "relations": {
          "parent": ["child", "grandchild" ]
        }
      }
    }
  }
}

インデキシング

join フィールドを利用してインデキシングするには、そのドキュメントの関連性と親ドキュメント(子の場合) を指定する必要があります。

次の例は Daddy Junior の2人の人物をインデキシングしています。

POST family/_doc/1
{
  "name" : "Daddy",
  "family_relation" : {
    "name" : "parent" # ①
  }
}

POST family/_doc/2?routing=1 # ②
{
  "name" : "Junior",
  "family_relation" : {
    "name" : "child", # ③
    "parent" : 1 # ④
  }
}

インデキシングで関連性を指定する場合には、下記を考慮する必要があります。

  • ① : ドキュメントがどの関連性に属するかを指定
    • 上記例だと Daddy のドキュメントは parent に属しています
  • ② : 関連性を持ったドキュメントは同じシャードでインデックスされる必要があるため、ルーティング で親のIDを指定する必要があります。
  • ③ : Junior は子のドキュメントなので、 child と関連性を指定します
  • ④ : 親ドキュメントの ID を指定します。今回だと JuniorDaddy の子なので、 Daddy の ID の1を指定します。

インデキシングに関しては以上です。次は検索をしてみましょう。

検索

join フィールドを利用した検索クエリには、 has_child, has_parent が用意されています。

それぞれ join フィールドで定義された関連性に対しての検索が可能です。

has_child query

has_child は指定した検索クエリに一致する子ドキュメントを持った、親ドキュメントを取得します。

has_child は結合を行うので他の検索クエリよりも遅いです。ドキュメント数が増えるにつれて検索性能は低下し、検索時間を大幅に増加させる可能性があります。 もし検索クエリ性能を重視するのであれば、十分に注意して利用してください。

下記は Junior という子ドキュメントを持っている、親ドキュメントを探すための検索クエリです。

POST family/_search
{
  "query": {
    "has_child": {
      "type": "child",
      "query": {
        "match": {
          "name": "Junior"
        }
      }
    }
  }
}

レスポンスは下記です。 確かに Junior の親は Daddy なので意図した検索結果となりました。

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "family",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "Daddy",
          "family_relation" : {
            "name" : "parent"
          }
        }
      }
    ]
  }
}

検索クエリでは .query.has_child 配下に下記を指定します。

  • .type
    • join フィールドに定義された子の名前を指定します。今回は child を指定。
  • .query
    • 子のドキュメントに対して実行したい検索クエリ

上記を指定することで、検索した子ドキュメントを持った親ドキュメントを取得できます。

他にも子ドキュメントの最大・最小数を指定可能な .max_children ,.min_children 等があります。 詳細は公式ドキュメント をご確認ください。

has_parent query

has_parent は指定した検索クエリに一致する親ドキュメントを持つ、子ドキュメントを検索します。

has_child と同様に、このクエリはコストがかかります。ドキュメント数が増えるにつれて検索速度が遅くなるため、利用時には注意してください。

下記は Daddy という親に持つ子ドキュメントを検索するためのクエリです。

POST family/_search
{
  "query": {
    "has_parent": {
      "parent_type": "parent",
      "query": {
        "match": {
          "name": "Daddy"
        }
      }
    }
  }
}

レスポンスは下記になります。 Daddy の子ドキュメント Junior を検索できました。

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "family",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "name" : "Junior",
          "family_relation" : {
            "name" : "child",
            "parent" : 1
          }
        }
      }
    ]
  }
}

検索クエリでは .query.has_parent 配下に下記を指定します。

  • .parent_type
    • join フィールドに定義された親の名前を指定
  • .query
    • 子のドキュメントに対して実行したい検索クエリ

他に指定できるパラメーターは公式ドキュメント をご参考ください。

ソート

has_child, has_parent クエリの検索結果では、ソート(sort) が利用出来ません。 検索結果をソートする必要がある場合には、 function_score クエリを利用してソートしてください。

以下は親ドキュメントのフィールドでソートする例です。

POST family/_search
{
  "query": {
    "has_parent": {
      "parent_type": "parent",
      "score": true,
      "query": {
        "function_score": {
          "script_score": {
            "script": "_score * doc['count'].value"
          }
        }
      }
    }
  }
}

制限

Elasticsearch では join フィールドに以下の制限が存在します。

  • 1つのインデックスにつき、1つの join フィールドのみ
  • 親と子のドキュメントは同じシャードでインデックスされなくてはならない
  • 1 つの join フィールドは複数の子をモテるが、親は1つだけ

参考

最後に

nested フィールドに続き join フィールドを勉強してみました。 RDBMS のような関連性を持たせることが可能ですが、利用方法には要注意ということがわかりました。

次回は、nested、 join をどのケースで利用すればいいのか、正規化、非正規化と併せて書いてみたいと思います。