検索処理をAmazon Elasticsearch Serviceに置き換えた話パート１

このエントリは Classi Advent Calendar 2016 の14日目です。

今回はClassiで提供しているテスト検索、問題検索をElasticsearchに置き換えた時の話です。
以下アジェンダです。

- 概要 置き換えた理由
- 課題解決のためにやったこと
    - mappingの検討
    - indexの作成
    - データimportバッチの作成
    - aliasの検討
    - 検索処理実装
    - バックアップ

# 概要 置き換えた理由
classiの検索はmroongaで作られていたのですが、データの増加とともに当初のデータ設計のままではレスポンス劣化によりユーザ体験的に良くない状況になってきていました。実際「遅い」いうクレームもあり、このままデータ増え続けた場合、サービスとして致命的な問題になりかねないと状況でしたので、改善することになりました。

<img width="1108" alt="スクリーンショット 2016-12-13 11.58.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/1686/6d443993-049d-334b-67a3-4eff7e78805f.png">

## データ構造
例えば「テスト検索」から何かテストを検索するとします。その時の登場人物は「テスト」「テストの共有設定」「テストに設定された属性たち」「グループ」「ユーザ」です。
「テスト属性たち」はテストを検索するときの条件に使うので、特にどうということはないですが、「テスト共有設定」が一番重くなる原因になっていました。
classiのテストを含めたドキュメントデータは階層構造で管理する仕組みになっていて、その親の階層で「XXさんがいるグループに共有する」となっていた場合、以下の階層も共有設定が反映される仕組みになっています。検索時にこの共有範囲を確認するのが処理として重いため、まずはそのデータをElasticsearchでどう組み込むかが課題になりました。

## 検索DBへのデータ反映
mroonga側にデータを反映させるバッチが数分おきに1回実行される仕組みになっていました。リアルタイムでないだけでなく、データが多くなってくるに従い、mroongaのindex反映処理が結構時間がかかるようになってきました。これも課題でした。


# 課題解決のためにやったこと
以上の課題を考慮に入れてelasticsearchを使って検索処理の高速化に取り組みました。以下にその時やったことを簡単に紹介していきます。多分長くなりそうなので、途中まで紹介し、残りはパート２で紹介します。

## mappingの検討
### データのマッピング
最初に今あるデータ構成をどうやって、Elasticsearchに置き換えるか考えました。今回の検索対象のデータは３つのテーブルに分かれます。文書のデータ、そして文章の属性データ（難易度とかカテゴリなど)、共有データです。

まずは文書データと属性データについてです。両者の関係は１対多になります。文章に関連付ける属性の名前(難易度とか）と実際の値(難易度Lv高とか)が一レコードになっているので、この属性データの一つ一つをindexのカラムにしました。以下の例のようになイメージです。これにより検索時に属性テーブルの中を見て条件にヒットする文書のIdを抽出し・・・・というような処理もなくなりElasticsearchの該当カラムを見るだけで済むようになりました。

```
# mappingの例
indexes :XX_age_id,         type: 'long'
indexes :XX_category_id,    type: 'long'
indexes :XX_format_id,      type: 'long'      
```

次に共有データについてです。これは上の方にも書きましたが、階層構造になっているため今までは検索処理時に階層をたどるという処理が入りました。データが少なければいいのですが、段々多くなってきたり階層が深くなると検索にも時間がかかってしまいます。今回Elasticsearch側では共有データを（Aさんに見せていいよとか、学校のXXXってグループに見せていいよとか)配列にして持つように変更しました。

```
# mappingの例
indexes :shared_read, type: 'string', index: 'not_analyzed'

# 実際に入っている例
shared_read: [ 'user_11111', 'hoge_123' ]
```

elasticsearchはデータをドキュメント思考DBなので登録データをjson形式にします。比較的柔軟な形式でデータを保持することができます。今回はこの柔軟性を活かした事もあり大分検索処理スピートを上げることが出来ました。

### プラグイン設定
elasticsearchはプラグインを追加することで検索機能をカスタマイズしたりもできます。今回使用した主なプラグインを紹介します。

- kuromoji-analyzer
日本語検索で使います。定番ですね。Elasticsearchの２系からはDefaultのpluginになっています。Amazon Elasticsearch Serviceでも入っています。

- icu_normalizer
大文字・小文字や全角・半角の区別なく検索させる場合に使います。

- HEAD
ローカルで開発するときに、データElasticsearchにどのように入っているのか確認したい時に便利です。

<img width="1226" alt="HeadPlugin.png" src="https://qiita-image-store.s3.amazonaws.com/0/1686/866e1654-2dd0-2b0f-a843-baf5c072f070.png">

### ソート
Elasticsearchで日本語のソートがうまくいかず問題になりました。投稿日付などの並び替えは問題ないのですが、タイトルで並び替えるという時に並びがバラバラになってダメでした。やり方を検討した結果以下のようにmappingに書くことで解決しました。

```before
indexes :name, type: 'string', analyzer: 'kuromoji_analyzer'
```
を

```after
indexes :name, type: 'string', analyzer: 'kuromoji_analyzer', fields: { raw: { type: 'string', index: :not_analyzed } }
```
のように<code>fields</code>の指定を追加しました。

※追記: 2016/12/15

検索query時に以下のよう書きます。

```
{
"query" :{
    "sort":[
      {"name.raw":"desc"}
    ]
  }
}
```

長くなりそうなのでパート１はここまでにします。続きは明日15日に！

# 最後に
[Classiではエンジニアを募集しています。](https://www.wantedly.com/companies/classi/projects)
興味のある方は是非お越しください。