如何基于筛选器和列对ActiveAdmin索引进行排序

时间:2015-09-22 16:04:30

标签: ruby-on-rails activeadmin

我有一个带有Movie模型的Rails应用程序。 Movie模型将'name'和'release_date'作为常规属性,以及用于使用elasticsearch搜索电影名称的范围。

class Movie < ActiveRecord::Base

   scope :movie_name_search, -> (term) {
     movie_ids = elasticSearch(term, :name).map(&id)
     Movie.where(id: movie_ids).reorder('').order_by_ids(movie_ids) unless movie_ids.nil?     
   }

end

然后我设置了我的活动管理员来显示此数据

ActiveAdmin.register Promotion do
  filter :movie_name_search, as: :string, label: "Movie Name"

  index do
    actions
    column :name
    column :release date, sortable: :release_date
  end

end

将一个电影名称放入搜索栏可以很好地工作,并且对release_date进行排序可以很好地完成,但我不能同时进行这两项操作。一旦我使用过滤器的电影名称,按日期排序不起作用。它仅在我删除重新排序和新订单时有效。

scope :movie_name_search, -> (term) {
   movie_ids = elasticSearch(term, :name).map(&id)
   Movie.where(id: movie_ids) unless movie_ids.nil?     
}

看起来我在范围内强制执行的排序优先于列的排序,但我不知道为什么。有什么想法吗?

1 个答案:

答案 0 :(得分:1)

当您在Movie.where中呼叫movie_search_name时,您正在重置范围链。您希望将where发送至self(即只删除Movie.部分),以便保留先前条件。

scope :movie_name_search, -> (term) {
  movie_ids = elasticSearch(term, :name).map(&id)
  where(id: movie_ids) unless movie_ids.nil?     
}

编辑:我现在明白了这个问题

就像你说的那样,Elastic Search会按排序顺序返回id个数组,但where不遵守该顺序。它只是在找到数据库时从数据库中提取记录,只要它们的id位于数组中即可。我们之后将记录排序为数组对我们没有好处,因为ActiveRecord不能将其他子句应用于查询。它必须作为查询的一部分完成。

SQL确实提供了一种强制执行任意顺序的方法:ORDER BY CASE,但它没有内置在Rails中。幸运的是,添加起来并不难。

假设您的搜索结果为[2, 1, 3]。在SQL中,我们可以按照以下顺序检索它们:

SELECT * FROM movies
  WHERE id IN (2, 1, 3)
  ORDER BY CASE id
    WHEN 2 THEN 0
    WHEN 1 THEN 1
    WHEN 3 THEN 2
    ELSE 3 END;

为了使它与ActiveRecord兼容,我们可以为Movie添加一个类方法:

应用/模型/ movie.rb

class Movie < ActiveRecord::Base

  # ...

  def self.order_by_ids_array(ids)
    order_clause = "CASE id "
    ids.each_with_index do |id, index|
      order_clause << sanitize_sql_array(["WHEN ? THEN ? ", id, index])
    end
    order_clause << sanitize_sql_array(["ELSE ? END", ids.length])
    order(order_clause)
  end
end

现在,您的ActiveAdmin范围同时使用whereorder_by_ids_array

scope :movie_name_search, -> (term) {
  movie_ids = elasticSearch(term, :name).map(&id)
  where(id: movie_ids).order_by_ids_array(movie_ids) unless movie_ids.nil?     
}

参考:

{{3}}

编辑II:真正的黑客

注意:这需要使用Ransack的最新版本的ActiveAdmin。

我们遇到的问题是过滤器在排序方面不能很好地发挥作用。所以这里 新计划:让我们在索引表中添加另一列来显示搜索 每部电影的排名。此列仅在我们按电影过滤后才会显示 名称,它将是可排序的。这样就没有&#34;默认&#34;排序,但是 你可以按你想要的任何东西排序,包括搜索排名。

诀窍是使用CASE之类的方法将计算列插入到查询中 上面,但在SELECT条款中。我们称之为search_rank,它可以 在任何返回的电影上访问movie.attributes['search_rank']

应用/管理/ movies.rb

ActiveAdmin.register Movie do

  filter :movie_search, as: string, label: 'Movie Name'

  index do

    # only show this column when a search term is present
    if params[:q] && params[:q][:movie_search]

      # we'll alias this column to `search_rank` in our scope so it can be sorted by
      column :search_rank, sortable: 'search_rank' do |movie|
        movie.attributes['search_rank']
      end
    end

  end
end

现在在我们的模型中,我们需要定义movie_search范围(作为类方法) 并将其标记为可以回收。

应用/模型/ movie.rb

class Movie < ActiveRecord::Base

  def self.ransackable_scopes(opts)
    [:movie_search]
  end

  def self.movie_search(term)
    # do search here
    ids = elasticSearch(term, :name).map(&id)

    # build extra column
    rank_col = "(CASE movies.id "
    ids.each_with_index do |id, index|
      rank_col << "WHEN #{ id } THEN #{ index } "
    end
    rank_col << 'ELSE NULL END) AS search_rank'

    select("movies.*, #{ rank_col }").where(id: ids)
  end
end