Acts_as_solr, UltraSphinx, acts_as_ferret, acts_as_xapian; there definitely isn’t a shortage of full text search options when it comes to Rails projects. However, the number of solutions suitable for a shared hosting setup is somewhat limited. Anything that is server or daemon based is most likely out the window, so basically we are left with acts_as_xapian.

Xapian has been around for a while and has a pretty interesting history behind it. It’s fast, feature rich and apparently scales very well.

This guide is very heavy on the code and command examples and it’s pretty long so grab a fresh cup of coffee and dive in.

1. The Server Side

SSH into your server. I install custom libraries into ~/opt and the tarballs and source files into ~/opt/src. If your server doesn’t have wget installed (unlikely), use curl. The commands below are originally from Kevin Colyar.

  1   # Downloading and untaring the source files
  2   cd ~/opt/src
  3   
  4   wget http://oligarchy.co.uk/xapian/1.0.10/xapian-core-1.0.10.tar.gz
  5   wget http://oligarchy.co.uk/xapian/1.0.10/xapian-bindings-1.0.10.tar.gz
  6   
  7   tar zxvf xapian-core-1.0.10.tar.gz
  8   tar zxvf xapian-bindings-1.0.10.tar.gz
  9   
 10  # Installing the Xapian-Core library
 11  cd xapian-core-1.0.10
 12  ./configure --prefix=$HOME/opt
 13  make
 14  make install
 15  
 16  # This is where our Ruby bindings will live
 17  mkdir ~/opt/ruby_modules
 18  
 19  # Installing the bindings to the above directory
 20  cd ../xapian-bindings-1.0.10
 21  ./configure --prefix=$HOME/opt RUBY_LIB=$HOME/opt/ruby_modules RUBY_LIB_ARCH=$HOME/opt/ruby_modules
 22  make
 23  make install

2. Telling Rails About the Bindings

The bindings are now in ~/opt/ruby_modules, but Rails has no idea that directory exists. To tell Rails about it put this in your environment.rb file.

  1   if RAILS_ENV == 'production'
  2       config.load_paths << "#{ENV['HOME']}/opt/ruby_modules"
  3   end

3. acts_as_xapian Rails Plugin

Next thing is to install the acts_as_xapian plugin and generate the migration for it.

  1   script/plugin install git://github.com/frabcus/acts_as_xapian.git
  2   script/generate acts_as_xapian
  3   rake db:migrate

To test your setup from the command line (SSH’ed into your server), first rebuild the index and then perform a search:

  1   rake xapian:rebuild_index models="ModelName1 ModelName2"
  2   rake xapian:query models="ModelName" query="search string"

4. The Actual Implementation

In whatever models you want to be searchable, add the following. The title and body are just the attributes you want included in the search.

  1   class Article < ActiveRecord::Base
  2       acts_as_xapian :texts => [:title, :body]
  3   end

Now you need a way to get search results in your app. The method below can go in whatever Controller you want. It’s just like any other Rails action.

  1   def search
  2       if params[:search] && params[:search][:words] != ""
  3           @xap_search = ActsAsXapian::Search.new([Article], params[:search][:words].to_s, { :limit => 100 })
  4           @xap_articles = @xap_search.results.collect { |r| r[:model] }
  5       end
  6   end

The above code is checking to make sure the results page wasn’t just visited and a search form (params[:search]) was actually submitted. Also it makes sure the user actually typed something in the field. If they left the search field blank and you try to search for null it will spit out an error. The @xap_search variable is an ActsAsXapian::Search object that gets generated based on the query. [Article] is an array of the models to search in, the second parameter is the search phrase, and the third is a hash of options, in this case limiting it to 100 results. From there we can return each search result as an ActiveRecord model using the last line. Then just loop over @xap_articles and display them in your view like any other Model.

5. Automatic Updates With after_filter

Making your app update the index automatically every time you create, update or delete an Article (or whatever models you are making searchable) is actually super-easy. Just add the following into it’s corresponding Controller:

  1   after_filter :update_xapian_index, :only => [:create, :update, :destroy]
  2   
  3   private
  4   def update_xapian_index
  5       Thread.new do
  6           system("rake xapian:update_index")
  7       end
  8   end

I put it in the Controller instead of the Model because it doesn’t directly pertain to a single Model, it just interacts with them. It pops up a new process, runs the rake task and then dies. Easy enough, right?

Bonus Points: Capistrano

If you’re deploying your app with Capistrano you’ll need to rebuild the index every time you deploy. It gets pretty annoying doing it every time by hand, especially if you’re deploying a lot (like you should be). The solution is pretty straight forward, just add this task to your deploy.rb file and replace the models with the appropriate ones for your app. It will run right before the server gets restarted each time you deploy.

  1   desc "Rebuilds and updates the Xapian search index"
  2   task :before_restart, :role => :app do
  3       run "cd #{current_path};
  4            rake xapian:rebuild_index models='Article' RAILS_ENV=production;
  5            rake xapian:update_index models='Article' RAILS_ENV=production"
  6   end

So there it is, one huge guide to Rails full text search with Xapian on a shared host. Hopefully it helps someone who was in the same position as me. If you’ve done the same thing before and there’s an easier way to do part of it, by all means let me know.