Value driven web development

With the help of acts_as_ferret plugin, it is very easy to add ferret search functionalities to active records. However, it doesn’t come with pagination out of the box. Following the good practise of fat models and skinny controllers, I’ll show you how I structure my search methods with ferret pagination.

First – get the pagination find plugin. Ilya released a very useful script to combine with paginationfind, however it is not using exactly the same syntax as paginationfind and there’s a tiny issue with the results count when active record :conditions are included.

I’ve created my own patched version here, it accepts the same :page => { :size => 10, :current => 1 } hash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def paginating_ferret_search(query,options = {}, find_options = {})

  page_options = find_options.delete(:page) || options.delete(:page) || {}

  current = page_options[:current] && page_options[:current].to_i > 0 ? page_options[:current] : 1
  first = page_options[:first] || 1
  auto = page_options[:auto] || false

  # Total size is either AR find result count or Search result total_hits or from limit param, whichever is less
  limit = options.delete(:limit)
  total_hits = if find_options[:group] || find_options[:conditions] || find_options[:select]
                  ids = []
                  raw_hits = find_id_by_contents(query, {:limit => :all})   {|type, id, score, data_hash| 
                     ids << id 
                  }
                  conditions_array = combine_conditions([ "#{self.table_name}.#{self.primary_key} in (?)", ids ], find_options[:conditions])
                  count :all, find_options.update({:conditions => conditions_array})        
                 #find_by_contents(query, {:limit => :all},find_options).total_hits # <- bad way of doing it     
               else
                 find_by_contents(query, {:lazy => true}).total_hits      
               end
  total_size = limit ? [limit, total_hits].min : total_hits


  # If :size isn't specified, then use the lesser of the total_size
  # and the default page size
  page_size = page_options[:size] || [total_size, DEFAULT_PAGE_SIZE].min


  PagingEnumerator.new(page_size, total_size, auto, current, first) do |page|

    # Set appropriate :offset and :limit options for this page
    offset = {:offset => (page - 1) * page_size  }
    limit = {:limit => (page_size) < total_size ? page_size : total_size}
    # set paging settings on AR results if AR find option exists.
    # otherwise set paging settings on Ferret results.
    if find_options[:group] || find_options[:conditions] || find_options[:select]
      find_options.merge!(offset).merge!(limit)
      options[:limit] = :all
    else
      options.merge!(offset).merge!(limit)
    end

    find_by_contents(query, options, find_options)
  end
end
Next in my product.rb model , I have this class method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Product < ActiveRecord::Base

def self.search(raw_query, *args)
    options = args.extract_options!
    brand_query = options.delete(:brand)

    query = Ferret::Search::BooleanQuery.new() 
    query.add_query Ferret::Search::FuzzyQuery.new("*", raw_query)
    query.add_query Ferret::Search::TermQuery.new(:brand_name, brand_query), :must unless brand_query.blank?
    query.add_query Ferret::Search::TermQuery.new(:hidden, "N"), :must if options.delete(:public) == true

    puts query.to_s
    self.search_by_ferret_query(query.to_s, options)
  end

  def self.search_by_ferret_query(query,options)
    if options[:page] 
        self.paginating_ferret_search(query, options)    
    else
        self.find_by_contents(query, options)
    end    
  end

end

Leave a Reply