Elasticsearchは検索だけじゃない！Aggregationって何？

# Aggregation

## Aggregation の概要

- Aggregation とは検索結果の集合に対して、分類や集計を行うことができる機能。
- Elasticsearch では通常の検索処理をしながら、同時に Aggregation による統計値を求めることが一回のクエリ操作で可能。

## Aggregation の構成

Aggregation には分類や集計を行うために多くのタイプがある。それらのタイプをカテゴリ分けすると以下の 4 つに分類される。

- Metrics
  Metrics は分類された各グループに対して、最小、最大、平均などの統計値を計算する。 SQL における `COUNT`、`SUM`、`AVG` などの関数に相当する。

- Buckets
  Buckets はドキュメントをそのフィールド値に基づいてグループ化するための分類方法を表す(書籍ジャンルを格納するフィールドを仕様してジャンルごとにグループ化するなど)。 SQL における `GROUP BY` に相当する。 Buckets を複数構成して、グループとそのサブグループといった階層構造を取ることも可能。

- Pipeline
  Pipeline は Bucket など他の Aggregation の結果をもとに、新しい集計を行う機能。例えば、月別の平均株価を分類・集計し、さらに前月分からの差分を計算するなど。

- Matrix
  Matrix は複数のフィールドの値を対象に相関や共分散などの統計値を計算することができる機能。
  一般的な分析ワークフローでは頻繁に使用されるものではなく、特殊なユースケース向けの機能であり、将来のリリースで利用できなくなる可能性もある。

このうち、Metrics と Bucket が基本的なカテゴリと言える。ドキュメント全体を Buckets でグループ化し、グループごとの統計値を Metrics で求めるというのが典型的なユースケース。

## Aggregation のクエリ記法

Aggregation を含めたクエリ全体は他のオプションも含めると以下の通り。
ここから発展して、"aggregations"句を複数指定したり、"aggregations"句の中にさらに"aggregations"句をネストした形で定義することも可能。

```bash
$ curl -XGET 'http://localhost:9200/test_index/_search' \
-H 'Content-Type: application/json' -d'
{
  "query": { <クエリ内容> },
  "from": <検索結果の開始位置>,
  "size": <検索結果の取得件数>,
  "sort": [ <検索結果のソート指定> ],
  "_source": [ <検索結果に含めたいフィールド名(デフォルトは全てのフィールド)> ],
  "aggregations": {                # 省略してaggs句としても可
    <aggregation_name>: {          # 任意の名前
      <aggregation_type>: {        # 使用する Aggregation のタイプ(metricsの場合は平均値を求める場合はavg)
      <aggregation_body>           # 使用する Aggregation のパラメーター(例: avgタイプの場合は平均する対象のフィールドを指定)
      }
    }
  }
}'
```

## Metrics の定義方法

Metrics は指定したデータセットにおける最小値、最大値、平均値、合計値などを計算する。
データセットを指定する方法は以下の 2 つ。

- **query**
  query 句で検索条件を指定することで、まずデータセットが絞り込まれ、その絞り込んだデータセットに対して Metrics の集計を行う。
- **Bucket**
  Bucket 句でドキュメントをグループ化することで、そのグループごとに Metrics の集計を行う。

上記のデータセットの指定を使わない場合は、全てのドキュメントに対して Metrics の集計を行う。

:::note info
"size": 0 を指定して Aggregation の統計値のみを取り出す

Aggregation のクエリで "size" 句に 0 を指定すると、"query" 句で検索したドキュメント内容はレスポンスに含まれずに、 Aggregation の結果のみが返される。
ドキュメントの検索結果が特に必要でなく、計算結果のみが知りたい場合はこちらの設定でクエリレスポンスを簡略化できる。
:::

### avg

```bash
# officeフィールドがTokyoのドキュメントに絞り込み、salaryフィールドの平均値を求める
$ curl -XGET 'http://localhost:9200/employee/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "query": {
    "match": {
      "office": "Tokyo"
    }
  },
  "aggregations": {
    "avg_salary": {
      "avg": {
        "field": "salary"
      }
    }
  }
}'
```

```bash: レスポンス
# クエリの結果として30件がヒットし、その平均値が300000となる
{
  "took": 6,
  "timed_out": false,
  "shards": {
    "total": 30,
    "successful": 30,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 30,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "avg_salary": {
      "value": 300000
    }
  }
}
```

### max/min

