Lately I have been playing around with the acts_as_ferret plugin which makes it dead simple to implement full text search in your models for Ruby on Rails. The plugin is designed for the Ferret gem (a full featured search engine based on Apache Lucene). In this article I will guide you through the steps needed to implement searching for your models and talk about some of the problems I faced on the way.

Related: Ajax search in Rails

Installation

Firstly install the Ferret gem:

sudo gem install ferret

Install the acts_as_ferret plugin:

ruby script/plugin install svn://projects.jkraemer.net/acts_as_ferret/tags/stable/acts_as_ferret

Usage

Setup a model to be indexed and searchable by acts_as_ferret. In this example im putting more importance in the results of my search based on the title by using boost:

class Post < ActiveRecord::Base
  # Ferret fields
  acts_as_ferret :fields =>{:title, :body }
end

You may now search through your model using the find_with_ferret method inside your controller.

@posts = Post.find_with_ferret("test")

The @posts object is actually an ActsAsFerret::SearchResults object rather than an array of ActiveRecords. This gives us access to a few extra attributes to give us the total results, the ferret score and support will_paginate.

Total number of results: <%= posts.total_hits %>
 
for post in @posts
  <%= post.title %> with a score of <%= post.ferret_score%>
end

Conditions

The above query can be customised further to include conditions for your searches. This is great for implementing an advanced search or by simply filtering your search to certain parameters. Lets say we want to run a new search on our Post model but only include posts with the category_id = 4.

In our controller:

 category_id = 4
  @posts = Post.find_with_ferret("test", :conditions => ['category_id = ?', category_id])

The view will remain the same as above, only showing different results from the @posts array.

Paginate

It is very important to be able to paginate for your search results and acts_as_ferret couldn’t have made it easier with making it compatible with will_paginate gem. For the latest version of will_paginate, mislav recommends you install via the mislav-will_paginate gem.

Install the mislav-will_paginate gem:

#add GitHub to local gem sources
sudo gem sources -a http://gems.github.com/
#install the gem
sudo gem install mislav-will_paginate

In config/environment.rb add the following line at the end:

require "will_paginate"

In the controller:

per_page = 10
@posts= Post.find_with_ferret(query, :page => params[:page], :per_page => per_page)

In the view:

<h2>Search Results</h2>
<%= @posts.total_hits %> results
 
<% for post in @posts %>
<h3><%= link_to(post.title, post_url(post)) %></h3>
<%= post.body %>
<% end %>
 
<%= will_paginate @posts%>

When you add conditions to your paginated results the pagination parameters must be group together as it is used as the second parameter for the find_with_ferret method. eg.

per_page = 10
@posts= Post.find_with_ferret(query, {:page => params[:page], :per_page => per_page}, :conditions => ['category_id = ?', category_id])

Database Field Storage

For fields in your models that have small data you may wish to store the data with the index so that you get better performance on your searches. You get better performance because the search does not require any queries to be sent to the database to retrieve the data.

Improving on the Post model:

class Post < ActiveRecord::Base
  # Ferret fields
  acts_as_ferret :fields =>{:title => {:store => true}, :body }
end

In our controller specify a “lazy load” from the ferret index:

per_page = 10
@posts = Post.find_with_ferret(query, :lazy => [:title], :page => params[:page], :per_page => per_page)

Boost

Using a boost on fields will apply more importance on them. The higher the boost factor, the more relevant the term will be. The default boost value is 1.

Improve the Post model to place more importance on the title of the post rather than the body:

class Post < ActiveRecord::Base
  # Ferret fields
  acts_as_ferret :fields =>{:title => {:store => true, :boost => 4}, :body }
end

Please note that if the query is an exact match for the body field then it will still be ranked above the title. As far as I know theres no way to separate your results so that title matches will always appear at the top of your search results.

Highlighting

If you wish to have words from your query appear bold in the search results, highlighting is what you need. Highlighting requires that the field you want highlighting on is stored in the index.

So using the previous example of the Post model, in our view we can highlight terms:

Search Results

<%= @posts.total_hits %> results
 
<% for post in @posts %>
	<%= link_to(post.highlight(@query, :field => :title, :num_excerpts => 1, :pre_tag => "<strong>", :post_tag => "</strong>"), post_url(post)) %>
	<%= post.body %>
<% end %>
 
<%= will_paginate @posts%>

Re-indexing

When you make changes to your models, you will want to re-index your data so that searches are accurate. To do this you can simply stop your server, delete the index directory inside your rails application and start the server again. Once you navigate to a page which uses the model, ferret will automatically re-index your models.

Troubleshooting

DRBConn Error

In case your getting any error like this:

This error is caused by the in-built DRB server (with acts_as_ferret) not running. If your not getting this error and your DRB server isn’t running then you are most likely running your application in the development environment.