ElasticsearchのAggregationsを利用したファセットの実現

ElasticsearchのAggregationsを利用していわゆるファセットを実現してみます。
Aggregationsを利用すると結構簡単に色々な集計が出来て便利です！
[Aggregationsの公式ドキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html)

Aggregationの基本的な書き方は以下のようになっています。

```json
"aggs": {
    "集計名(レスポンスのAggregations内で使われるのでわかりやすければ何でもOK)": {
        "Aggregationの種類": {}
    }
}
```

# 環境
Elasticsearch 6.2.3

# 対象データ
以下のような店名と取り扱っている商品の種類、商品名がいくつか入っています。

```json
{
    "market_name": "A八百屋",
    "type": "野菜",
    "product_name": "にんじん"
},
{
    "market_name": "Bスーパー",
    "type": "果物",
    "product_name": "りんご"
},
{
    "market_name": "Cマーケット",
    "type": "野菜",
    "product_name": "セロリ"
}
```

# 商品の種類で集計
商品の種類毎でファセットを作成するには以下のようになります。
[Terms Aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html)を利用すると指定したフィールドの単語単位でドキュメント数が集計されます。
sizeには最大何種類の単語を集計するかを指定します。

```json:query
{
  "aggs": {
    "types": {
      "terms": {
        "field": "type",
        "size": 5
      }
    }
  }
}
```
```json:response(aggregations)
{
  "aggregations": {
    "types": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "野菜",
          "doc_count": 12
        },
        {
          "key": "果物",
          "doc_count": 10
        }
      ]
    }
  }
}
```
これで商品の種類が野菜のドキュメントは12個、果物は10個あることがわかりました。

sizeで上位N件を指定する都合上、全ての種類のドキュメントが集計出来なかったり、集計件数が正確でないパターンが発生することがあります。
そういった場合、以下のレスポンス項目でそういった集計データが存在したことを教えてくれます。

* doc_count_error_upper_bound
インデックスが複数シャードに保存されている場合、上位N件を取得した際に各シャード内で取得対象から漏れるドキュメントがあり、正確に集計できない場合があります。
その場合、doc_count_error_upper_boundにそういったドキュメントが存在した件数が返却されます。
（[公式ドキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-approximate-counts)に詳しい解説があります）
  
* sum_other_doc_count
集計対象には含まれていないユニークな種類の件数を表しています。
この項目が0なら全ての種類のドキュメントが含まれていることになります。

# 商品の種類と商品名で集計
商品の種類と商品名でファセットを作成するには以下のようになります。
商品の種類による集計にネストする形で商品名の集計も指定します。

```json:query
{
  "aggs": {
    "types": {
      "terms": {
        "field": "type",
        "size": 5
      },
      "aggs": {
        "products": {
          "terms": {
            "field": "product_name",
            "size": 20
          }
        }
      }
    }
  }
}
```
```json:response(aggregations)
{
  "aggregations": {
    "types": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "野菜",
          "doc_count": 12,
          "products": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
              {
                "key": "にんじん",
                "doc_count": 3
              },
              {
                "key": "ピーマン",
                "doc_count": 2
              },
              {
                "key": "白菜",
                "doc_count": 2
              },
              {
                "key": "だいこん",
                "doc_count": 1
              },
              {
                "key": "セロリ",
                "doc_count": 1
              },
              {
                "key": "ブロッコリー",
                "doc_count": 1
              },
              {
                "key": "小松菜",
                "doc_count": 1
              },
              {
                "key": "玉ねぎ",
                "doc_count": 1
              }
            ]
          }
        },
        {
          "key": "果物",
          "doc_count": 10,
          "products": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
              {
                "key": "りんご",
                "doc_count": 3
              },
              {
                "key": "みかん",
                "doc_count": 2
              },
              {
                "key": "いちご",
                "doc_count": 1
              },
              {
                "key": "オレンジ",
                "doc_count": 1
              },
              {
                "key": "キウイフルーツ",
                "doc_count": 1
              },
              {
                "key": "バナナ",
                "doc_count": 1
              },
              {
                "key": "レモン",
                "doc_count": 1
              }
            ]
          }
        }
      ]
    }
  }
}
```
これで例えばにんじんが3個、ピーマンが2個あることがわかりました。