```bash
# groupフィールドがHR sectionのドキュメントに絞り込み、salaryフィールドの最大/最小値を求める
$ curl -XGET 'http://localhost:9200/employee/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "query": {
    "term": {
      "group": "HR section"
    }
  },
  "aggs": {
    "min_salary": {
      "min": {
        "field": "salary"
      }
    },
    "max_salary": {
      "max": {
        "field": "salary"
      }
    }
  }
}
'
```

```bash: レスポンス
{
  略
  "aggregations": {
    "max_salary": {
      "value": 500000
    },
    "min_salary": {
      "value": 100000
    }
  }
}
```

### sum

sum は指定したフィールドの値を合計する。クエリ、レスポンスは avg, max/min と同じ。

### cardinality

- cardinality は指定したフィールドの値の種類の数を概算値で計算する。(例: カレンダーの月を格納するフィールド = 12 種類)
- 大量のデータを扱う場合、完全に正確な値を求めようとするとメモリリソースなどを大量に消費するので、Elasticsearch では概算値が計算されるようになっている。
- デフォルトでは約 2% の誤差で計算されるが、`precision_threshold`パラメータで精度を調整可能。
- より高い精度を求める場合はメモリ使用量が増える。

```bash
#
$ curl -XGET 'http://localhost:9200/access_log/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "unique_user": {
      "cardinality": {
        "field": "user_name.keyword"
      }
    }
  }
}
'
```

```bash: レスポンス
{
  略
  "aggregations": {
    "unique_user": {
      "value": 1000
    }
  }
}
```

### stats

stats は指定したフィールドの最小値、最大値、平均値、合計値、データ数をまとめて計算する。

```bash
$ curl -XGET 'http://localhost:9200/employee/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "my_stats_example": {
      "stats": {
        "field": "salary"
      }
    }
  }
}
'
```

```bash: レスポンス
{
  略
  "aggregations": {
    "my_stats_example": {
      "stats": {
        "count": 1000,
        "min": 100000,
        "max": 500000,
        "avg": 300000,
        "sum": 300000000
      }
    }
  }
}
```

## Buckets の定義方法

- Buckets はドキュメント群をあるフィールド値に基づいてグループ化するための方法を定義する。
- Metrics を定義せず、 Buckets のみを定義することも可能で、その場合は doc_count の値が取得できる。

### terms

terms は指定したフィールドの一意の値ごとにドキュメントをグループ化する。

```bash
#
$ curl -XGET 'http://localhost:9200/books/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "my_genre_buckets": {          # 任意のBucket名
      "terms": {
        "field": "genre.keyword",  # グループ化に使用するフィールドを指定
        "size": 3                  # レスポンスで返却されるBucketの上限数を指定(デフォルトは10)
      }
    }
  }
}
'
```

```bash: レスポンス
{
  略
  "aggregations": {
    "my_genre_buckets": {
      "buckets": [
        {
          "key": "Science fiction",
          "doc_count": 1000
        },
        {
          "key": "Fantasy",
          "doc_count": 250
        },
        {
          "key": "Romance",
          "doc_count": 30
        }
      ]
    }
  }
}
```

### range

- range は数値型のフィールドに対して、指定したフィールド値の範囲に従って Buckets を分類する機能。
- range を使用する場合は、 `field` 句とともに `ranges` 句を使用して、範囲を指定する。
- 範囲指定は `from` 句は「以上」、`to` 句は「未満」という扱いとなる。(2000 は 「"from": 2000, "to": 3000」という範囲に含まれる)

```bash
$ curl -XGET 'http://localhost:9200/books/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "my_range_buckets": {
      "range": {
        "field": "price",
        "ranges": [
          { "from": 0, "to": 1000 },
          { "from": 1000, "to": 2000 },
          { "from": 2000, "to": 3000 }
        ]
      }
    }
  }
}
'
```

```bash: レスポンス
{
  略
  "aggregations": {
    "my_range_buckets": {
      "buckets": [
        {
          "key": "*-1000.0",
          "to": 1000.0,
          "doc_count": 1000
        },
        {
          "key": "1000.0-2000.0",
          "from": 1000.0,
          "to": 2000.0,
          "doc_count": 2000
        },
        {
          "key": "2000.0-3000.0",
          "from": 2000.0,
          "to": 3000.0,
          "doc_count": 3000
        },
        {
          "key": "3000.0-*",
          "from": 3000.0,
          "doc_count": 4000
        }
      ]
    }
  }
}
```

### date_range

- date_range は日付型のフィールドを指定した範囲に基づいて Buckets を分類する機能。
- `from` 句は `to` 句には以下の時刻を表す特殊表現を使用できる。

