Skip to content

Commit

Permalink
Bump version to 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
David Celis committed Jan 28, 2012
1 parent 8cff898 commit f65cedc
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 70 deletions.
244 changes: 185 additions & 59 deletions README.rdoc
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
= recommendable

NOTE: This is a work in progress, and has not yet been fully pushed to RubyGems. Check back later!

Recommendable is a Rails Engine to add Like/Dislike functionality to your
application and uses redis to generate recommendations quickly through
collaborative filtering. Your users' tastes are compared with one another and
used to give them great recommendations!

== Installation

Recommendable currently depends on {redis}[http://redis.io/]. It will install
the gem as a dependency, but you must have a redis server running for
Recommendable to connect to. Also note that your redis database must be
persistent. While much of Recommendable's logic uses redis only for temporary
storage, the sorted sets that contain the ultimate results of the collaborative
filtering algorithm (the similarity values and predictions for recommendations)
are stored in redis. If your redis database is not persistent, they will be lost.
If you have not yet installed redis, here's how:
used to give them great recommendations! Yes, redis is required. Scroll to the
end of the README for more info on that.

For Mac OS X:
== Why Likes and Dislikes?

$ [brew | port] install redis
$ redis-server
Why not? {This is why}[http://youtube-global.blogspot.com/2009/09/five-stars-dominate-ratings.html].

Ubuntu/Linux:
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: peoples binary votes indicate, in general, a dislike
or like of what they're voting on. I'm just giving the people what they want.

$ sudo apt-get install redis
$ redis-server

Or you can build from source.
== Installation

$ wget http://redis.googlecode.com/files/redis-2.4.6.tar.gz
$ tar xzf redis-2.4.6.tar.gz
$ cd redis-2.4.6
$ make
$ src/redis-server

Once redis-server is running, you're ready to install Recommendable. Simply add
Add the following to your Rails application's Gemfile:

gem "recommendable"

to your Rails app's Gemfile and bundle. Then, from your shell:
After bundling, you can then run:

$ rails g recommendable:install (--user-class=User)

After running the installation generator, you should double check `config/initializers/recommendable.rb` for options on configuring your Redis connection.
After running the installation generator, you should double check
config/initializers/recommendable.rb for options on configuring your Redis
connection.

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 redis-server running...

$ 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}[https://github.com/defunkt/resque]

== Usage

Expand All @@ -63,62 +61,190 @@ Then, from any Rails model you wish your user to be able to like/dislike:

# ...
end

class Show < ActiveRecord::Base
acts_as_recommendable

# ...
end

And that's it!

=== Liking/Disliking

At this point, your user will be ready to like movies...

>> user = User.create(:username => 'davidcelis')
>> awesome_movie = Movie.create(:title => '2001: A Space Odyssey', :year => 1968)
>> user.like(awesome_movie)
=> true
>> another_movie = Movie.create(:title => 'Back to the Future', :year => 1985)
>> user.like(another_movie)
>> current_user.like Movie.create(:title => '2001: A Space Odyssey', :year => 1968)
=> true

... or dislike them:

>> shitty_movie = Movie.create(:title => 'Star Wars: Episode I - The Phantom Menace', :year => 1999)
>> user.dislike(shitty_movie)
>> current_user.dislike Movie.create(:title => 'Star Wars: Episode I - The Phantom Menace', :year => 1999)
=> true

Several helpful methods are available to your user now:
In addition, several helpful methods are available to your user now:

>> user.likes?(awesome_movie)
>> current_user.likes? Movie.find_by_title('2001: A Space Odyssey')
=> true
>> user.dislikes?(shitty_movie)
>> current_user.dislikes? Movie.find_by_title('Star Wars: Episode I - The Phantom Menace')
=> true
>> user.dislikes?(another_movie)
>> other_movie = Movie.create('Back to the Future', :year => 1985)
>> current_user.dislikes? other_movie
=> false
>> user.liked_objects
=> [#<Movie id: 1, name: '2001: A Space Odyssey', year: 1968>, #<Movie id: 2, name: 'Back to the Future', :year => 1985>]
>> user.disliked_objects
=> [#<Movie id: 3, name: 'Star Wars: Episode I - The Phantom Menace', year: 1999>]
>> current_user.like other_movie
=> true
>> current_user.liked_records
=> [#<Movie id: 1, name: '2001: A Space Odyssey', year: 1968>, #<Movie id: 3, name: 'Back to the Future', :year => 1985>]
>> current_user.disliked_records
=> [#<Movie id: 2, 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_records_for(Movie)
=> [#<Movie id: 1, name: '2001: A Space Odyssey', year: 1968>, #<Movie id: 3, name: 'Back to the Future', :year => 1985>]
>> current_user.disliked_records_for(Show)
=> []

Liking a movie that has already been disliked (or vice versa) will simply destroy the old rating and create a new one. If you wish to manually remove an item from a user's likes or dislikes, you can:
=== Ignoring

>> decent_movie = Movie.create(:title => 'Avatar', :year => 2009)
>> user.like(decent_movie)
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
>> user.liked_objects
=> [#<Movie id: 1, name: '2001: A Space Odyssey', year: 1968>, #<Movie id: 2, name: 'Back to the Future', :year => 1985>, #<Movie id: 4, name: 'Avatar', year: 2009>]
>> user.unlike(decent_movie)
>> current_user.ignored_records
=> [#<Movie id: 4, name: 'Cool World', year: 1998>]
>> current_user.ignored_records_for(Show)
=> []

Do what you will with this list of records. The power is yours.

=== Unliking/Undisliking/Unignoring

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 like
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
>> user.unlike Movie.find_by_title('Avatar')
=> true
>> user.liked_objects
=> [#<Movie id: 1, name: '2001: A Space Odyssey', year: 1968>, #<Movie id: 2, name: 'Back to the Future', :year => 1985>]
>> user.liked_records
=> []

You can use User#undislike and User#unignore in the same fashion.

You can call `user.undislike(movie)` in the same way. So, as far as the Likes and Dislikes go, do you think that's enough? Because I didn't.
You can call `user.undislike(movie)` in the same way. So, as far as the Likes
and Dislikes go, do you think that's enough? Because I didn't.

>> friend = User.create(:username => 'joeblow')
>> friend.like(awesome_movie)
>> awesome_movie = Movie.find_by_title('2001: A Space Odyssey')
>> friend.like awesome_movie
=> true
>> awesome_movie.liked_by
=> [#<User id: 1, username: 'davidcelis'>, #<User id: 2, username: 'joeblow'>]
>> shitty_movie.disliked_by
>> Movie.find_by_title('Star Wars: Episode I - The Phantom Menace).disliked_by
=> [#<User id: 1, username: 'davidcelis'>]

=== Recommendations

I'll update this section soon.
When a user submits a new Like or 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.recommendations_for(Show)
=> [#<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.

== Documentation

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}[http://redis.io/]. 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).

== Installing Redis

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.

=== Homebrew

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

=== Via Resque

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 ctrl-\
to detach and keep Redis running in the background.

(Thanks to {defunkt}[https://github.com/defunkt/resque] for mentioning this
method, and thanks to {ezmobius}[https://github.com/ezmobius/redis-rb] for
making it possible)

== 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

Expand Down
1 change: 1 addition & 0 deletions lib/recommendable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'recommendable/acts_as_recommendable'
require 'recommendable/exceptions'
require 'recommendable/railtie' if defined?(Rails)
require 'recommendable/version'

module Recommendable
mattr_accessor :redis, :user_class
Expand Down
18 changes: 9 additions & 9 deletions lib/recommendable/acts_as_recommended_to.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def unlike(object)
# Get a list of records that `self` currently likes

# @return [Array] an array of ActiveRecord objects that `self` has liked
def liked_objects
def liked_records
likes.map {|like| like.likeable}
end

Expand All @@ -74,7 +74,7 @@ def liked_objects
# @param [Class, String, Symbol] klass the class for which you would like to
# return `self`'s likes. Can be the class constant, or a String/Symbol
# representation of the class name.
# @note You should not need to use this method. (see {#liked_objects_for})
# @note You should not need to use this method. (see {#liked_records_for})
def likes_for(klass)
likes.where(:likeable_type => klassify(klass).to_s)
end
Expand All @@ -86,7 +86,7 @@ def likes_for(klass)
# class constant, or a String/Symbol representation of the class name.
# @return [Array] an array of ActiveRecord objects that `self` has liked
# belonging to `klass`
def liked_objects_for(klass)
def liked_records_for(klass)
klassify(klass).find likes_for(klass).map(&:likeable_id)
end
end
Expand Down Expand Up @@ -132,7 +132,7 @@ def undislike(object)
# Get a list of records that `self` currently dislikes

# @return [Array] an array of ActiveRecord objects that `self` has disliked
def disliked_objects
def disliked_records
dislikes.map {|dislike| dislike.dislikeable}
end

Expand All @@ -142,7 +142,7 @@ def disliked_objects
# @param [Class, String, Symbol] klass the class for which you would like to
# return `self`'s dislikes. Can be the class constant, or a String/Symbol
# representation of the class name.
# @note You should not need to use this method. (see {#disliked_objects_for})
# @note You should not need to use this method. (see {#disliked_records_for})
def dislikes_for(klass)
dislikes.where(:dislikeable_type => klassify(klass).to_s)
end
Expand All @@ -154,7 +154,7 @@ def dislikes_for(klass)
# class constant, or a String/Symbol representation of the class name.
# @return [Array] an array of ActiveRecord objects that `self` has disliked
# belonging to `klass`
def disliked_objects_for(klass)
def disliked_records_for(klass)
klassify(klass).find dislikes_for(klass).map(&:dislikeable_id)
end
end
Expand Down Expand Up @@ -201,7 +201,7 @@ def unignore(object)
# Get a list of records that `self` is currently ignoring

# @return [Array] an array of ActiveRecord objects that `self` has ignored
def ignored_objects
def ignored_records
ignores.map {|ignore| ignore.ignoreable}
end

Expand All @@ -211,7 +211,7 @@ def ignored_objects
# @param [Class, String, Symbol] klass the class for which you would like to
# return `self`'s ignores. Can be the class constant, or a String/Symbol
# representation of the class name.
# @note You should not need to use this method. (see {#ignored_objects_for})
# @note You should not need to use this method. (see {#ignored_records_for})
def ignores_for(klass)
ignores.where(:ignoreable_type => klassify(klass).to_s)
end
Expand All @@ -223,7 +223,7 @@ def ignores_for(klass)
# class constant, or a String/Symbol representation of the class name.
# @return [Array] an array of ActiveRecord objects that `self` has ignored
# belonging to `klass`
def ignored_objects_for(klass)
def ignored_records_for(klass)
klassify(klass).find ignores_for(klass).map(&:ignoreable_id)
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/recommendable/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Recommendable
VERSION = '0.0.1'
VERSION = '0.1.0'
end
Loading

0 comments on commit f65cedc

Please sign in to comment.