Tutorial: Easy Rails recommendations with acts_as_recommendable
Following up on Alex MacCaw’s post on collaborative filtering. The plugin we recently released acts_as_recommendable allows Rails developers to quickly add some user-driven recommendations of items to their latest great millionaire-making startup. At Made By Many we’ve been developing some great niche social-media Ruby On Rails sites recently with New Bamboo and Headshift. The new edge of social media is in the maths, commenting and rating is so old-school, it’s what you do with that data that counts.
This is going to be a tutorial for simple integration of acts_as_recommendable to recommend your users some books.
Lets create the Rails application and install a few essential plugins..
rails bookstore
cd bookstore
./script/plugin install http://ennerchi.googlecode.com/svn/trunk/plugins/jrails
./script/plugin install git://github.com/technoweenie/restful-authentication.git
./script/plugin install git://github.com/maccman/acts_as_recommendable.git
We are going to use a bit of jquery later, hence the use of jrails, and we are going to need to have users hence restful-authentication.
We need to enable restful-authentication and set up our books scaffold
./script/generate authenticated user sessions
./script/generate scaffold book title:string
./script/generate model user_book book_id:integer user_id:integer
rake db:migrate
Now our simple bookstore will use the UserBook model as the join between users and books which act_as_recommendable will use to find book recommendations for our users based on what they and others have bought.
class Book < ActiveRecord::Base
has_many :user_books
has_many :users, :through => :user_books
def bought_by_user?(user)
rtn = false
rtn = user.books.include?(self) if user
end
end
class UserBook < ActiveRecord::Base
belongs_to :book
belongs_to :user
end
class User < ActiveRecord::Base
has_many :user_books
has_many :books, :through => :user_books
acts_as_recommendable :books, :through => :user_books
def buy_book(book)
books << book
self.save
end
Thats it. Well we could do with a bit of an interface. Lets add an AJAX buy to the controller and a way to display the recommendations.
class BooksController < ApplicationController
def buy
@book = Book.find(params[:id])
self.current_user.buy_book(@book) unless self.current_user.nil?
respond_to do |format|
format.js {render :partial => “bought”, :locals => {:book =>@book} }
format.xml { head :ok }
end
end
def recommended
unless self.current_user.nil?
@books = self.current_user.recommended_books
respond_to do |format|
format.html # recommended.html.erb
format.xml { render :xml => @books }
end
end
end
The we can just add our bought partial _bought.html.erb
<div id="book_<%=book.id%>"><em>you bought this</em></div>
and _buyit.html.erb
<div id="book_<%=book.id%>"><em><%=link_to_remote 'Buy This', :update => "book_#{book.id}", :url => buy_book_path(book)%></em></div>
And our recommended view
<h1>Recommended Books For You</h1>
<table border="0">
<tbody>
<tr>
<th>Title</th>
</tr>
<% for book in @books %>
<tr>
<td><%=h book.title %></td>
</tr>
<% end %>
</tbody></table>
<%= link_to 'View all books', books_path %>
Change index.html.erb
<h1>Listing books</h1>
<%=link_to('recommended for you', recommended_books_path()) unless self.current_user.nil?%>
<table border="0">
<tbody>
<tr>
<th>Title</th>
</tr>
<% for book in @books %>
<tr>
<td><%=h book.title %></td>
<td><%if book.bought_by_user?(self.current_user)%>
<%= render :partial => "bought", :locals => {:book => book} %>
<%else%>
<%= render :partial => "buyit", :locals => {:book => book} %>
<%end%></td>
</tr>
<% end %>
</tbody></table>
Change the routes
map.resources :books, :member => {:buy => :post}, :collection => {:recommended => :get}
And we are good to go. Start the server and go to http://localhost:3000/signup and create 3 or 4 users. Now create 15 or so some books and have those users buy a few. After you have a small dataset of conflicting purchases you will be able to go to recommendations page and get your users some books.
Of course there is more. What if you wanted to recommend items based on a rating the user has given rather than just a direct link. Well by adding a rating attribute to the join table acts_as_recommendable can do that.
acts_as_recommendable :books, :through => :user_books, :score => :rating
Using this method you can make the join table precalculate the quantity of the relationship between the user and the item based on many factors, such as the rating, buying and wishlists.
But what about performance? Well that’s the big problem. At the moment this acts_as_recommendable setup is doing user-based recommendations which require loading all the data before running it through the algorithm. As the dataset increases this will slow down hugely. So acts_as_recommendable lets you move to item-based recommendations which uses a cached similarity matrix between items, then at runtime applies a user’s preferences to it.
acts_as_recommendable :books, :through => :user_books, :score => :rating, :use_dataset => true
This dataset is stored in the RailsCache and can you can then use a batch rake task to update the similarity matrix offline on a regular basis.
acts_as_recommendable is still in alpha but we are hoping we can use it in a few gigs and see how it works for a production site. As a postscript, Laurie at New Bamboo says ActsAs is old school, so we are thinking about renaming it recommend_me. What do you think?

Steve Mohapi-Banks 13 Aug 2008 4:42 pm
What about recommend_you, or for that authentic rails plugin vibe, recommend_yu
Karl 13 Aug 2008 4:42 pm
Honest, I’m not trying to be critical… but you have a few errors with your tutorial code (i.e. _buyit.html.erb, where is the link_to_remote call?) and other small issues, like ’self.current_user’ is not a controller method. You need to find the user with ‘@user = User.find(session[:user_id])’.
Looks very interesting and I’ll be sure to add it to my github watch list.
stuart 13 Aug 2008 4:42 pm
No harm in the crtique.
Wordpress had eaten the link_to_remote (now restored) but self.current_user is restful_authentication thing to reference the current logged in user. You need to move include AuthenticatedSystem to the ApplicationController.
rick 13 Aug 2008 4:42 pm
I agree with Steve, ‘acts_as_recommendable :books’ is awkward.
validates_presence_of :login
has_many :user_books
recommends :books, :through => :user_books, …
David Backeus 13 Aug 2008 4:42 pm
I agree that the api should use “recommends” however the plugin could still be named acts as… since it ensures an original name that works and sort of follows an activerecord convention people are used to seeing.
Hanna 13 Aug 2008 4:42 pm
Thanks for the great work, first! It´s all working fine so far.
But when I try to recommend items using a cached dataset, no recommendations are shown. I used the rake task recommendations:build first, there´s no mistake. But I think the dataset is somehow not created.
Can someone give me a hint? Am I missing something?
(I´m relatively new in RoR…) Thanks in advance
Cassio 13 Aug 2008 4:42 pm
sounds like a great plugin, but I couldn’t checkout it (./script/plugin install git://github.com/maccman/acts_as_recommendable.git).