# 指定した商品の種類の中の商品名で集計
特定の条件で絞り込んだ中で集計を行う場合はqueryとして絞り込み条件を指定した上で集計を行います。

```json:query
{
  "query": {
    "match": {
      "type": "野菜"
    }
  },
  "aggs": {
    "products": {
      "terms": {
        "field": "product_name",
        "size": 5
      }
    }
  }
}
```
```json:response(aggregations)
{
  "aggregations": {
    "products": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 3,
      "buckets": [
        {
          "key": "にんじん",
          "doc_count": 3
        },
        {
          "key": "ピーマン",
          "doc_count": 2
        },
        {
          "key": "白菜",
          "doc_count": 2
        },
        {
          "key": "だいこん",
          "doc_count": 1
        },
        {
          "key": "セロリ",
          "doc_count": 1
        }
      ]
    }
  }
}
```

# 指定した商品の種類の中の商品名とそれ以外の0件ヒットも集計
特定の条件に一致する件数以外に一致しなかった種類も0件ヒットとして取得したい場合は以下のように集計します。
これにより指定した条件に一致しなくても、データとしては存在するドキュメントを知る事が出来ます。  
集計データに絞り込み条件を適用するには[Filter Aggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filter-aggregation.html)を利用します。
queryで条件を指定する場合は集計元となるドキュメント自体を絞り込みますが、Filter Aggregationを行った場合は指定した条件での集計結果を取得するという違いがあります。

```json:query
{
  "aggs": {
    "products": {
      "terms": {
        "field": "product_name",
        "size": 15
      },
      "aggs": {
        "market_filter": {
          "filter": {
            "term": {
              "market_name": "A八百屋"
            }
          }
        }
      }
    }
  }
}

```
```json:response(aggregations)
{
  "aggregations": {
    "products": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 5,
      "buckets": [
        {
          "key": "にんじん",
          "doc_count": 3,
          "market_filter": {
            "doc_count": 1
          }
        },
        {
          "key": "りんご",
          "doc_count": 3,
          "market_filter": {
            "doc_count": 1
          }
        },
        {
          "key": "みかん",
          "doc_count": 2,
          "market_filter": {
            "doc_count": 1
          }
        },
        {
          "key": "ピーマン",
          "doc_count": 2,
          "market_filter": {
            "doc_count": 1
          }
        },
        {
          "key": "白菜",
          "doc_count": 2,
          "market_filter": {
            "doc_count": 1
          }
        },
        {
          "key": "いちご",
          "doc_count": 1,
          "market_filter": {
            "doc_count": 1
          }
        },
        {
          "key": "だいこん",
          "doc_count": 1,
          "market_filter": {
            "doc_count": 0
          }
        },
        {
          "key": "オレンジ",
          "doc_count": 1,
          "market_filter": {
            "doc_count": 0
          }
        },
        {
          "key": "キウイフルーツ",
          "doc_count": 1,
          "market_filter": {
            "doc_count": 0
          }
        },
        {
          "key": "セロリ",
          "doc_count": 1,
          "market_filter": {
            "doc_count": 0
          }
        }
      ]
    }
  }
}
```
これでA八百屋がどんな商品を取り扱っていて、他にどんな商品があるけど取り扱っていないかがわかります。
market_filterという名前を付けたので、そこの値を見ていきます。
doc_countが0のものがA八百屋で取り扱っていない商品です。

# 商品の種類と店名でそれぞれ別に集計
以下のようにすることで商品の種類毎の集計と店名毎の集計をそれぞれ同時に実行することが出来ます。

```json:query
{
  "aggs": {
    "markets": {
      "terms": {
        "field": "market_name",
        "size": 5
      }
    },
    "types": {
      "terms": {
        "field": "type",
        "size": 5
      }
    }
  }
}

```
```json:response(aggregations)
{
  "aggregations": {
    "types": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "野菜",
          "doc_count": 12
        },
        {
          "key": "果物",
          "doc_count": 10
        }
      ]
    },
    "markets": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "A八百屋",
          "doc_count": 8
        },
        {
          "key": "Bスーパー",
          "doc_count": 8
        },
        {
          "key": "Cマーケット",
          "doc_count": 6
        }
      ]
    }
  }
}
```
typesが商品の種類毎の集計、marketsが店名毎の集計になります。