Elasticsearch > 親子関係データを格納 > Join field type

10 min read読了の目安(約9400字

Elasticsearch > 親子関係データを格納 > Join field type

環境:Elasticsearch 7.9
Elasticsearchがローカルで作動済みである前提です。
Elasticsearchの環境がない場合にはDockerでローカルにて作動させるか、ElasticCloudを使用します。

はじめに

Elasticsearchにて親子関係データを格納する方法であるJoin field typeを設定するチュートリアルをやってみたメモです

以下のような階層データをElasticsearch内に保持できるようになります。

   question
    /    \
   /      \
comment  answer
           |
           |
          vote

以下に沿って進みます。

Join field type | Elasticsearch Reference [7.9] | Elastic

親子関係を持つJoin field type

join data typeは、同じインデックスのドキュメント内に親子関係を持つデータ構造を作成する特別なフィールドです。

インデックスの作成

relationsセクションでは、ドキュメント内で可能なrelationのセットを定義し、それぞれの関係は親名と子名となります。

以下の例では、"question": "answer"とする事で、questionが親、answerが子として定義しています。

以下をKibanaの開発者ツールに打ち込みます。
(もしくはPOSTMAN等でPUTします。)

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_id": {
        "type": "keyword"
      },
      "my_join_field": { 
        "type": "join",
        "relations": {
          "question": "answer" 
        }
      }
    }
  }
}

relationは以下のように複数定義することもできます。

"relations": {
  "question": ["answer", "comment"]}
}

親ドキュメントの作成

以下では、質問コンテキストで2つの親ドキュメントを作成しています。

PUT my-index-000001/_doc/1?refresh
{
  "my_id": "1",
  "text": "This is a question",
  "my_join_field": {
    "name": "question" 
  }
}

PUT my-index-000001/_doc/2?refresh
{
  "my_id": "2",
  "text": "This is another question",
  "my_join_field": {
    "name": "question"
  }
}

親ドキュメントを作成する際に、通常のオブジェクト記法でカプセル化するのではなく、ショートカットとしてリレーションの名前だけを指定することができます。


"my_join_field": {
	"name": "question"
}

  or 
  
"my_join_field": "question" 

子ドキュメントの作成

以下は2つの子ドキュメントを作成しています。

PUT my-index-000001/_doc/3?routing=1&refresh
{
  "my_id": "3",
  "text": "This is an answer",
  "my_join_field": {
    "name": "answer",
    "parent": "1"
  }
}

PUT my-index-000001/_doc/4?routing=1&refresh
{
  "my_id": "4",
  "text": "This is another answer",
  "my_join_field": {
    "name": "answer",
    "parent": "1"
  }
}

子をインデックス化する際には、_sourceにドキュメントの親IDだけでなく、リレーションの名前も追加しなければなりません。

  • ①: 親ドキュメントと子ドキュメントは同じシャードでインデックスを作成しなければならないので、ルーティング値は必須です。

  • ②: "answer"は、このドキュメントの結合名です。

  • ③: ドキュメントの親 ID

検索

以下のクエリで全件データが取得できます。

GET my-index-000001/_search
{
  "query": {
    "match_all": {}
  },
  "sort": ["my_id"]
}

親結合では、ドキュメント内にリレーションの名前をインデックス化するためのフィールドを1つ作成します。(my_parent, my_child, ....)

また、親子関係ごとに1つのフィールドを作成します。
このフィールドの名前は、結合フィールドの名前の後に#と親の名前が続きます。例えば、my_parent → [my_child, another_child]リレーションの場合、joinフィールドはmy_join_field#my_parentという名前の追加フィールドを作成します。

このフィールドには、ドキュメントが子(my_child または another_child)の場合はドキュメントがリンクしている親の_id、親(my_parent)の場合はドキュメントの_idが含まれています。

結合フィールドを含むインデックスを検索する場合、これら2つのフィールドは常に検索レスポンスで返されます。

親結合クエリとアグリゲーション編集

詳細は has_child および has_parent クエリ、children aggregationinner hitsを参照してください。

結合フィールドの値は、アグリゲーションやスクリプトでアクセス可能で、parent_idクエリで問い合わせることができます。

GET my-index-000001/_search
{
  "query": {
    "parent_id": { 
      "type": "answer",
      "id": "1"
    }
  },
  "aggs": {
    "parents": {
      "terms": {
        "field": "my_join_field#question", 
        "size": 10
      }
    }
  },
  "script_fields": {
    "parent": {
      "script": {
         "source": "doc['my_join_field#question']" 
      }
    }
  }
}

複数の子

複数の子を定義することもできます。

Indexを消して再作成します。

PUT my-index-000002
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
          "question": ["answer", "comment"]  
        }
      }
    }
  }
}

以下のような親子構造を作成できます。

   question
    /    \
   /      \
comment  answer

データを格納します。

PUT my-index-000002/_doc/1?refresh
{
  "my_id": "1",
  "text": "This is a question",
  "my_join_field": "question" 
}

PUT my-index-000002/_doc/2?refresh
{
  "my_id": "2",
  "text": "This is another question",
  "my_join_field": "question"
}

PUT my-index-000002/_doc/3?routing=1&refresh
{
  "my_id": "3",
  "text": "This is an answer",
  "my_join_field": {
    "name": "answer",
    "parent": "1"
  }
}

PUT my-index-000002/_doc/4?routing=1&refresh
{
  "my_id": "4",
  "text": "This is another answer",
  "my_join_field": {
    "name": "answer",
    "parent": "1"
  }
}

