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 and the tarballs and source files into . If your server doesn’t have 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 , but Rails has no idea that directory exists. To tell Rails about it put this in your 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 () 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 it will spit out an error. The variable is an ActsAsXapian::Search object that gets generated based on the query. 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 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 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.