olivereを使ってElastic Searchのネストした集計を実行する

# はじめに
Go言語のElastic Searchのクライアントを調べると、
- [elastic社公式クライアント](https://github.com/elastic/go-elasticsearch)
- [olivere](https://github.com/olivere/elastic)

の2つが候補になるかと思います。

今回は`olivere`クライアントを使った、
ネストした集約クエリの実装方法と実行結果の取得方法についてまとめます。

(補足) olivereクライアントは2022/07/23現在Elastic Search v8以降に対応していないので注意してください

### 実行環境
Go: `ver 1.18`
Elastic Search: `ver 7.17`
　
# サンプルIndex
サンプルとして以下ようなIndexを想定します
```
POST users/_bulk 
{ "index" : {} }
{ "name": "user1", "age": 15, "created_at": "2022-07-01" }
{ "index" : {} }
{ "name": "user2", "age": 18, "created_at": "2022-07-01" }
{ "index" : {} }
{ "name": "user3", "age": 22, "created_at": "2022-07-02" }
{ "index" : {} }
{ "name": "user4", "age": 34, "created_at": "2022-07-02" }
{ "index" : {} }
{ "name": "user5", "age": 14, "created_at": "2022-07-03" }
{ "index" : {} }
{ "name": "user6", "age": 40, "created_at": "2022-07-03" }
{ "index" : {} }
{ "name": "user7", "age": 60, "created_at": "2022-07-03" }
```

# 集計の設定
実際は検索結果を集計したい場面も多いと思うので、
- 検索
	- 18歳以上のユーザーで絞り込み
- 集計
	- 作成日で1日ごとにグループ化
		- [Date histogram aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html#search-aggregations-bucket-datehistogram-aggregation)で集計できます
	- グループ内の平均年齢を計算
		- [Avg aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html)で集計できます

という設定で集計をしてみようと思います


# 想定クエリ
上記の要件でElastic Searchのクエリを作成すると以下になります

クエリ
```
GET users/_search?pretty 
{
  "size": 0,
  "query": {
    "range": {
      "age": {
        "gte": 18
      }
    }
  },
  "aggs": {
    "group_by_created_at": {
      "date_histogram": {
        "calendar_interval": "day",
        "field": "created_at"
      },
      "aggs": {
        "average_age": {
          "avg": {
            "field": "age"
          }
        }
      }
    }
  }
}
```

実行結果
```
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 5,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "group_by_created_at" : {
      "buckets" : [
        {
          "key_as_string" : "2022-07-01T00:00:00.000Z",
          "key" : 1656633600000,
          "doc_count" : 1,
          "average_age" : {
            "value" : 18.0
          }
        },
        {
          "key_as_string" : "2022-07-02T00:00:00.000Z",
          "key" : 1656720000000,
          "doc_count" : 2,
          "average_age" : {
            "value" : 28.0
          }
        },
        {
          "key_as_string" : "2022-07-03T00:00:00.000Z",
          "key" : 1656806400000,
          "doc_count" : 2,
          "average_age" : {
            "value" : 50.0
          }
        }
      ]
    }
  }
}
```

# olivereによるクエリ実行
本記事の本題に移ります！

上記のクエリをoliverクライアントを使って記述すると以下になります
```go
package main

import (
	"context"
	"fmt"

	"github.com/olivere/elastic/v7"
)

func main() {
	// See: https://olivere.github.io/elastic/#getting-started
	client, err := elastic.NewClient(
		elastic.SetURL("http://es:9200"),
		elastic.SetBasicAuth("elastic", "password"),
	)

	srv := client.Search("users").Size(0)

	// 1. 検索クエリを定義
	query := elastic.NewRangeQuery("age").Gte(18)
	srv.Query(query)

	// 2. 集約クエリを定義
	agg := elastic.NewDateHistogramAggregation().
		Field("created_at").
		CalendarInterval("day")
	{
		// subクエリを定義
		subAgg := elastic.NewAvgAggregation().Field("age")
		agg.SubAggregation("average_age", subAgg)
	}
	srv.Aggregation("group_by_created_at", agg)

	// 3. 実行
	ctx := context.Background()
	result, err := srv.Pretty(true).Do(ctx)
	if err != nil {
		panic(err)
	}

	// 4. 集約結果を取得
	dateHistItems, found := result.Aggregations.DateHistogram("group_by_created_at")
	if !found {
		panic("not found")
	}
	for _, bucket := range dateHistItems.Buckets {
		fmt.Println("--- date ---")
		fmt.Println(*bucket.KeyAsString)

		avgResult, found := bucket.Aggregations.Avg("average_age")
		if !found {
			panic("not found")
		}
		fmt.Println("--- average age ---")
		fmt.Println(*avgResult.Value)
		fmt.Println()
	}
}
```

実行結果
```
--- date ---
2022-07-01T00:00:00.000Z
--- average age ---
18

--- date ---
2022-07-02T00:00:00.000Z
--- average age ---
28

--- date ---
2022-07-03T00:00:00.000Z
--- average age ---
50
```

# おわりに
olivereはメソッドチェーンのように記述でき、慣れてくると直感的に実装出来るようになるのでおすすめです。

ただし、複雑な集計をした実行結果を取得する方法に関するの記事が少なかったので、

少しでも役に立てば幸いです。

https://github.com/olivere/elastic