PUT my-index-000002/_doc/5?routing=1&refresh
{
  "my_id": "5",
  "text": "This is comment",
  "my_join_field": {
    "name": "comment",
    "parent": "1"
  }
}

PUT my-index-000002/_doc/6?routing=1&refresh
{
  "my_id": "6",
  "text": "This is another comment",
  "my_join_field": {
    "name": "comment",
    "parent": "1"
  }
}

検索

親要素に"question"を持つ子要素を検索

GET my-index-000002/_search
{
  "query": {
    "has_parent": {
      "parent_type": "question",
      "query": {
        "match_all": {}
      },
      "inner_hits": {}
    }
  }
}

子要素"answer"を持つ親を検索

GET my-index-000002/_search
{
  "query": {
    "has_child": {
      "type": "answer",
      "query": {
        "match_all": {}
      }
    }
  }
}

親 - 子 - 孫

PUT my-index-000003
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
          "question": ["answer", "comment"],  
          "answer": "vote" 
        }
      }
    }
  }
}
   question
    /    \
   /      \
comment  answer
           |
           |
          vote

データの投入

PUT:

http://localhost:9200/_bulk?refresh=false

親情報の投入

{"index":{"_index":"my-index-000003","_type":"_doc","_id":"question.1"}}
{"type":"question","my_join_field":{"name":"question"},"my_id":"question.1","text":"This is a question"}
{"index":{"_index":"my-index-000003","_type":"_doc","_id":"question.2"}}
{"type":"question","my_join_field":{"name":"question"},"my_id":"question.2","text":"This is another question"}

子情報の投入

{"index":{"_index":"my-index-000003","_type":"_doc","_id":"answer.1","routing":"question.1"}}
{"type":"answer","my_join_field":{"name":"answer","parent":"question.1"},"my_id":"answer.1","text":"This is an answer"}
{"index":{"_index":"my-index-000003","_type":"_doc","_id":"answer.2","routing":"question.1"}}
{"type":"answer","my_join_field":{"name":"answer","parent":"question.1"},"my_id":"answer.2","text":"This is another answer"}
{"index":{"_index":"my-index-000003","_type":"_doc","_id":"answer.3","routing":"question.2"}}
{"type":"answer","my_join_field":{"name":"answer","parent":"question.2"},"my_id":"answer.3","text":"This is more another answer"}

検索

question.1を親に持つ要素の検索

GET my-index-000003/_search
{
  "query": {
    "parent_id": {
      "type": "answer",
      "id": "question.1"
    }
  }
}

親と子検索

GET my-index-000003/_search
{
  "query": {
    "has_child": {
      "type": "answer",
      "query": {
          "match_all": {}
      },
      "inner_hits": {}
     }
  }
}

孫情報の投入

{"index":{"_index":"my-index-000003","_type":"_doc","_id":"vote.1","routing":"answer.1"}}
{"type":"vote","my_join_field":{"name":"vote","parent":"answer.1"},"my_id":"vote.1","text":"This is an vote"}
{"index":{"_index":"my-index-000003","_type":"_doc","_id":"vote.2","routing":"answer.1"}}
{"type":"vote","my_join_field":{"name":"vote","parent":"answer.1"},"my_id":"vote.2","text":"This is another vote"}
{"index":{"_index":"my-index-000003","_type":"_doc","_id":"vote.3","routing":"answer.2"}}
{"type":"vote","my_join_field":{"name":"vote","parent":"answer.2"},"my_id":"vote.3","text":"This is more another vote"}

検索

親-子-孫 検索

GET my-index-000003/_search
{
  "query": {
    "has_child": {
      "type": "answer",
      "query": {
        "has_child": {
          "type": "vote",
          "query": {
              "match_all": {}
          },
          "inner_hits": {
          }
         }
      },
      "inner_hits": {
      }
     }
  }
}

親と子検索

GET my-index-000003/_search
{
  "query": {
    "has_child": {
      "type": "answer",
      "query": {
          "match_all": {}
      },
      "inner_hits": {}
     }
  }
}

子と孫検索

GET my-index-000003/_search
{
  "query": {
    "has_child": {
      "type": "vote",
      "query": {
          "match_all": {}
      },
      "inner_hits": {}
     }
  }
}

パフォーマンス

親参加とパフォーマンス.edit
結合フィールドは、リレーションデータベースの結合のように使うべきではありません。Elasticsearchでは、データをドキュメントに非正規化することがパフォーマンスを良くする鍵となります。各joinフィールド、has_childまたはhas_parentクエリは、クエリのパフォーマンスに大きな税金を追加します。

結合フィールドが意味をなす唯一のケースは、データに一対多の関係が含まれていて、一方のエンティティが他方のエンティティを大幅に上回っている場合です。このようなケースの例としては、商品とその商品のオファーがあるユースケースがあります。オファーの数が製品の数を大幅に上回っている場合は、製品を親ドキュメントとして、オファーを子ドキュメントとしてモデル化することが理にかなっています。

親参加の制限を編集する

1つのインデックスにつき1つの結合フィールドマッピングのみが許可されます。
親ドキュメントと子ドキュメントは、同じシャード上でインデックス化されていなければなりません。つまり、子ドキュメントを取得、削除、更新する際には、同じルーティング値を提供する必要があります。
1つの要素は複数の子を持つことができますが、親は1つだけです。
既存の結合フィールドに新しい関係を追加することができます。
既存の要素に子を追加することも可能ですが、その要素が既に親である場合に限ります。