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まとめ
ドキュメントはちゃんと読みましょう。
