Ransackの検索をElasticsearchに置き換える

# 概要
* Elasticsearchを使って検索機能を強化しました
* 個人でHerokuで運用している[サイト](https://www.dokode.work)のransckを使った全文検索をElasticsearchに置き換えました
* Elasticsearchは herokuのaddon [Bonsai Elasticsearch](https://elements.heroku.com/addons/bonsai)を使いました

# 環境
* herokuでrailsアプリを運用している
* 環境変数を [dot-env](https://github.com/bkeepers/dotenv) で管理している。

# 手順
## HerokuにBonsaiを入れる
https://elements.heroku.com/addons/bonsai から __Install Bonsai Elasticsearch__ を押すだけです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/124445/f95389dc-7b61-b9d4-18f3-1be14aa00b08.png)

Bonsaiは件数が少なければ無料で使えます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/124445/ae568a91-aadc-414c-c03e-f3d0bb9368b2.png)


## Railsで検索できるようにする
### elasticsearchのgemを入れる
[こちら](https://docs.bonsai.io/docs/ruby-on-rails) を参照。

環境変数にさきほど取得したBonsai ElasticsearchのURLを書く（下図参照）。

```.env
BONSAI_URL=https://user:password@example-acme-development.us-east.bonsaisearch.net
```

下図のRead & WriteのURLを上記に入れてください。

[![Image from Gyazo](https://i.gyazo.com/c55ad2fa9366bd30e32d10dd1eeed58e.png)](https://gyazo.com/c55ad2fa9366bd30e32d10dd1eeed58e)

* elasticsearchのgemを入れる

```Gemfile
gem 'bonsai-elasticsearch-rails'
```

### ransackと共存できるようにする
既存の検索は[ransack](https://github.com/activerecord-hackery/ransack)をつかっています。そこでモデルに生える `search` methodが `bonsai-elasticsearch-rails` で生やされる `search
` とバッティングしないように対処します。

[RansackのsearchとElasticSearchのsearchがコンフリクトするのでその対応](https://qiita.com/h5y1m141@github/items/1aa86aa1e59454a211a1#ransack%E3%81%AEsearch%E3%81%A8elasticsearch%E3%81%AEsearch%E3%81%8C%E3%82%B3%E3%83%B3%E3%83%95%E3%83%AA%E3%82%AF%E3%83%88%E3%81%99%E3%82%8B%E3%81%AE%E3%81%A7%E3%81%9D%E3%81%AE%E5%AF%BE%E5%BF%9C)をみて、
[こちら](https://github.com/activerecord-hackery/ransack#ransack-search-method)の方法をやりました。

```config/initializers/ransack.rb
Ransack::Adapters::ActiveRecord::Base.class_eval('remove_method :search')
```

これでOK

### activerecord-importと共存できるようにする
ransackと同様に[activerecord-import](https://github.com/zdennis/activerecord-import)のimportもバッティングします。

ググって[activerecord-importとelasticsearch-railsでメソッドが被る問題](https://blog.dakatsuka.jp/2015/11/18/activerecord-import.html) がありましたので、これを参考に `config/application.rb` を編集すればOKです。

なお、これにより、activerecord-importの __import__ メソッドは __bulk_insert__ として呼び出すことになるので、注意してください。

### 検索用のmodelのconcernを書く
こちらの[Elasticsearchを使ったRailsサンプルアプリケーションの作成](https://morizyun.github.io/blog/elasticsearch-rails-tutorial/index.html)のサイトに従ってやりました。一部変更した箇所のみ説明します。

マッピングは `string` ではなく `text` に変更しました。https://dev.classmethod.jp/server-side/elasticsearch/released-elastic-stack-5-2-0/ や https://www.elastic.co/blog/strings-are-dead-long-live-strings などをみるとバージョン 5 以降で変更になったみたいです。

また、後述のクエリのために dateのindexもはりました。

```app/models/concerns/event_searchable.rb
    # マッピング情報
    settings do
      mappings dynamic: 'false' do # 動的にマッピングを生成しない
        indexes :name, analyzer: 'kuromoji', type: 'text'
        indexes :content, analyzer: 'kuromoji', type: 'text'
        indexes :start_at, type: 'date'
        indexes :end_at, type: 'date'
      end
    end

```

### 検索用のクエリ書く

検索は `app/forms/event_search.rb` に書いていますので、それをelasticsearchを使った形に修正します。

search_paramsには`content_cont_all` というransackに対応するようにkeyが割り振られていて、それを直接つかって検索していました。
elastcisearchではransackのdslは使わないので、そこに入っている検索キーワード（スペース区切り）を取り出して `search_params[:content_cont_all]` 渡しています。

ransackの `content_cont_all` に該当するような全てにマッチするクエリを作るため boolとmustをつかいました。
mustに配列を渡すと、配列内の条件を __AND__ でつなげて検索します。

日付の指定にはrangeを使います。 元の検索式は、ransack の `start_at_gteq: @start_date`を使ってます。これをelasticsearchのクエリで書くと `gte: 'now' となります（gteは __greater than equal__ の略）。

レコードのソートは sortを使います。元の検索式では、  `order(start_at: :asc)` と書いています。これを elasticsearchのクエリで書くと `sort: { start_at: { order: 'asc' } }` となります。

件数の指定は、size を使います。 今回は最大50件にしたかったので `size: 50` と書きました。

コントローラー

```app/controllers/home_controller.rb
class HomeController < ApplicationController
  include Pagy::Backend
  def search
    @event_search = if params[:event_search].present?
                      EventSearch.new(event_search_params)
                    else
                      EventSearch.new
                    end
    @pagy, @events = pagy(@event_search.exec)
  end

  private

  def event_search_params
    params.require(:event_search).permit(:keyword)
  end
end

```

EventSearch クラスElasticsearch対応前

```app/forms/event_search.rb
  def exec
    Event.search(search_params).result.order(start_at: :asc)
  end

```

EventSearch クラスElasticsearch対応後

```app/forms/event_search.rb
  def exec
    query = { query: {
      bool: {
        must: [range: {
          end_at: {
            gte: 'now'
          }
        }] + search_params[:content_cont_all].map { |key| { match: { content: key } } }
      }
    },
              sort: { start_at: { order: 'asc' } },
              size: 50 }
    Event.search(query).records.all
  end
```

### pagyのpaginationに対応させる

ページネーションはpagyでやっています。こちらに対応させます・・・が特にやることないです。
pagyは `@pagy, @events = pagy(@event_search.exec)` のように使います。pagyの引数はActiveRecord_Relationです。
elasticsearchの検索結果にたいして  `all` メソッドを実行すればをActiveRecord_Relationが返りますのでそれだけでOKです。

```app/forms/event_search.rb
Event.search(query).records.all
```


以上