Python × Elasticsearchで検索と集計をしてみよう

#目次
[1.はじめに](#1-はじめに)
[2.準備](#2-準備)
[3.検索](#3-検索)
[4.集計](#4-集計)
[5.後片付け](#5-後片付け)
[6.終わりに](#6-終わりに)

#1. はじめに
この記事は [2020 年の RevComm アドベントカレンダー](https://qiita.com/advent-calendar/2020/revcomm) 13 日目の記事です。 12 日目は tomohiro86 さんの「[Apollo Clientでwebsocket通信してみよう](https://qiita.com/tomohiro86/private/9d36b4c7731c5dcb1dde)」でした。 

[株式会社RevComm](https://www.revcomm.co.jp/)は音声解析AI電話[MiiTel](https://miitel.revcomm.co.jp/)の開発を行っています。
MiiTel では会話の全文文字起こしを提供しており、文字起こしした文章から単語の検索ができます。

この記事では、Python（elasticsearch_dsl）とElasticsearch(AWS)を使って検索と集計をする方法をご紹介します。

#2. 準備
##IAM ユーザーの作成
https://console.aws.amazon.com/iam/home#users
ユーザーを追加ボタンからAWS Elasticsearch Serviceを使えるユーザーを作成します。
**アクセスキー ID、シークレットアクセスキー、ARNをコピーしておいてください。**
##Elasticsearch ドメインの作成
https://ap-northeast-1.console.aws.amazon.com/es/home
新しいドメインの作成ボタンから作成します。
1. デプロイタイプは開発およびテスト
![スクリーンショット 2020-12-04 15.18.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321447/68484899-6bb7-667e-3018-3419b0161711.png)
2. Elasticsearch ドメイン名を適当につけます。
![スクリーンショット 2020-12-04 15.21.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321447/8c1d0933-9f4c-4e35-b122-f844e0a4d24c.png)
3. ネットワーク構成は今回はパブリックアクセスにします。
　そして、「細かいアクセスコントロールを有効化」のチェックを外し、
　アクセスポリシーに先ほどコピーしておいたARNを貼り付けます。
![スクリーンショット 2020-12-04 15.23.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321447/a5f1bb2c-3e3c-ee0b-787f-02dd7943e798.png)
![スクリーンショット 2020-12-04 15.29.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321447/652c5795-b6de-724a-7c3e-5994294f4be2.png)
4. 内容を確認して確認ボタンを押したら作成が開始されます。
　ドメインのステータスが「読み込み中...」から「アクティブ」に変わったら作成完了です。
　少し時間がかかるのでの次に進みましょう。

##データ用意
日本歴代興行収入ランキングのTOP10を用意しました。
2020年11月29日時点のデータです。（鬼滅の刃はどこまで伸びるのだろうか？）
出典: http://www.kogyotsushin.com/archives/alltime/

```json:movies.json
[
    {
        "title": "千と千尋の神隠し",
        "company": "東宝",
        "type": "animation",
        "year": 2011,
        "rank": 1,
        "income": 308.0
    },
    {
        "title": "劇場版「鬼滅の刃」無限列車編",
        "company": "東宝",
        "type": "animation",
        "year": 2020,
        "rank": 2,
        "income": 275.1
    },
    {
        "title": "タイタニック",
        "company": "FOX",
        "type": "live_action",
        "year": 1997,
        "rank": 3,
        "income": 262.0
    },
    {
        "title": "アナと雪の女王",
        "company": "ディズニー",
        "type": "animation",
        "year": 2014,
        "rank": 4,
        "income": 255.0
    },
    {
        "title": "君の名は。",
        "company": "東宝",
        "type": "animation",
        "year": 2016,
        "rank": 5,
        "income": 250.3
    },
    {
        "title": "ハリー・ポッターと賢者の石",
        "company": "ワーナー",
        "type": "live_action",
        "year": 2001,
        "rank": 6,
        "income": 203.0
    },
    {
        "title": "ハウルの動く城",
        "company": "東宝",
        "type": "animation",
        "year": 2004,
        "rank": 7,
        "income": 196.0
    },
    {
        "title": "もののけ姫",
        "company": "東宝",
        "type": "animation",
        "year": 1997,
        "rank": 8,
        "income": 193.0
    },
    {
        "title": "踊る大捜査線 THE MOVIE2 レインボーブリッジを封鎖せよ！",
        "company": "東宝",
        "type": "live_action",
        "year": 2003,
        "rank": 9,
        "income": 173.5
    },
    {
        "title": "ハリー・ポッターと秘密の部屋",
        "company": "ワーナー",
        "type": "live_action",
        "year": 2002,
        "rank": 10,
        "income": 173.0
    }
]
```

##ライブラリのインストール
```bash
pip install elasticsearch elasticsearch_dsl requests_aws4auth
```

##データ追加
準備したElasticsearchにPythonスクリプトでデータを追加していきましょう。

```python
import json
from elasticsearch import Elasticsearch, RequestsHttpConnection, helpers
from requests_aws4auth import AWS4Auth

HOST = 'search-test-foobar.ap-northeast-1.es.amazonaws.com'
awsauth = AWS4Auth(
    'アクセスキー ID',
    'シークレットアクセスキー',
    'ap-northeast-1',
    'es'
)

es = Elasticsearch(
    hosts=[{'host': HOST, 'port': 443}],
    http_auth=awsauth,
    use_ssl=True,
    verify_certs=True,
    connection_class=RequestsHttpConnection
)

def generate():

    with open('movies.json', 'r') as f:
        movies = json.load(f)

    for movie in movies:
        yield {
            "_op_movie_type": "create",
            "_index": "movies",
            "_source": movie
        }

helpers.bulk(es, generate())
print(es.count(index="movies")["count"])
# 結果
# 10　もし10じゃなかったら少し時間を置いて試してみてください。
```
次にtextデータのfielddataをtrueにします。
これをしないとtextデータを集計の対象にしてくれません。

```python
from pprint import pprint

MAPPING = {
    "properties": {
        "company": {"fields": {"keyword": {"ignore_above": 256, "type": "keyword"}}, "type": "text", "fielddata": True},
        "title": {"fields": {"keyword": {"ignore_above": 256, "type": "keyword"}}, "type": "text", "fielddata": True},
        "type": {"fields": {"keyword": {"ignore_above": 256, "type": "keyword"}}, "type": "text", "fielddata": True},
    }
}

es.indices.put_mapping(index="movies", body=MAPPING)
pprint(es.indices.get_mapping()["movies"])
# 結果
# {'mappings': {'properties': {'company': {'fielddata': True,
#                                         'fields': {'keyword': {'ignore_above': 256,
#                                                                'type': 'keyword'}},
#                                         'type': 'text'},
#                             'income': {'type': 'float'},
#                             'rank': {'type': 'long'},
#                             'title': {'fielddata': True,
#                                       'fields': {'keyword': {'ignore_above': 256,
#                                                              'type': 'keyword'}},
#                                       'type': 'text'},
#                             'type': {'fielddata': True,
#                                      'fields': {'keyword': {'ignore_above': 256,
#                                                             'type': 'keyword'}},
#                                      'type': 'text'},
#                             'year': {'type': 'long'}}}}
```

これで準備は完了です。

#3. 検索
それでは検索をしてみましょう。
[Elasticsearchのドキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-your-data.html)
[elasticsearch_dslのドキュメント](https://elasticsearch-dsl.readthedocs.io/en/latest/search_dsl.html)

```python:会社が東宝のアニメ作品
from elasticsearch_dsl import Search

s = Search(using=es).sort('rank').query("match", type="animation").query("match", company="東宝")
for hit in s:
    print(hit.title)

# 結果
# 千と千尋の神隠し
# 劇場版「鬼滅の刃」無限列車編
# 君の名は。
# ハウルの動く城
# もののけ姫
```

```patch:2000年以前の作品
- s = Search(using=es).sort('rank').query("match", type="animation").query("match", company="東宝")
+ s = Search(using=es).filter("range", year={"lte": 2000})

# 結果
# タイタニック
# もののけ姫
```

```python:「ハリー」または「アナ」がつく作品
from elasticsearch_dsl import Search, Q

q = Q("multi_match", query='ハリー アナ', fields=['title'])

s = Search(using=es).sort('rank').query(q)
for hit in s:
    print(hit.title)

# 結果
# アナと雪の女王
# ハリー・ポッターと賢者の石
# ハリー・ポッターと秘密の部屋
```

```patch:会社が東宝でタイトルに「の」がつかない作品
- q = Q("multi_match", query='ハリー アナ', fields=['title'])
+ q = Q('bool', must=[Q("match", company="東宝"), ~Q("match", title="の")])

# 結果
# 踊る大捜査線 THE MOVIE2 レインボーブリッジを封鎖せよ！
```


#4. 集計
次に集計をしてみましょう。
[Elasticsearchのドキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html)
[elasticsearch_dslのドキュメント](https://elasticsearch-dsl.readthedocs.io/en/latest/search_dsl.html#aggregations)

まずはtermsを試してみます。

```python:companyで集計
from pprint import pprint
from elasticsearch_dsl import Search, A

s = Search(using=es)

a = A('terms',  field='company')
s.aggs.bucket('results', a)
resp = s.execute()
pprint(resp.aggregations._d_['results']['buckets'])

# 結果
# [{'doc_count': 6, 'key': '宝'},
#  {'doc_count': 6, 'key': '東'},
#  {'doc_count': 2, 'key': 'ワーナー'},
#  {'doc_count': 1, 'key': 'fox'},
#  {'doc_count': 1, 'key': 'ディズニー'}]
```
東宝が6作品、ワーナー、FOX、ディズニーがそれぞれ１作品ということがわかりました！

```patch:typeで集計
- a = A('terms',  field='company')
+ a = A('terms',  field='type')

# 結果
# [{'doc_count': 6, 'key': 'animation'},
#  {'doc_count': 4, 'key': 'live_action'}]
```
アニメが６作品実写が４作品ということがわかりました！


```patch:公開年のヒストグラム
- a = A('terms',  field='type')
+ a = A('histogram', field='year', interval='5', offset='1901')

# 結果
# [{'doc_count': 2, 'key': 1996.0},
#  {'doc_count': 4, 'key': 2001.0},
#  {'doc_count': 0, 'key': 2006.0},
#  {'doc_count': 2, 'key': 2011.0},
#  {'doc_count': 2, 'key': 2016.0}]
```
2001年から2005年が4作品と最も多いことがわかりました！

他にもいろいろな集計方法がありますのでドキュメントを見てぜひ遊んでみてください。

#5. 後片付け 
最後に作成したIAMユーザーとElasticsearchは忘れずに削除しましょう。
## IAMユーザーの削除
https://console.aws.amazon.com/iam/home?#/users
対象のユーザーにチェックを入れてユーザーの削除ボタンをクリック
## Elasticsearchの削除
https://ap-northeast-1.console.aws.amazon.com/es/home
対象のドメインをクリックし、アクション>ドメインの削除をクリック

#6. 終わりに
今まで、ElasticSearchを使ったことはありませんでしたが、弊社ではやりたいと手を挙げればやらせてもらえる環境のため挑戦させてもらいました。
来年もどんどん挑戦し、どんどん成長していきたいと思います！

明日は  tatakahashi35 さんの「[RevCommの裏側で動くアルゴリズム [二分探索編]](https://qiita.com/tatakahashi35/items/624c5fe9efc0bd5e340f)」です。
お楽しみにー！