Rails ApplicationからAmazon Elasticsearch Serviceを使って素早く全文検索を可能にする

# 背景
プロダクトに全文検索機能が必要となりElasticSearchを使って開発することに、かといってElasticSearchの実装・運用経験がない私。
そこで最近Amazon Elasticsearch Serviceがリリースされたので導入してみました。

# Amazon Elasticsearch Service
[Amazon Elasticsearch Service](https://aws.amazon.com/jp/elasticsearch-service/)

```
Amazon Elasticsearch Service は、AWS クラウドで Elasticsearch を簡単にデプロイ、
操作、スケーリングできるようにするマネージドサービスです。
```
（https://aws.amazon.com/jp/elasticsearch-service より引用）
スタートアップで働く私にとってできる限りサービス開発に時間を割きたく、インフラストラクチャへの注意を可能な限り少なくしたいです。
その点においてAmazon Elasticsearch Serviceの導入は悪くないと考えました。

# Amazon Elasticsearch Serviceセットアップ
## ドメイン
ドメインを指定します。
注意として例えば```coubic```というドメインを指定すると下記のように```search```がprefixされたendpointになります。
``search-coubic-xxxxxxxxxxxxxxxxx.aws-region.es.amazonaws.com``
##クラスター
ElasticSearchは、1台だけで検索サーバーとして動作します。また、大量のデータを処理したり耐障害性を実現するために複数のサーバー間で協調して動作することもできます。
ElasticSearchではこれらのサーバー群のことをクラスタ、群をなす1台１台のサーバーのことをノードと呼びます。
サービス要求に応じてクラスター等の設定をします。
料金イメージについては (http://calculator.s3.amazonaws.com/index.html?lng=ja_JP) よりシミュレーションすることが可能です。
※必ずしも正確な料金であることを私は保証できません。
##アクセスポリシー
サービス構成の都合に合わせてアクセスポリシーを設定します。
# Application
## 目的
商品をタイトル、詳細から全文検索できるようにします。
## Gem
RailsからAmazon Elasticsearch Serviceを簡単に使えるGemをインストールします。

```rb:Gemfile
gem 'elasticsearch-model'
gem 'elasticsearch-dsl'
```

elasticsearch-modelはActiveRecordからElasticSearchを簡単に使える機能を提供
elasticsearch-dslはrubyからElasticSearchの検索クエリを簡単に書けるDSLを提供

## 接続設定
Amazon Elasticsearch Serviceへの接続を指定します。

```rb:/config/initializers/elasticsearch.rb
Elasticsearch::Model.client =
    Elasticsearch::Client.new hosts: [
                                  {host: 'your-aws-es-endpoint',
                                   port: '80'}]
```

私はここでport指定をせずに少しハマりました。
## インデックスから検索まで流れ
アプリケーションへ検索を実装する前に、ElasticSearchがどのように動作するか初めに大体の流れを理解します。

+ インデキシング
    + ElasticSearchに送られてきたデータを処理し、インデックスに保存する。
+ アナライズ
    + インデキシングの時にデータの内容を解析し分割しインデックスに書き込む。
+ 検索
    + クエリの条件を満たすドキュメントを探す。
+ アナライザ
    + 検索時にクエリの操作を指定する。

インデックス作成から検索まで順に見ていきます。

## インデックス名決定
インデックスはElasticSearchがデータを保存する場所です。RDBMSへ例えるとテーブルに相当するものと言えるでしょうか。
今回はモデル単位でインデックスを作成する想定です。

```rb:searchable.rb
module Searchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model
    index_name  [model_name.collection, Rails.env].join('_')
  end
end
```

##スキーママッピング
スキーママッピングはインデックス構造を定義するために使用します。
インデックスは複数のタイプを含めることができますが、ここでは1つのタイプを定義しています。
次の属性を持つタイプであるCourseをスキーママッピングします。
- name :string
- description :text

```rb:course_searchable.rb
module CourseSearchable
  extend ActiveSupport::Concern

  included do
    include Searchable

    settings index: {
                 analysis: {
                      tokenizer: {
                          kuromoji_tokenizer: {
                            type: 'kuromoji_tokenizer',
                            mode: 'search'
                          }
                      },
                      filter: {
                         pos_filter: {
                             type:     'kuromoji_part_of_speech',
                             stoptags: %w(助詞-格助詞-一般 助詞-終助詞),
                         },
                         greek_lowercase_filter: {
                             type:     'lowercase',
                             language: 'greek',
                         },
                      },
                      char_filter: {
                          custom_mapping: {
                              type: 'mapping',
                              mappings: ['ｶ => カ', 'ｶﾞ=>ガ']
                          }
                      },
                      analyzer: {
                          kuromoji_analyzer: {
                             type:      'custom',
                             tokenizer: 'kuromoji_tokenizer',
                             filter: %w(kuromoji_baseform pos_filter greek_lowercase_filter cjk_width),
                             char_filter: %w(custom_mapping)
                         }
                      }
                 }
             }

    mappings dynamic: 'false' do
      indexes :name, analyzer: 'kuromoji_analyzer', index: 'analyzed'
      indexes :description, analyzer: 'kuromoji_analyzer', index: 'analyzed'
    end

    def as_indexed_json(options = {})
      attributes
          .symbolize_keys
          .slice(:name, :description)
    end
  end
end
```

実際にはCourseモデルにCourseSearchableモジュールをincludeしています。

```rb:course.rb
class Course < ActiveRecord::Base
  include CourseSearchable
end
```

## インデックスの作成
rails consoleからAmazon Elasticsearch Serviceへインデックスを作成します。

```irb
Course.__elasticsearch__.create_index! force: true
```

Amazon Elasticsearch Serviceのダッシュボードからインデックスが作成されたことを確認できると思います。

## データの投入
rails consoleからAmazon Elasticsearch Serviceへ前商品データを投入します。

```irb
Course.__elasticsearch__.import
```

## 検索
入力されたキーワードから全文検索を行うクエリを組み立て、検索を実行します。

```rb:course_searchable.rb
module CourseSearchable
  extend ActiveSupport::Concern
    .....省略  
    
    def self.search(query, size)
      search_definition = Elasticsearch::DSL::Search.search {
        size size         
        query do
          multi_match do
            query keyword
            fields %w(name description)
          end
        end
      }
      __elasticsearch__.search(search_definition)
    end
  end
end
```

実際の検索ではElasticSearchの検索結果からRDBMSへ問い合わせを行い、検索結果に肉付けをしています。

```rb:courses_controller.rb
courses = Course.search(keyword, SEARCH_LIMIT)
results =  Course.where course_id: courses.records.to_a.map(&:id)
```

# ドキュメント更新
Courseモデルが作成・更新された時にElasticSearchへドキュメントの更新を伝えます。
UXを考慮して、ActiveJobを使って非同期で処理しています。

```rb:indexer_job.rb
class IndexerJob < ActiveJob::Base
  include Elasticsearch::Model

  queue_as :default

  def perform(course)
    course.__elasticsearch__.update_document
  end
end
```

# 終わり
シンプルですが全文検索ができました。
弊社サービスよりデモが確認できます。
https://popcorn.coubic.com/search?category=13&location=1&q=%E8%82%A9%E3%81%93%E3%82%8A

# 参考書籍
[高速スケーラブル検索エンジン　ElasticSearch Server](http://www.amazon.co.jp/%E9%AB%98%E9%80%9F%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%A9%E3%83%96%E3%83%AB%E6%A4%9C%E7%B4%A2%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%B3%E3%80%80ElasticSearch-Server-%E3%82%A2%E3%82%B9%E3%82%AD%E3%83%BC%E6%9B%B8%E7%B1%8D-%EF%BC%B2%EF%BD%81%EF%BD%86%EF%BD%81%EF%BD%8C%E3%80%80%EF%BC%AB%EF%BD%95%EF%BD%83%E3%80%80%EF%BC%88%EF%BD%8C%E3%81%AB%E3%82%B9%E3%83%88%E3%83%AD%E3%83%BC%E3%82%AF%E7%AC%A6%E5%8F%B7%E3%80%81%EF%BD%83%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%B5%E3%83%B3%E3%83%BB%E3%83%86%E3%82%AE%E3%83%A5%E4%BB%98%E3%81%8F%EF%BC%89-ebook/dp/B00J4KDYZU/ref=sr_1_1?ie=UTF8&qid=1449137245&sr=8-1&keywords=elasticsearch+server)