ransack
Elasticsearch
bonsai

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


概要


  • Elasticsearchを使って検索機能を強化しました

  • 個人でHerokuで運用しているサイトのransckを使った全文検索をElasticsearchに置き換えました

  • Elasticsearchは herokuのaddon Bonsai Elasticsearchを使いました


環境


  • herokuでrailsアプリを運用している

  • 環境変数を dot-env で管理している。


手順


HerokuにBonsaiを入れる

https://elements.heroku.com/addons/bonsai から Install Bonsai Elasticsearch を押すだけです。

image.png

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

image.png


Railsで検索できるようにする


elasticsearchのgemを入れる

こちら を参照。

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

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

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

Image from Gyazo


  • elasticsearchのgemを入れる

gem 'bonsai-elasticsearch-rails'


ransackと共存できるようにする

既存の検索はransackをつかっています。そこでモデルに生える search methodが bonsai-elasticsearch-rails で生やされる search

とバッティングしないように対処します。

RansackのsearchとElasticSearchのsearchがコンフリクトするのでその対応をみて、

こちらの方法をやりました。


config/initializers/ransack.rb

Ransack::Adapters::ActiveRecord::Base.class_eval('remove_method :search')


これでOK


activerecord-importと共存できるようにする

ransackと同様にactiverecord-importのimportもバッティングします。

ググってactiverecord-importとelasticsearch-railsでメソッドが被る問題 がありましたので、これを参考に config/application.rb を編集すればOKです。

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


検索用のmodelのconcernを書く

こちらのElasticsearchを使ったRailsサンプルアプリケーションの作成のサイトに従ってやりました。一部変更した箇所のみ説明します。

マッピングは 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


以上