👓

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

2022/07/23に公開

はじめに

Go言語のElastic Searchのクライアントを調べると、

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

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

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

実行環境

Go: 1.18
Elastic Search: 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歳以上のユーザーで絞り込み
  • 集計

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

想定クエリ

上記の要件で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クライアントを使って記述すると以下になります

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

Discussion