Elasticsearchでもjoinしたい！Join field typeを使って集計をかけるまで

この記事は[モチベーションクラウドシリーズ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/mcs)の2日目の記事になります。

ElasticsearchでRDBのjoinのようなことを試す機会があったので、そこまでの道のりを備忘録的にまとめてみました。


![Elasticsearch](https://2.bp.blogspot.com/-VuYOw4U5L2Q/V2ubWG5LP_I/AAAAAAAA7oA/Ymw2X9_yP6MYy_31sC9FFY9gAW5VUtv-wCLcB/s400/computer_search_kensaku.png)


# この記事のゴール

以下のようなテストの結果を持つユーザーテーブルと、ユーザーの属性（性別や学年などをイメージして欲しい）を管理するテーブルがあるとします。
このテーブルをRDBでいうところのjoinをして、属性ごとにグルーピングしテストの結果を集計することをゴールとします。

**users table**

| id | name | favorite | english | math | chemistry |
|:---|-----:|:--------:|:-------:|:----:|:---------:|
| 1  | モチベ太郎 | 開発             | 100 | 100 | 100 | 
| 2  | モチベ次郎 | 筋トレ            | 50 | 60 | 85 | 
| 3  | モチベ三郎 | モチベーションの向上 | 70 | 90 | 80 | 

**user_attributes table**

| id | user_id | attribute_id |
|:---|--------:|:------------:|
| 1  | 1       | 1            |
| 2  | 1       | 2            |
| 3  | 1       | 3            |
| 4  | 2       | 1            |
| 5  | 2       | 2            |
| 6  | 3       | 2            |
| 7  | 3       | 3            |


**RDBで実行**

```sql:RDBのクエリ
select attribute_id, AVG(english), AVG(math), AVG(chemistry)
from users 
inner join user_attributes on users.id = user_attributes.user_id
group by user_attributes.attribute_id
```

**実行結果**

```sql:結果
attribute_id	AVG(english)	AVG(math)	AVG(chemistry)
1	75.0000	80.0000	92.5000
2	73.3333	83.3333	88.3333
3	85.0000	95.0000	90.0000
```


# Elasticsearchの環境
Amazon Elasticsearch Serviceを利用しました。
ドメインの作成方法はAWSの[公式ドキュメント（Amazon Elasticsearch Service ドメインの作成と管理）](https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/es-createupdatedomains.html)を参考にしてます。
途中でマスターユーザーの作成を行いますが、後述のKibanaで利用します。

この記事の内容を実行するだけなら、docker上に構築した環境でも問題ありません。

## Elasticsearchバージョン
7.8

# Kibanaログイン
Amazon Elasticsearch Serviceドメインを作成するとKibanaも使用できるようになります。
[Kibana](https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/es-kibana.html) は Elasticsearch で稼働するように設計された、ポピュラーなオープンソースの可視化ツールです。
今回はKibanaを使ってクエリの実行を行います。

## ログイン手順
作成したドメインのダッシュボードにKibanaの項目があるのでリンクをクリックします。
![スクリーンショット 2020-11-20 11.23.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282373/4075bd14-9fbd-ac26-bb6b-298c39174e3c.png)

Kibanaのログイン画面に遷移したらドメイン作成時に作成したマスターユーザーとパスワードでログインします。
![スクリーンショット 2020-11-20 11.28.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282373/74b177b5-62b0-9020-0d50-c47ba63be19a.png)

ログインに成功したらサイドバーを開きDev Toolsを開きます。
![スクリーンショット 2020-11-20 11.30.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282373/5444cf32-db39-609a-9d99-067d2dbd9d8c.png)

Console画面でクエリを実行していきます。
左のエリアにクエリを入力し実行すると右のエリアに結果が表示されます。
レスポンスタイムも表示されるのでパフォーマンス検証にも活用できます。
![スクリーンショット 2020-11-20 11.32.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282373/a6e65242-489b-601a-c8b7-c919189755a8.png)

# ちょっと練習
まずはKibanaでクエリの実行を試したいと思います。

## document作成

まずはdocument作成していきます。

### 1件ずつ登録
indexの作成をしなくてもdocument作成時に指定したindexが無ければ自動で作成されるので省略します。

```Kibana:document作成
PUT hoge/_doc/1
{
  "name": "モチベ太郎",
  "favorite": "開発",
  "english": 100,
  "math": 100,
  "chemistry": 100
}

PUT hoge/_doc/2
{
  "name": "モチベ次郎",
  "favorite": "筋トレ",
  "english": 50,
  "math": 60,
  "chemistry": 85
}

PUT hoge/_doc/3
{
  "name": "モチベ三郎",
  "favorite": "モチベーションの向上",
  "english": 70,
  "math": 90,
  "chemistry": 80
}
```

それぞれ以下が返れば成功
<details><summary>結果</summary><div>

```
{
  "_index" : "hoge",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}
```
</div></details>

### 一括で登録
bulkを使用すれば一括で登録することも可能です。

```Kibana:bulkでdocument作成
POST hoge/_bulk
{"index":{"_index":"hoge","_id":"1"}}
{"name":"モチベ太郎","favorite":"開発","english":100,"math":100,"chemistry":100}
{"index":{"_index":"hoge","_id":"2"}}
{"name":"モチベ次郎","favorite":"筋トレ","english":50,"math":60,"chemistry":85}
{"index":{"_index":"hoge","_id":"3"}}
{"name":"モチベ三郎","favorite":"モチベーションの向上","english":70,"math":90,"chemistry":80}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  "errors" : false,
  "items" : [
    {
      "index" : {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "1",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "2",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "3",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 2,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}
```
</div></details>


## documentの取得

次に作成したdocumentの取得をします。

### 全件取得

```Kibana:document全件取得
GET hoge/_search
{
  "query": {
    "match_all": {}
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ太郎",
          "favorite" : "開発",
          "english" : 100,
          "math" : 100,
          "chemistry" : 100
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ次郎",
          "favorite" : "筋トレ",
          "english" : 50,
          "math" : 60,
          "chemistry" : 85
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ三郎",
          "favorite" : "モチベーションの向上",
          "english" : 70,
          "math" : 90,
          "chemistry" : 80
        }
      }
    ]
  }
```
</div></details>

### 検索して取得

```Kibana:document検索して取得
GET hoge/_search
{
  "query": {
    "term": {
      "name.keyword": {
        "value": "モチベ太郎"
      }
    }
  }
}
```


以下が返れば成功
<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.9808291,
    "hits" : [
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.9808291,
        "_source" : {
          "name" : "モチベ太郎",
          "favorite" : "開発",
          "english" : 100,
          "math" : 100,
          "chemistry" : 100
        }
      }
    ]
  }
}
```
</div></details>

## 集計クエリを使う

Elasticsearchには便利な[集計クエリ](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html)が用意されています。

`group by`や`avg`は集計クエリを使うことで簡単にできます。


### 平均値取得
avgを使います。

```Kibana:平均値取得
GET hoge/_search
{ 
  "aggs": {
    "english_avg": {
      "avg": {
        "field": "english"
      }
    },
    "math_avg": {
      "avg": {
        "field": "math"
      }
    },
    "chemistry_avg": {
      "avg": {
        "field": "chemistry"
      }
    }
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ太郎",
          "favorite" : "開発",
          "english" : 100,
          "math" : 100,
          "chemistry" : 100
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ次郎",
          "favorite" : "筋トレ",
          "english" : 50,
          "math" : 60,
          "chemistry" : 85
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ三郎",
          "favorite" : "モチベーションの向上",
          "english" : 70,
          "math" : 90,
          "chemistry" : 80
        }
      }
    ]
  },
  "aggregations" : {
    "english_avg" : {
      "value" : 73.33333333333333
    },
    "chemistry_avg" : {
      "value" : 88.33333333333333
    },
    "math_avg" : {
      "value" : 83.33333333333333
    }
  }
}
```
</div></details>

### 平均値のみ取得
`"size": 0`を指定します。

```Kibana:size0で平均値取得
GET hoge/_search
{ 
  "size": 0,
  "aggs": {
    "english_avg": {
      "avg": {
        "field": "english"
      }
    },
    "math_avg": {
      "avg": {
        "field": "math"
      }
    },
    "chemistry_avg": {
      "avg": {
        "field": "chemistry"
      }
    }
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "english_avg" : {
      "value" : 73.33333333333333
    },
    "chemistry_avg" : {
      "value" : 88.33333333333333
    },
    "math_avg" : {
      "value" : 83.33333333333333
    }
  }
}
```
</div></details>

### グルーピングして取得
`terms`を使います。

```
GET hoge/_search
{ 
  "size": 0,
  "aggs": {
    "name_aggs": {
      "terms": {
        "field": "name.keyword"
      }
    }
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "name_aggs" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "モチベ三郎",
          "doc_count" : 1
        },
        {
          "key" : "モチベ太郎",
          "doc_count" : 1
        },
        {
          "key" : "モチベ次郎",
          "doc_count" : 1
        }
      ]
    }
  }
}
```
</div></details>

# Join field type を使う

いよいよ本題に入っていきます。
Elasticsearchでjoinするには`Join field type`を使用します。
[Join field type](https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html)とは同じindexのdocument内に親子関係を作成する特別なフィールドです。

## 親子関係の定義
まずはdocumentが親子関係になるように親子関係の定義をします。

userが親、user_attributeが子になるように親子関係を定義します。

```Kibana:親子関係を定義
PUT hoge/_mapping
{
  "properties": {
    "join_field": { 
      "type": "join",
      "relations": {
        "user": "user_attribute" 
      }
    }
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  "acknowledged" : true
}
```

</div></details>

```Kibana:親子関係の確認
GET hoge/_mapping
```
以下が返れば成功
<details><summary>結果</summary><div>

```
{
  "hoge" : {
    "mappings" : {
      "properties" : {
        ... # 省略
        "join_field" : {
          "type" : "join",
          "eager_global_ordinals" : true,
          "relations" : {
            "user" : "user_attribute"
          }
        },
  ... # 省略
}
```

</div></details>


## 親になるdocumentの作成

まず親になるdocumentを作成します。
親になるdocumentには親子関係の定義で設定した`join_field`に`name: user`を指定します。

```Kibana:親document作成
PUT hoge/_doc/1
{
  "name": "モチベ太郎",
  "favorite": "開発",
  "english": 100,
  "math": 100,
  "chemistry": 100,
  "join_field": {
    "name": "user" 
  }
}

PUT hoge/_doc/2
{
  "name": "モチベ次郎",
  "favorite": "筋トレ",
  "english": 50,
  "math": 60,
  "chemistry": 85,
  "join_field": {
    "name": "user" 
  }
}

PUT hoge/_doc/3
{
  "name": "モチベ三郎",
  "favorite": "モチベーションの向上",
  "english": 70,
  "math": 90,
  "chemistry": 80,
  "join_field": {
    "name": "user" 
  }
}
```

## 子になるdocumentの作成

次に子になるdocumentを作成します。
子になるdocumentには`join_field`に`name: user_attribute`と`parent: {親になるdocumetn_id}`を指定します。
また、URLパラメータに`routing=親になるdocumetn_id`を指定します。

```Kibana:子document作成
PUT hoge/_doc/4?routing=1
{
  "user_id": 1,
  "attribute_id": 1,
  "join_field": {
    "name": "user_attribute" ,
    "parent": 1
  }
}

PUT hoge/_doc/5?routing=1
{
  "user_id": 1,
  "attribute_id": 2,
  "join_field": {
    "name": "user_attribute" ,
    "parent": 1
  }
}

PUT hoge/_doc/6?routing=1
{
  "user_id": 1,
  "attribute_id": 3,
  "join_field": {
    "name": "user_attribute" ,
    "parent": 1
  }
}

PUT hoge/_doc/7?routing=2
{
  "user_id": 2,
  "attribute_id": 1,
  "join_field": {
    "name": "user_attribute" ,
    "parent": 2
  }
}

PUT hoge/_doc/8?routing=2
{
  "user_id": 2,
  "attribute_id": 2,
  "join_field": {
    "name": "user_attribute" ,
    "parent": 2
  }
}

PUT hoge/_doc/9?routing=3
{
  "user_id": 3,
  "attribute_id": 2,
  "join_field": {
    "name": "user_attribute" ,
    "parent": 3
  }
}

PUT hoge/_doc/10?routing=3
{
  "user_id": 3,
  "attribute_id": 3,
  "join_field": {
    "name": "user_attribute" ,
    "parent": 3
  }
}
```

## user_attributeを子に持つ親を取得
データの投入ができたのでデータの取得を行なっていきます。
has_childクエリを使用して子documentから親documentを取得します。

### 全件取得

```Kibana:子documentから親documentを取得
GET hoge/_search
{
  "query": {
    "has_child": {
      "type": "user_attribute",
      "query": {
        "match_all": {}
      }
    }
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ太郎",
          "favorite" : "開発",
          "english" : 100,
          "math" : 100,
          "chemistry" : 100,
          "join_field" : {
            "name" : "user"
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ次郎",
          "favorite" : "筋トレ",
          "english" : 50,
          "math" : 60,
          "chemistry" : 85,
          "join_field" : {
            "name" : "user"
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ三郎",
          "favorite" : "モチベーションの向上",
          "english" : 70,
          "math" : 90,
          "chemistry" : 80,
          "join_field" : {
            "name" : "user"
          }
        }
      }
    ]
  }
}
```

</div></details>

### 子のフィールドを検索

```Kibana:子documentのフィールドを検索して親documentを取得
GET hoge/_search
{
  "query": {
    "has_child": {
      "type": "user_attribute",
      "query": {
        "term": {
          "attribute_id": {
            "value": 1
          }
        }
      }
    }
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ太郎",
          "favorite" : "開発",
          "english" : 100,
          "math" : 100,
          "chemistry" : 100,
          "join_field" : {
            "name" : "user"
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "モチベ次郎",
          "favorite" : "筋トレ",
          "english" : 50,
          "math" : 60,
          "chemistry" : 85,
          "join_field" : {
            "name" : "user"
          }
        }
      }
    ]
  }
}
```

</div></details>

## userを親にもつ子を取得する
反対に親documentから子documentを取得する場合はhas_parentクエリを使用します。

### userを親にもつ子documentを全件取得

```Kibana:全件取得
GET hoge/_search
{
  "query": {
    "has_parent": {
      "parent_type": "user",
      "query": {
        "match_all": {}
      }
    }
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 7,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "user_id" : 1,
          "attribute_id" : 1,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 1
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "user_id" : 1,
          "attribute_id" : 2,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 1
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "6",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "user_id" : 1,
          "attribute_id" : 3,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 1
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "7",
        "_score" : 1.0,
        "_routing" : "2",
        "_source" : {
          "user_id" : 2,
          "attribute_id" : 1,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 2
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "8",
        "_score" : 1.0,
        "_routing" : "2",
        "_source" : {
          "user_id" : 2,
          "attribute_id" : 2,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 2
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "9",
        "_score" : 1.0,
        "_routing" : "3",
        "_source" : {
          "user_id" : 3,
          "attribute_id" : 2,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 3
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "10",
        "_score" : 1.0,
        "_routing" : "3",
        "_source" : {
          "user_id" : 3,
          "attribute_id" : 3,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 3
          }
        }
      }
    ]
  }
}
```

</div></details>


### 親のフィールドを検索
もちろん検索もできます

```Kibana:親documentのフィールドを検索して子documentを取得
GET hoge/_search
{
  "query": {
    "has_parent": {
      "parent_type": "user",
      "query": {
        "term": {
          "name.keyword": {
            "value": "モチベ太郎"
          }
        }
      }
    }
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "user_id" : 1,
          "attribute_id" : 1,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 1
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "user_id" : 1,
          "attribute_id" : 2,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 1
          }
        }
      },
      {
        "_index" : "hoge",
        "_type" : "_doc",
        "_id" : "6",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "user_id" : 1,
          "attribute_id" : 3,
          "join_field" : {
            "name" : "user_attribute",
            "parent" : 1
          }
        }
      }
    ]
  }
}
```

</div></details>

## グルーピングして平均値を出す
いよいよゴールです。

### 子のフィールドでグルーピング

```Kibana:子ドキュメントのフィールドでグルーピング
GET hoge/_search
{
  "size": 0, 
  "aggs": {
    "attr_aggs": {
      "terms": {
        "field": "attribute_id"
      }
    }
  }
}
```

以下が返れば成功
<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "attr_aggs" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 2,
          "doc_count" : 3
        },
        {
          "key" : 1,
          "doc_count" : 2
        },
        {
          "key" : 3,
          "doc_count" : 2
        }
      ]
    }
  }
}
```

