Recommendable is a Rails Engine to add Like/Dislike functionality to your application. It uses Redis to generate recommendations quickly through a modified Jaccardian collaborative filtering algorithm. Your users' tastes are compared with one another and used to give them great recommendations! Yes, Redis is required. Scroll to the end of the README for more info on that.
Why Likes and Dislikes?
tl;dr: Binary voting habits are most certainly not an odd phenomenon. People tend to vote in only two different ways. Some folks give either 1 star or 5 stars. Some people fluctuate between 3 and 4 stars. There are always outliers, but what it comes down to is this: a person's binary votes indicate, in general, a dislike or like of what they're voting on. I'm just giving the people what they want.
Add the following to your Rails application's
gem "recommendable", :git => "git://github.com/davidcelis/recommendable"
bundle install, you can then run:
$ rails g recommendable:install
After running the installation generator, you should double check
config/initializers/recommendable.rb for options on configuring your Redis
Finally, Recommendable uses Resque to place users in a queue. Users must wait their turn to regenerate recommendations so that your application does not get throttled. Don't worry, though! Most of the time, your users will barely have to wait. In fact, you can run multiple resque workers if you wish.
Assuming you have
$ QUEUE=recommendable rake environment resque:work
You can run this command multiple times if you wish to start more than one worker. This is the standard rake task for starting a Resque worker so, for more options on this task, head over to defunkt/resque
In your Rails model that represents your application's user:
class User < ActiveRecord::Base recommends :movies, :shows, :other_things # ... end
Just pass in a list of classes and that's it! You can declare any model in
your app to receive recommendations; it does not have to be
User. As long as
only one model calls the
recommends method, you're fine!
At this point, your user will be ready to
current_user.like Movie.create(:title => '2001: A Space Odyssey', :year => 1968) #=> true
current_user.dislike Movie.create(:title => 'Star Wars: Episode I - The Phantom Menace', :year => 1999) #=> true
In addition, several helpful methods are available to your user now:
current_user.likes? Movie.find_by_title('2001: A Space Odyssey') #=> true current_user.dislikes? Movie.find_by_title('Star Wars: Episode I - The Phantom Menace') #=> true other_movie = Movie.create('Back to the Future', :year => 1985) current_user.dislikes? other_movie #=> false current_user.like other_movie #=> true current_user.liked #=> [#<Movie name: '2001: A Space Odyssey', year: 1968>, #<Movie name: 'Back to the Future', :year => 1985>] current_user.disliked #=> [#<Movie name: 'Star Wars: Episode I - The Phantom Menace', year: 1999>]
Because you are allowed to declare multiple models as recommendable, you may wish to return a set of liked or disliked objects for only one of those models.
current_user.liked_movies #=> [#<Movie name: '2001: A Space Odyssey', year: 1968>, #<Movie name: 'Back to the Future', :year => 1985>] current_user.disliked_shows #=> 
If you want to give your user the ability to
ignore recommendations or even
just hide stuff on your website that they couldn't care less about, you can!
weird_movie_nobody_wants_to_watch = Movie.create(:title => 'Cool World', :year => 1998) current_user.ignore weird_movie_nobody_wants_to_watch #=> true current_user.ignored #=> [#<Movie name: 'Cool World', year: 1998>] current_user.ignored_shows #=> 
Do what you will with this list of records. The power is yours.
Saving for later
Your user might want to maintain a list of items to try later but still receive new recommendations. For this, you can use Recommendable::StashedItems. Note that adding an item to a user's stash will remove it from their list of recommendations. Additionally, an item can not be stashed if the user already likes or dislikes it.
movie_to_watch_later = Movie.create(:title => 'The Descendants', :year => 2011) current_user.stash(movie_to_watch_later) #=> true current_user.stashed_movies #=> [#<Movie name: 'The Descendants', year: 2011>] # Later... current_user.like(movie_to_watch_later) #=> true current_user.stash(movie_to_watch_later) #=> nil current_user.stashed?(movie_to_watch_later) #=> false
Note that liking a movie that has already been disliked (or vice versa) will
simply destroy the old rating and create a new one. If a user attempts to
a movie that they already like, however, nothing happens and
nil is returned.
If you wish to manually remove an item from a user's likes or dislikes or
ignored records, you can:
current_user.like Movie.create(:title => 'Avatar', :year => 2009) #=> true current_user.unlike Movie.find_by_title('Avatar') #=> true current_user.liked #=> 
You can use
unstash in the same fashion. So, as
far as the Likes and Dislikes go, do you think that's enough? Because I didn't.
friend = User.create(:username => 'joeblow') awesome_movie = Movie.find_by_title('2001: A Space Odyssey') friend.like awesome_movie #=> true awesome_movie.liked_by #=> [#<User username: 'davidcelis'>, #<User username: 'joeblow'>] Movie.find_by_title('Star Wars: Episode I - The Phantom Menace').disliked_by #=> [#<User username: 'davidcelis'>] current_user.common_likes_with(friend) #=> [#<Movie title: '2001: A Space Odyssey', year: 1968>] current_user.common_likes_with(friend, :class => Show) #=> 
disagreements_with are available for similar use.
When a user submits a new
Dislike, they enter a queue to have their
recommendations refreshed. Once that user exits the queue, you can retrieve
these like so:
current_user.recommendations #=> [#<Movie highly_recommended>, #<Show somewhat_recommended>, #<Movie meh>] current_user.recommended_shows #=> [#<Show somewhat_recommended>]
The top recommendations are returned in an array ordered by how good recommendable believes the recommendation to be (from best to worst).
current_user.like somewhat_recommended_show #=> true current_user.recommendations #=> [#<Movie highly_recommended>, #<Movie meh>]
Finally, you can also get a list of the users found to be most similar to your current user:
current_user.similar_raters #=> [#<User username: 'joe-blow'>, #<User username: 'less-so-than-joe-blow']
Likewise, this list is ordered from most similar to least similar.
Some of the above methods are tweakable with options. For example, you can adjust the number of recommendations returned to you (the default is 10) and the number of similar uses returned (also 10). To see these options, check the documentation.
A note on Redis
Recommendable currently depends on Redis. It will install the redis-rb gem as a dependency, but you must install Redis and run it yourself. Also note that your Redis database must be persistent. Recommendable will use Redis to permanently store sorted sets to quickly access recommendations. Please take care with your Redis database! Fortunately, if you do lose your Redis database, there's hope (more on that later).
Recommendable requires Redis to deliver recommendations. Why? Because my collaborative filtering algorithm is based almost entirely on set math, and Ruby's Set class just won't cut it for fast recommendations.
For Mac OS X users, homebrew is by far the easiest way to install Redis.
$ brew install redis $ redis-server /usr/local/etc/redis.conf
You should now have Redis running as a daemon on localhost:6379
Resque (which is also a dependency of recommendable) includes Rake tasks that will install and run Redis for you:
$ git clone git://github.com/defunkt/resque.git $ cd resque $ rake redis:install dtach:install $ rake redis:start
If you do not have admin rights to your machine:
$ git clone git://github.com/defunkt/resque.git $ cd resque $ PREFIX=<your_prefix> rake redis:install dtach:install $ rake redis:start
Redis will now be running on localhost:6379. After a second, you can hit
to detach and keep Redis running in the background.
Manually regenerating recommendations
If a catastrophe occurs and your Redis database is either destroyed or rendered unusable in some other way, there is hope. You can run the following from your application's console (assuming your user class is User):
User.all.each do |user| user.update_similarities user.update_recommendations end
But please try not to have to do this manually!
Contributing to recommendable
Read the Contributing wiki page first.
Once you've made your great commits:
- Fork recommendable
- Create a feature branch
- Write your code (and tests please)
- Push to your branch's origin
- Create a Pull Request from your branch
- That's it!
git clone git://github.com/davidcelis/recommendable.git
- Home: http://github.com/davidcelis/recommendable
- Docs: http://rubydoc.info/gems/recommendable/frames
- Bugs: http://github.com/davidcelis/recommendable/issues
- Gems: http://rubygems.org/gems/recommendable
Copyright © 2012 David Celis. See LICENSE.txt for further details.