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?

10 comments

Author: Steve Mohapi-Banks Steve Mohapi-Banks

What about recommend_you, or for that authentic rails plugin vibe, recommend_yu

Author: Karl Karl

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[:userid])’.

Looks very interesting and I’ll be sure to add it to my github watch list.

Author: stuart stuart

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.

Author: rick rick

I agree with Steve, ‘acts_as_recommendable :books’ is awkward.

validates_presence_of :login
has_many :user_books
recommends :books, :through => :user_books, …



Author: David Backeus David Backeus

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.

Author: Hanna Hanna

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 :-)




Author: sandrar sandrar

Hi! I was surfing and found your blog post… nice! I love your blog. :) Cheers! Sandra. R.

Author: movielike movielike

Sign: kxdrb Hello!!! zsmbs and 2985wbvdddzsws and 671 My Comments: Nice blog!

Author: black celebs black celebs

Sign: wdpad Hello!!! qazyx and 668kspfsthwts and 2021 : I will try to recommend this post to my friends and family, cuz its really helpful.