</div></details>

### 親のフィールドで集計

子ドキュメントのフィールドでグルーピングして親ドキュメントのフィールドを集計するには、集計クエリをネストすることで実現できます。

```Kibana:子ドキュメントのフィールドでグルーピングして平均値を出す
GET hoge/_search
{
  "size": 0,
  "aggs": {
    "attr_aggs": {
      "terms": {
        "field": "attribute_id"
      },
      "aggs": {
        "test_result": {
          "parent": {
            "type": "user_attribute"
          },
          "aggs": {
            "english_avg": {
              "avg": {
                "field": "english"
              }
            },
            "math_avg": {
              "avg": {
                "field": "math"
              }
            },
            "chemistry_avg": {
              "avg": {
                "field": "chemistry"
              }
            }
          }
        }
      }
    }
  }
}
```

<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "attr_aggs" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 2,
          "doc_count" : 3,
          "test_result" : {
            "doc_count" : 3,
            "english_avg" : {
              "value" : 73.33333333333333
            },
            "chemistry_avg" : {
              "value" : 88.33333333333333
            },
            "math_avg" : {
              "value" : 83.33333333333333
            }
          }
        },
        {
          "key" : 1,
          "doc_count" : 2,
          "test_result" : {
            "doc_count" : 2,
            "english_avg" : {
              "value" : 75.0
            },
            "chemistry_avg" : {
              "value" : 92.5
            },
            "math_avg" : {
              "value" : 80.0
            }
          }
        },
        {
          "key" : 3,
          "doc_count" : 2,
          "test_result" : {
            "doc_count" : 2,
            "english_avg" : {
              "value" : 85.0
            },
            "chemistry_avg" : {
              "value" : 90.0
            },
            "math_avg" : {
              "value" : 95.0
            }
          }
        }
      ]
    }
  }
}
```

</div></details>

参考 [Parent aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-parent-aggregation.html)


### 子のフィールドで集計
おまけで子のフィールドでも集計してみましょう。
`parent`クエリではなく`children`クエリを使用します。

``````Kibana:親ドキュメントのフィールドでグルーピングして合計値を出す
GET hoge/_search
{
  "size": 0,
  "aggs": {
    "user_aggs": {
      "terms": {
        "field": "name.keyword"
      },
      "aggs": {
        "test_result": {
          "children": {
            "type": "user_attribute"
          },
          "aggs": {
            "user_id_sum": {
              "sum": {
                "field": "user_id"
              }
            }
          }
        }
      }
    }
  }
}
```

