elasticsearch-dsl で集計を伴う検索クエリを書く

Elasticsearch の高レベル Python クライアントである [elasticsearch-dsl](https://github.com/elastic/elasticsearch-dsl-py) ([ドキュメント](https://elasticsearch-dsl.readthedocs.io/en/latest/index.html)) を使ってみました[^1]。
[^1]: 今回は elasticsearch-dsl のバージョンとして `6.3.0` を使いました。
elasticsearch-dsl を用いると、低レベル Python クライアントである [elasticsearch](https://github.com/elastic/elasticsearch-py) を用いる場合に比べて直観的に検索クエリを作成することができます。

# 事前準備

まずは下記のようなインデックス・データを作成します。

```python
from elasticsearch import Elasticsearch
elastic = Elasticsearch()

index = "dsl_sample"
elastic.indices.create(index)

data = [                                                         
    {"hoge": 1, "fuga": 1, "value": 3},
    {"hoge": 1, "fuga": 2, "value": 4},
    {"hoge": 2, "fuga": 1, "value": 5},
    {"hoge": 2, "fuga": 2, "value": 6},
    {"hoge": 1, "fuga": 1, "value": 100}
]

for i, body in enumerate(data, 1):
    elastic.index(index=index, doc_type="_doc", id=i, body=body)
```

# 検索・集計をする

今回は下記のような集計を行う検索クエリを作ってみます。

- valueが1以上100未満の値のみが集計対象となるようにフィルタする
- 1つのクエリで「hoge で Groub By した集計結果」と「fuga で Group By した集計結果」の2つを取得する

このクエリは elasticsearch-dsl を用いて下記のように実行できます。

```python
from elasticsearch_dsl import Search, A

search = Search(using=elastic, index=index)  # ① Search オブジェクトの作成
search = search.filter("range", value={"gte": 1, "lt": 100})  # ②フィルタ条件の記載
search = search.extra(size=0)  # ③ドキュメントの取得件数を指定

# ④ Aggregation オブジェクトの作成
aggs_hoge = A("terms", field="hoge", size=10).metric('sum_value', 'sum', field='value')
aggs_fuga = A("terms", field="fuga", size=10).metric('sum_value', 'sum', field='value')

# ⑤ Aggregation オブジェクトを Search オブジェクトに紐付ける
search.aggs.bucket("aggs_name_hoge", aggs_hoge)
search.aggs.bucket("aggs_name_fuga", aggs_fuga)

response = search.execute()
```

- ①で検索に用いるオブジェクトを作成します。例のように `using` で既に作成した低レベルクライアントを渡すことも可能ですし、 [connections](https://elasticsearch-dsl.readthedocs.io/en/latest/configuration.html#) を用いて接続することもできるようです。
- 今回は集計結果が取得できればよく、ドキュメント自体を返却する必要はないので③で取得件数を 0 に指定しています。[extra](https://elasticsearch-dsl.readthedocs.io/en/latest/search_dsl.html)を用いれば検索リクエストに任意のプロパティ値を設定できるようです。
- ④の `A` で集計について定義しています。
    - `A` の `field` で集計のキーを指定しています。
    - `metric` を用いて、どのカラムをどのように集計するのかを指定します。なおドキュメント数のみで良い場合は `metric` の指定が不要となります。
- ⑤で検索オブジェクトに集計オブジェクトを関連づけます。このように2回 `bucket` を呼ぶことで、2種類の集計が関連づけられます。

説明のために行を分けましたが、①〜③あたりは `Search(・・・).filter(・・・).extra(・・・)` みたいにメソッドチェーンで直観的に書けるのが良いですね。

結果に対しては下記のようにアクセス可能です。

```python
response.aggregations.aggs_name_hoge.buckets
# [{'key': 1, 'doc_count': 2, 'sum_value': {'value': 7.0}}, {'key': 2, 'doc_count': 2, 'sum_value': {'value': 11.0}}]

response.aggregations.aggs_name_fuga.buckets
# [{'key': 1, 'doc_count': 2, 'sum_value': {'value': 8.0}}, {'key': 2, 'doc_count': 2, 'sum_value': {'value': 10.0}}]

```

正しく集計されていることがわかります。

# dict のクエリを確認する
elasticsearch-dsl では `to_dict` を用いて、低レベルクライアントでリクエストする場合の検索クエリ dict を取得することができます。

```python
search.to_dict()

# {
#   'query': {
#     'bool': {
#       'filter': [
#         {
#           'range': {
#             'value': {
#               'gte': 1,
#               'lt': 100
#             }
#           }
#         }
#       ]
#     }
#   },
#   'aggs': {
#     'aggs_name_hoge': {
#       'terms': {
#         'field': 'hoge',
#         'size': 10
#       },
#       'aggs': {
#         'sum_value': {
#           'sum': {
#             'field': 'value'
#           }
#         }
#       }
#     },
#     'aggs_name_fuga': {
#       'terms': {
#         'field': 'fuga',
#         'size': 10
#       },
#       'aggs': {
#         'sum_value': {
#           'sum': {
#             'field': 'value'
#           }
#         }
#       }
#     }
#   },
#   'size': 0
# }
```
低レベル Python クライアントの elasticsearch を用いて検索クエリを作る場合はこの dict を直接作成する必要があるので、それに比べると elasticsearch-dsl を使えばだいぶ楽に検索クエリが書ける気がしますね。

# おわりに
今回は elasticsaerch-dsl で簡単な集計クエリを書いてみました。
「集計を2つ設定するにはどうするのか？」とか「size=0を指定するにはどうするのか？」など最初は少し試行錯誤しましたが、慣れれば django の QuerySet のようにメソッドチェーンで検索クエリが書けるので直観的で良いなと思いました。

明日は @usangit が何か書いてくれます。お楽しみに！