| 記号       | 意味       |
| ---------- | ---------- |
| NOW        | 現在の時刻 |
| y          | 年         |
| M          | 月         |
| w          | 週         |
| d          | 日         |
| h          | 時間       |
| h または H | 時間       |
| m          | 分         |
| s          | 秒         |

```bash
$ curl -XGET 'http://localhost:9200/books/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "my_date_range_buckets": {
      "date_range": {
        "field": "published",
        "ranges": [
          { "from": "Now-1y", "to": "Now-1M" },
          { "from": "Now-1M", "to": "Now" }
        ]
      }
    }
  }
}
'
```

### histogram

- histogram は数値型のフィールドを指定して、値の範囲で Buckets を分類する機能。
- 範囲を直接指定するのではなく、増分値を指定する。

```bash
$ curl -XGET 'http://localhost:9200/books/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "my_histogram_buckets": {
      "histogram": {
        "field": "price",
        "interval": 1000
      }
    }
  }
}
'
```

```bash: レスポンス
{
  略
  "aggregations": {
    "my_histogram_buckets": {
      "buckets": [
        {
          "key": 0,
          "doc_count": 1000
        },
        {
          "key": 1000,
          "doc_count": 2000
        },
        {
          "key": 2000,
          "doc_count": 3000
        },
        {
          "key": 3000,
          "doc_count": 4000
        },
        ...
      ]
    }
  }
}
'
```

:::note info
テキスト文字列を Aggregation

テキスト文字列をフィールドに格納する場合、 text 型もしくは keyword 型の 2 つの型がある。
しかし、 text 型は格納時に Analyzer によって単語に分割され元の語句を保持しないため、 Aggregation で使用できるのは keyword 型のみ。
通常、テキスト文字列を型を指定しない、もしくは text 型を指定してインデックスした場合、サブフィールドとして keyword 型のサブフィールドが自動的に作成される。

:::

:::note info
doc values

keyword 型や他の数値型をインデックスに格納する際には、転置インデックスとは別に `doc_values` というデータ構造にフィールド単位で格納・管理される。
Aggregation では転置インデックスではなく、この `doc_values` を使用することで高速な処理が実現可能となっている。

:::

## Metrics と Buckets の組み合わせ

Metrics と Buckets の組み合わせるには、まず Buckets でドキュメントをグループ化し、その Bucket ごとに Metrics を定義することで実現できる。

```bash
$ curl -XGET 'http://localhost:9200/books/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "my_genre_buckets": {                # 任意のBucket名
      "terms": {                         # まずは Buckets を定義して分類方法を決める
        "field": "genre.keyword"         # グループ化に使用するフィールドを指定
      },
      "aggs": {                          # その Bucket ごとに Metrics を定義する
        "my_price_avg": {                # 任意のMetrics名
          "avg": { "field": "price" }    # 使用する Metrics のタイプと対象フィールドを指定
        }
      }
    }
  }
}
'
```

```bash: レスポンス
{
  略
  "aggregations": {
    "my_genre_buckets": {
      "buckets": [
        {
          "key": "Science fiction",
          "doc_count": 1000,
          "my_price_avg": {
            "value": 1500.0
          }
        },
        {
          "key": "Fantasy",
          "doc_count": 250,
          "my_price_avg": {
            "value": 1500.0
          }
        },
        {
          "key": "Romance",
          "doc_count": 30,
          "my_price_avg": {
            "value": 1500.0
          }
        }
      ]
    }
  }
}
'
```

## significant_terms

significant_terms はある語句の傾向として、全体的に見ると目立たないけど、特定の範囲の中で見ると突出して目立つといった傾向を見つけるための Aggregation タイプ。

```bash
$ curl -XGET 'http://localhost:9200/cars/_search' \
-H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "prefecture_terms": {
      "terms": {
        "field": "prefecture"
      }
    },
    "aggs": {
      "market_share_of_cars_manufacturer": {
        "significant_terms": {
          "field": "manufacturers"
        }
      }
    }
  }
}
'
```

```bash: レスポンス
...
"key": "Hiroshima",
"doc_count": 58000,
"market_share_of_cars_manufacturer": {
  "doc_count": 58000,
  "buckets": [
    {
      "key": "Mazda",
      "doc_count": 6356,
      "score": 2.438765688376262        # この値が大きいほど、全国平均と比べて Hiroshima でのシェアが目立って高いということを示す(全国平均: 5%, Hiroshima: 10% である場合など)
    },
    {
      "key": "Toyota",
      "doc_count": 1000,
      "score": 1.688399484989348
    },
    {
      "key": "Honda",
      "doc_count": 1000,
      "score": 1.4978932894738974
    },
    ...
}
...
```