Faster Rails auto_complete with memcached

Posted on December 17, 2007

The original "roll your own" autocomplete action:

1
2
3
4
5
6
7
8
9
10
11


  def auto_complete_for_email_destination
    value = params[:email][:destination]
    @contacts = @user.contacts.find(:all, 
      :conditions => [ 'LOWER(email) LIKE ? OR LOWER(name) LIKE ?', '%' + value.downcase + '%', '%' + value.downcase + '%'], 
      :order => 'contacts.email ASC',
      :limit => 5)
    render :partial => 'contacts/destination_autocomplete'
  end

now, we change it to:

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


  def auto_complete_for_email_destination
    value = params[:email][:to_all]
    @contacts = @user.find_user_contacts(value,{:limit => 5})
    render :partial => 'contacts/destination_autocomplete'
  end

# User model

require 'digest/sha1'

class User < ActiveRecord::Base
  
  acts_as_cached
  after_save :reset_cache
  
  # find contacts with matching pattern 
  # the key to the cache is the user.id 
  def find_user_contacts(query,options = {})
    user_id = self.id
    limit = (options[:limit] ||= 5).to_i
    contacts = User.get_cache(self.id).cached(:contacts)
    # iterate through the contacts and use regexp to match it
    reg = Regexp.new(query)
    # find_all works with Enumerator Class.
    results = contacts.to_enum.find_all {|t| (t.email =~ reg) || (t.name =~ reg)}
    results[0..(limit-1)]
  end
end


# Contact mode, sweep the user cache if saved.
class Contact < ActiveRecord::Base  
  belongs_to :user

  acts_as_cached
  after_save :reset_cache
  after_save :sweep_cache_from_user


  def sweep_cache_from_user
     User.reset_cache(self.user_id)
  end

if you use shared host, you could as well use Ferret instead of memcached.

That's it folks...

Speed improvement:

before: ~ 20-40 req/sec

now: ~ 350-400 req/sec

on Amazon EC2 small AMI

Note: Experimental code... test it well before using in production...