<details><summary>結果</summary><div>

```
{
  ..., # 省略
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "user_aggs" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "モチベ三郎",
          "doc_count" : 1,
          "test_result" : {
            "doc_count" : 2,
            "user_id_sum" : {
              "value" : 6.0
            }
          }
        },
        {
          "key" : "モチベ太郎",
          "doc_count" : 1,
          "test_result" : {
            "doc_count" : 3,
            "user_id_sum" : {
              "value" : 3.0
            }
          }
        },
        {
          "key" : "モチベ次郎",
          "doc_count" : 1,
          "test_result" : {
            "doc_count" : 2,
            "user_id_sum" : {
              "value" : 4.0
            }
          }
        }
      ]
    }
  }
}
```

</div></details>



# 最後に
無事に成し遂げることが出来ました。
`Join field type`は親子だけではなく親子孫や親に子を二人つけることも可能です。
データ構造やクエリが複雑になりそうですがいろいろな検索に役立てそうなことがわかりました
ただし、`has_child`または`has_parent`クエリはクエリのパフォーマンスに大きな負担をかけるので、パフォーマンス重視であれば別の方法を検討しましょう。
注意点としてはあくまでもRDBのjoinとは似て非なるものなので、RDBのjoinのように使用してはいけないということを覚えておきましょう。

参考 [Parent-join and performance](https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html#_parent_join_and_performance)