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?

See also:

  • No similar posts

About the author

Stuart is a technologist’s technologist and one of the founding partners at Made By Many. He also is a champion of fluid, Agile business structures and new disruptive business models for a disruptive age. Follow @stueccles on Twitter

Leave a comment

Our latest tweets

Categories

Recent comments

  • James Higgs: At some level Kujau wanted the attention, and the same seems to be true of Manning if he is indeed t...
  • William Owen: Sara, you've asked lots of pertinent questions here but I think you’re really asking quite a lot of ...
  • Sara Williams: James, as much as I want to agree with you -- you are right a very good percentage of the time -- th...
  • James Higgs: There is a certain logic to this: people are unlikely to go through a great deal of effo...
  • Tim Malbon: I think we should remember that we are in Afghanistan because its leaders allowed it to be used as a...