RailsとElasticsearchとkaminariの組み合わせで気をつけること
RailsでElasticsearchと言えばelasticsearch-rails gemの出番ですが、kaminariと組み合わせる時に気をつけた方が良いことをまとめてみました。小ネタです。
この記事はRuby on Rails Advent Calendar 2016の17日目です。
RailsでElasticsearchといえばelasticsearch-railsですね。
今回はelasticsearch-railsとkaminariを合わせて使う時に押さえておいた方が良いことをまとめてみました。
ただ、基本的にはドキュメントにも書いてあることなのでちゃんとドキュメントを読んでおいた方が良いです。
検索結果をArelオブジェクトとして扱うと順番が変わる
まず前準備として、これはドキュメントにも書いてあることなのですが
records = User.search(sort: {id: :desc}).records
records.class # => Elasticsearch::Model::Response::Records
records.ids # => ["3", "2", "1"]
という結果を取得した時に
arel = records.includes(:user_category)
arel.class # => Tag::ActiveRecord_Relation
arel.map(&:id) # => [1, 2, 3]
と、Arelオブジェクトとして評価してしまうと順番が維持されません。
ドキュメントにはto_a
を使うようにと書いてあるのでそうしましょう。
User.search(sort: {id: :desc}).records.includes(:user_category).to_a.map(&:id) # => [3, 2, 1]
elasticsearch-railsとkaminariと合わせて使う
こちらもドキュメントに書いてあるのですが、elasticsearch-railsはkaminariやwill_paginateと合わせて使うことができます。
例えばこんな感じです
records = User.search(sort: {id: :desc}).page(1).per(10).records
records.current_page # => 1
records.limit_value # => 10
records.total_count # => 3
ここまでは特に問題ありません。
検索結果をArelオブジェクトとして扱う&kaminariと合わせて使う
検索結果をArelオブジェクトとして扱いたくて、さらにページングもやりたいという時は問題が起きます。
普通に書くと
records = User.search(sort: {id: :desc}).page(1).per(10).records.includes(:user_category).to_a
となりますが、to_a
で返ってくるのはArray
オブジェクトなので、ページング情報が失われてしまいます。
この場合は少し面倒ですが、こんな感じにする必要があります。
records = User.search(sort: {id: :desc}).page(1).per(10).records
records.class # => Elasticsearch::Model::Response::Records
kaminari_options = {
limit: records.limit_value,
offset: records.offset_value,
total_count: records.total_count
}
arel = records.includes(:user_category)
paginatable_array = Kaminari.paginate_array(arel.to_a, kaminari_options)
paginatable_array.class # => Kaminari::PaginatableArray
paginatable_array.map(&:id) # => [3, 2, 1]
ただ、これをそのままメソッド化してしまうとArel
オブジェクトのメソッドチェーンを自由にできないので、こんな感じにしておくと良いと思います。
class BaseSearcher
def initialize(params)
@params = params
end
def search(options = {}, &block)
page = options[:page] || 1
per = options[:per] || 10
model.search(@params).page(page).per(per).records
kaminari_options = {
limit: records.limit_value,
offset: records.offset_value,
total_count: records.total_count
}
records = yield(records) if block_given?
Kaminari.paginate_array(arel.to_a, kaminari_options)
end
end
module Users
class Searcher < BaseSearcher
def model
User
end
end
end
使い方はこんな感じですね
records = Users::Searcher.new(sort: {id: :desc}).search(page: 1, per: 10) { |scope| scope.includes(:user_category) }
records.map(&:id) # => [3, 2, 1]
records.current_page # => 1
records.limit_value # => 10
records.total_count # => 3
まとめ
ドキュメントはちゃんと読みましょう。