Django から Elasticsearch を使う


これは[Elasticsearch Advent Calendar 2014](http://qiita.com/advent-calendar/2014/elasticsearch) の24日目の記事です。

**2019/12/03更新**
`ElasticUtils` は開発終了し `haystack` も開発が停滞しているため、現在はElastic公式の [elasticsearch-dsl-py](https://github.com/elastic/elasticsearch-dsl-py) を使うのが最もベターな選択肢でしょう。


## 前置き
Django を使って検索機能を実装する場合、標準ORMである QuerySet を使うと思います。
しかし ORMを通してDBへクエリを発行するので、データ数が数十万件規模だったり、複数の Model をまたぐ検索だとJOINが多くなりパフォーマンスが出ません。

当然といえば当然ですね。

そこで、検索のバックエンドに Elasticsearch を導入するのが一つの案となります。
ここでは、Django に Elasticsearch を導入する方法をいくつか紹介したいと思います。

## Python用 Elasticsearch ライブラリ

### Haystack を使う
http://haystacksearch.org/

Haystack は複数の検索バックエンドに対して、Django の QuerySet とよく似たAPIを提供してくれます。

標準ORMとよく似たコードで検索クエリを発行できるので、検索エンジンごとのクエリDSLを覚える必要が無く、学習コストが低く抑えられます。

使い方は以下の記事を参考にすればだいたい分かるかと思います。

- [Django + Elasticsearch コトハジメ](https://gist.github.com/voluntas/21759d5c45aacc0e6656)
- [Haystack + Elasticsearch + kuromoji コトハジメ](https://gist.github.com/voluntas/6739918)

Django の `settings.py` での Elasticsearch の設定を記述を拡張する [elasticstack](https://github.com/bennylope/elasticstack) や
タスクキューの [Celery](http://www.celeryproject.org/) を利用する [celery-haystack](https://github.com/jezdez/celery-haystack) を使えば
Django のモデルが更新された際に Elasticsearch の Index の非同期更新できるようになるなどの機能拡張もできます。

#### Haystack の注意点
Haystack は周辺ライブラリも充実して便利なのですが **Elasticsearch 用のバックエンドが長らく更新されていない** ため導入には以下の点で注意が必要です。

- Aggregation が使えない
   - Facet のみ対応
- Haystack 全体で1つの Index しか生成しない
   - 複数の Index にドキュメントを分けることができない

基本的に Elasticseach 1.0 移行の新機能に対応していないため、かなりの機能が制限されてしまいます。
特にAggregationが使えないのがかなり痛いです。

以上より、DjangoでHaystackを使うためには以下の方針となります

- Elasticsearch の集計機能を使用する場合 Haystack は **使わない**
- ドキュメントを複数の Index に分ける場合 Haystack は **使わない**

単純な全文検索の利用に特化するなら、Haystackでも大丈夫でしょう。

ただ、検索に求められる機能は日々増していく状況で、全文検索だけで機能が済む状況はあまり考えられないですし
Haystackは便利な分、使用するDjangoアプリに密結合となるため、いざHaystackを辞めたいとなった時にかなり負担となります。

また、保存されるドキュメントデータも Haystack独自の特徴のある形式になるので、既に生成してあるIndexデータを引き継いたり、
別の検索バックエンドに使いまわすのは難しいでしょう。

### ElasticUtils を使う
https://github.com/mozilla/elasticutils

Mozilla 製の Python向け Elasticsearch ライブラリです。
ちなみに Mozilla は巨大な Django サイトを運用しています([URL](http://www.slideshare.net/andymckay/anatomy-of-a-large-django-site-7590098))。

実は elasticutil はまだ使ったことがなく、実際の使用感や利便性、困りそうな部分はわかりません(すいません)

なので、今回はドキュメントを読んで受けた印象を書いてみたいと思います。

- Django 専用ではない
   - 他のPythonプロジェクトにも適用可能
   - `ElasticUtils.contrib` モジュールで対応可能([URL](http://elasticutils.readthedocs.org/en/latest/django.html))
      - Django の Model と連携可能
      - Celery との連携可能
- 検索APIがQuerySetに似てる
   - Haystack ほどではない
   - `S()` オブジェクトで検索クエリを発行する
      - Django の QuerySet と同じ立ち位置
   - `Q()` オブジェクトで検索条件を個別に定義して `S()` へ渡せる
      - 条件によって変化する複雑なクエリの組み立てに使える
   - `F()` オブジェクトでフィルタの or 検索ができる
      - Django の `Q` と似た用途
- Index と mapping は個別に定義できる
- 最新の Aggregation に対応しているか怪しい
   - 実際に発行されるQueryDSLやソースの中身を確認していないので断言はできませんが、ドキュメントのURLのリンク先が Facet のままなので Es の集計機能に対応していない可能性があり注意が必要です。
   - http://ElasticUtils.readthedocs.org/en/latest/searching.html#id46

#### 印象
ElasticUtils は Haystack ほどではありませんが、各種APIが程よく抽象化されていて、ガチガチに機能が制限されるHaystackよりも柔軟に使えそうな印象を受けます。
また、APIも Django の API と似ている部分が多く、普段 QuerySet のコードを書いている人には馴染みやすいのではないでしょうか。
ただし、Aggregationに対応していない可能性が高いため、集計機能を利用したい場合には注意が必要そうです。

### elasticsearch-py を使う
http://www.elasticsearch.org/guide/en/elasticsearch/client/python-api/current/

Elasticsearch が提供する公式ライブラリです。

- Python を Elasticsearch の薄いラッパー
- PythonのDictでそのまま QueryDSL を書くため、QueryDSLを覚える必要がある
- 依存が少なく、Esの仕様を知っていればフル機能を呼び出せる

このライブラリは REST API の呼び出しや返り値の受け取り部分を隠してくれる程度なので、普段 curl で叩いている部分をPythonの中で書けるようになるだけ、という印象です。

Elasticsearch の各種 API を覚える必要があるため学習コストは高くなります。また、コードの量も多くなるでしょう。

しかし、以下の利点があります
- Elasticsearch の変更に楽に追従できる
- 新機能を簡単に試せる

Elasticsearchへの導入に時間的余裕があるなら、これが最も柔軟な方法になるでしょう。

## まとめ
Django から Elasticsearch を利用する方針は以下のよう考えました。

- Elasticsearch の全文検索機能のみしか今後つかうことがなく Haystack で十分まかなえる
    - 複雑な検索クエリもHaystackだとそこそこ楽に書ける
    - Elasticsearch 向けのバックエンドを自分で書き変えて拡張する覚悟があるなら非常に優秀なライブラリ
- Haystackほどの抽象化を求めないなら ElasticUtils
    - Haystack より柔軟性はありそう
    - 集計機能を使う予定ならばドキュメントやソースを要確認
- Elasticsearch の機能を積極的に利用し, 導入まで余裕があるなら elasticsearch-py
    - 作業工数は高いが、結局のところ利用方法に最も柔軟性がある

求める機能と導入までの時間的余裕によりますが、Haystack には色々と苦労した思い出があるので、個人的には Haystack は極力使わず ElasticUtils を検討したいところです。

ただし、Elasticsearch の強力な集計機能を使いたい場合は、ElasticUtils でも難しそうな雰囲気があるので、公式モジュールである elasticsearch-py に落ち着くことになりそうです。

## 参考文献
- http://pixabay.com/ja/blog/posts/django-search-with-elasticsearch-47/
   - Django サイトで Elasticsearch を導入する際の検討結果が書かれています。この記事はもっと極端で elasticsearch-py も使わず [requests](http://docs.python-requests.org/en/latest/) を使った JSONベース HTTPリクエストを直接投げるという結論の落ち着いています。
   - 最初に箇条書きで検討結果がまとめられていて参考になります。Django 製のサイトに検索エンジンを導入する際には一読をおすすめします。

- http://blog.kevinastone.com/elasticsearch-bliss-with-elasticutils.html
   - ElasticUtils の使い方

- http://www.stamkracht.com/extending-haystacks-elasticsearch-backend/
   - Haystack のクエリ拡張方法について書かれています