概要
- Elasticsearchを使って検索機能を強化しました
- 個人でHerokuで運用しているサイトのransckを使った全文検索をElasticsearchに置き換えました
- Elasticsearchは herokuのaddon Bonsai Elasticsearchを使いました
環境
- herokuでrailsアプリを運用している
- 環境変数を dot-env で管理している。
手順
HerokuにBonsaiを入れる
https://elements.heroku.com/addons/bonsai から Install Bonsai Elasticsearch を押すだけです。
Bonsaiは件数が少なければ無料で使えます。
Railsで検索できるようにする
elasticsearchのgemを入れる
こちら を参照。
環境変数にさきほど取得したBonsai ElasticsearchのURLを書く(下図参照)。
BONSAI_URL=https://user:password@example-acme-development.us-east.bonsaisearch.net
下図のRead & WriteのURLを上記に入れてください。
- elasticsearchのgemを入れる
gem 'bonsai-elasticsearch-rails'
ransackと共存できるようにする
既存の検索はransackをつかっています。そこでモデルに生える search
methodが bonsai-elasticsearch-rails
で生やされる search
とバッティングしないように対処します。
RansackのsearchとElasticSearchのsearchがコンフリクトするのでその対応をみて、
こちらの方法をやりました。
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もはりました。
# マッピング情報
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
と書きました。
コントローラー
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対応前
def exec
Event.search(search_params).result.order(start_at: :asc)
end
EventSearch クラスElasticsearch対応後
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です。
Event.search(query).records.all
以上