recommendable 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.markdown CHANGED
@@ -1,8 +1,27 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
- 0.1.5 (current version)
4
+ 0.1.6 (current version)
5
5
  -----------------------
6
+ * Dynamic finders for your User class:
7
+
8
+ `current_user.liked_movies`
9
+ `current_user.disliked_shows`
10
+ `current_user.recommended_movies`
11
+
12
+ * Implement sorting of recommendable models:
13
+
14
+ ``` ruby
15
+ >> Movie.top(5)
16
+ => [#<Movie id: 14>, #<Movie id: 15>, #<Movie id: 13>, #<Movie id: 12>, #<Movie id: 11>]
17
+ >> Movie.top
18
+ => #<Movie id: 14>
19
+ ```
20
+
21
+ * Bugfix: users/recommendable objects will now be removed from Redis upon being destroyed
22
+
23
+ 0.1.5
24
+ -----
6
25
  * Major bugfix: similarity values were, incorrectly, being calculated as 0.0 for every user pair. Sorry!
7
26
  * The tables for all models are now all prepended with "recommendable_" to avoid possible collision. If upgrading from a previous version, please do the following:
8
27
 
data/Gemfile CHANGED
@@ -13,5 +13,5 @@ group :development do
13
13
  gem "shoulda"
14
14
  gem "miniskirt"
15
15
  gem "yard", "~> 0.6.0"
16
- gem "bundler", "~> 1.0.0"
16
+ gem "bundler", ">= 1.0.0"
17
17
  end
data/Gemfile.lock CHANGED
@@ -106,7 +106,7 @@ PLATFORMS
106
106
  ruby
107
107
 
108
108
  DEPENDENCIES
109
- bundler (~> 1.0.0)
109
+ bundler (>= 1.0.0)
110
110
  miniskirt
111
111
  minitest
112
112
  rails (>= 3.1.0)
data/LICENSE.txt CHANGED
@@ -1,3 +1,5 @@
1
+ (The MIT License)
2
+
1
3
  Copyright (c) 2012 David Celis
2
4
 
3
5
  Permission is hereby granted, free of charge, to any person obtaining
data/README.markdown CHANGED
@@ -1,22 +1,6 @@
1
- # recommendable [![Build Status](https://secure.travis-ci.org/davidcelis/recommendable.png)](http://travis-ci.org/davidcelis/recommendable)
1
+ # Recommendable [![Build Status](https://secure.travis-ci.org/davidcelis/recommendable.png)](http://travis-ci.org/davidcelis/recommendable)
2
2
 
3
- Recommendable is a Rails Engine to add Like/Dislike functionality to your
4
- application. It uses Redis to generate recommendations quickly through [a
5
- collaborative filtering algorithm that I modified myself][6]. Your users' tastes
6
- are compared with one another and used to give them great recommendations!
7
- Yes, Redis is required. Scroll to the end of the README for more info on that.
8
-
9
- Why Likes and Dislikes?
10
- -----------------------
11
-
12
- [I hate five-star rating scales][0].
13
-
14
- **tl;dr:** Binary voting habits are most certainly not an odd phenomenon.
15
- People tend to vote in only two different ways. Some folks give either 1 star
16
- or 5 stars. Some people fluctuate between 3 and 4 stars. There are always
17
- outliers, but what it comes down to is this: a person's binary votes indicate,
18
- in general, a dislike or like of what they're voting on. I'm just giving the
19
- people what they want.
3
+ Recommendable is an engine for Rails 3 applications to quickly add the ability for your users to Like/Dislike items and receive recommendations for new items. It uses Redis to store your recommendations and keep them sorted by how good the recommendation is.
20
4
 
21
5
  Installation
22
6
  ------------
@@ -24,38 +8,28 @@ Installation
24
8
  Add the following to your Rails application's `Gemfile`:
25
9
 
26
10
  ``` ruby
27
- gem "recommendable"
11
+ gem "recommendable", :git => "git://github.com/davidcelis/recommendable"
28
12
  ```
29
13
 
30
- After your `bundle install`, you can then run:
14
+ After bundling, run the installation generator:
31
15
 
32
16
  ``` bash
33
17
  $ rails g recommendable:install
34
18
  ```
35
19
 
36
- After running the installation generator, you should double check
37
- `config/initializers/recommendable.rb` for options on configuring your Redis
38
- connection.
39
-
40
- Finally, Recommendable uses Resque to place users in a queue. Users must wait
41
- their turn to regenerate recommendations so that your application does not get
42
- throttled. Don't worry, though! Most of the time, your users will barely have
43
- to wait. In fact, you can run multiple resque workers if you wish.
44
-
45
- Assuming you have `redis-server` running...
20
+ Double check `config/initializers/recommendable.rb` for options on configuring your Redis connection. After a user likes or dislikes something new, they are placed in a Resque queue to have their recommendation updated. Start up resque like so:
46
21
 
47
22
  ``` bash
48
23
  $ QUEUE=recommendable rake environment resque:work
49
24
  ```
50
25
 
51
26
  You can run this command multiple times if you wish to start more than one
52
- worker. This is the standard rake task for starting a Resque worker so, for
53
- more options on this task, head over to [defunkt/resque][1]
27
+ worker. For more options on this task, head over to [defunkt/resque][resque].
54
28
 
55
29
  Usage
56
30
  -----
57
31
 
58
- In your Rails model that represents your application's user:
32
+ In your Rails model that will be receiving recommendations:
59
33
 
60
34
  ``` ruby
61
35
  class User < ActiveRecord::Base
@@ -65,247 +39,49 @@ class User < ActiveRecord::Base
65
39
  end
66
40
  ```
67
41
 
68
- Just pass in a list of classes and that's it!
69
-
70
- ### Liking/Disliking
71
-
72
- At this point, your user will be ready to `like` movies...
73
-
74
- ``` ruby
75
- current_user.like Movie.create(:title => '2001: A Space Odyssey', :year => 1968)
76
- #=> true
77
- ```
78
-
79
- ... or `dislike` them:
80
-
81
- ``` ruby
82
- current_user.dislike Movie.create(:title => 'Star Wars: Episode I - The Phantom Menace', :year => 1999)
83
- #=> true
84
- ```
85
-
86
- In addition, several helpful methods are available to your user now:
87
-
88
- ``` ruby
89
- current_user.likes? Movie.find_by_title('2001: A Space Odyssey')
90
- #=> true
91
- current_user.dislikes? Movie.find_by_title('Star Wars: Episode I - The Phantom Menace')
92
- #=> true
93
- other_movie = Movie.create('Back to the Future', :year => 1985)
94
- current_user.dislikes? other_movie
95
- #=> false
96
- current_user.like other_movie
97
- #=> true
98
- current_user.liked
99
- #=> [#<Movie name: '2001: A Space Odyssey', year: 1968>, #<Movie name: 'Back to the Future', :year => 1985>]
100
- current_user.disliked
101
- #=> [#<Movie name: 'Star Wars: Episode I - The Phantom Menace', year: 1999>]
102
- ```
103
-
104
- Because you are allowed to declare multiple models as recommendable, you may
105
- wish to return a set of liked or disliked objects for only one of those
106
- models.
107
-
108
- ``` ruby
109
- current_user.liked_for(Movie) # or "movie", or :movie
110
- #=> [#<Movie name: '2001: A Space Odyssey', year: 1968>, #<Movie name: 'Back to the Future', :year => 1985>]
111
- current_user.disliked_for(Show)
112
- #=> []
113
- ```
114
-
115
- ### Ignoring
116
-
117
- If you want to give your user the ability to `ignore` recommendations or even
118
- just hide stuff on your website that they couldn't care less about, you can!
119
-
120
- ``` ruby
121
- weird_movie_nobody_wants_to_watch = Movie.create(:title => 'Cool World', :year => 1998)
122
- current_user.ignore weird_movie_nobody_wants_to_watch
123
- #=> true
124
- current_user.ignored
125
- #=> [#<Movie name: 'Cool World', year: 1998>]
126
- current_user.ignored_for(Show)
127
- #=> []
128
- ```
129
-
130
- Do what you will with this list of records. The power is yours.
131
-
132
- ### Saving for later
133
-
134
- Your user might want to maintain a list of items to try later but still receive
135
- new recommendations. For this, you can use Recommendable::StashedItems. Note that
136
- adding an item to a user's stash will remove it from their list of recommendations.
137
- Additionally, an item can not be stashed if the user already likes or dislikes it.
138
-
139
- ``` ruby
140
- movie_to_watch_later = Movie.create(:title => 'The Descendants', :year => 2011)
141
- current_user.stash(movie_to_watch_later)
142
- #=> true
143
- current_user.stashed
144
- #=> [#<Movie name: 'The Descendants', year: 2011>]
145
- # Later...
146
- current_user.like(movie_to_watch_later)
147
- #=> true
148
- current_user.stash(movie_to_watch_later)
149
- #=> nil
150
- current_user.stashed?(movie_to_watch_later)
151
- #=> false
152
- ```
153
-
154
- ### Unliking/Undisliking/Unignoring/Unstashing
155
-
156
- Note that liking a movie that has already been disliked (or vice versa) will
157
- simply destroy the old rating and create a new one. If a user attempts to `like`
158
- a movie that they already like, however, nothing happens and `nil` is returned.
159
- If you wish to manually remove an item from a user's likes or dislikes or
160
- ignored records, you can:
161
-
162
- ``` ruby
163
- current_user.like Movie.create(:title => 'Avatar', :year => 2009)
164
- #=> true
165
- current_user.unlike Movie.find_by_title('Avatar')
166
- #=> true
167
- current_user.liked
168
- #=> []
169
- ```
170
-
171
- You can use `undislike`, `unignore` and `unstash` in the same fashion. So, as
172
- far as the Likes and Dislikes go, do you think that's enough? Because I didn't.
173
-
174
- ``` ruby
175
- friend = User.create(:username => 'joeblow')
176
- awesome_movie = Movie.find_by_title('2001: A Space Odyssey')
177
- friend.like awesome_movie
178
- #=> true
179
- awesome_movie.liked_by
180
- #=> [#<User username: 'davidcelis'>, #<User username: 'joeblow'>]
181
- Movie.find_by_title('Star Wars: Episode I - The Phantom Menace').disliked_by
182
- #=> [#<User username: 'davidcelis'>]
183
- current_user.common_likes_with(friend)
184
- #=> [#<Movie title: '2001: A Space Odyssey', year: 1968>]
185
- current_user.common_likes_with(friend, :class => Show)
186
- #=> []
187
- ```
188
-
189
- `common_dislikes_with` and `disagreements_with` are available for similar use.
190
-
191
- Recommendations
192
- ---------------
193
-
194
- When a user submits a new `Like` or `Dislike`, they enter a queue to have their
195
- recommendations refreshed. Once that user exits the queue, you can retrieve
196
- these like so:
197
-
198
- ``` ruby
199
- current_user.recommendations
200
- #=> [#<Movie highly_recommended>, #<Show somewhat_recommended>, #<Movie meh>]
201
- current_user.recommendations_for(Show)
202
- #=> [#<Show somewhat_recommended>]
203
- ```
204
-
205
- The top recommendations are returned in an array ordered by how good recommendable
206
- believes the recommendation to be (from best to worst).
207
-
208
- ``` ruby
209
- current_user.like somewhat_recommended_show
210
- #=> true
211
- current_user.recommendations
212
- #=> [#<Movie highly_recommended>, #<Movie meh>]
213
- ```
214
-
215
- Finally, you can also get a list of the users found to be most similar to your
216
- current user:
217
-
218
- ``` ruby
219
- current_user.similar_raters
220
- #=> [#<User username: 'joe-blow'>, #<User username: 'less-so-than-joe-blow']
221
- ```
222
-
223
- Likewise, this list is ordered from most similar to least similar.
224
-
225
- Documentation
226
- -------------
227
-
228
- Some of the above methods are tweakable with options. For example, you can
229
- adjust the number of recommendations returned to you (the default is 10) and
230
- the number of similar uses returned (also 10). To see these options, check
231
- the documentation.
42
+ That's it! Please note, however, that you may only do this in one model at this time.
232
43
 
233
- A note on Redis
234
- ---------------
235
-
236
- Recommendable currently depends on [Redis](http://redis.io/). It will install
237
- the redis-rb gem as a dependency, but you must install Redis and run it
238
- yourself. Also note that your Redis database must be persistent. Recommendable
239
- will use Redis to permanently store sorted sets to quickly access recommendations.
240
- Please take care with your Redis database! Fortunately, if you do lose your
241
- Redis database, there's hope (more on that later).
44
+ For more details on how to use Recommendable once it's installed and configured, [check out the more detailed README][recommendable] or see the [documentation][documentation].
242
45
 
243
46
  Installing Redis
244
47
  ----------------
245
48
 
246
- Recommendable requires Redis to deliver recommendations. Why? Because my
247
- collaborative filtering algorithm is based almost entirely on set math, and
248
- Ruby's Set class just won't cut it for fast recommendations.
49
+ Recommendable requires Redis to deliver recommendations. The collaborative filtering logic is based almost entirely on set math, and Redis is blazing fast for this. _NOTE: Your redis database MUST be persistent._
249
50
 
250
51
  ### Homebrew
251
52
 
252
53
  For Mac OS X users, homebrew is by far the easiest way to install Redis.
253
54
 
254
- $ brew install redis
255
- $ redis-server /usr/local/etc/redis.conf
256
-
257
- You should now have Redis running as a daemon on localhost:6379
55
+ ``` bash
56
+ $ brew install redis
57
+ ```
258
58
 
259
59
  ### Via Resque
260
60
 
261
61
  Resque (which is also a dependency of recommendable) includes Rake tasks that
262
62
  will install and run Redis for you:
263
63
 
264
- $ git clone git://github.com/defunkt/resque.git
265
- $ cd resque
266
- $ rake redis:install dtach:install
267
- $ rake redis:start
268
-
269
- If you do not have admin rights to your machine:
270
-
271
- $ git clone git://github.com/defunkt/resque.git
272
- $ cd resque
273
- $ PREFIX=<your_prefix> rake redis:install dtach:install
274
- $ rake redis:start
275
-
276
- Redis will now be running on localhost:6379. After a second, you can hit `ctrl-\`
277
- to detach and keep Redis running in the background.
278
-
279
- (Thanks to [defunkt][1] for mentioning this
280
- method, and thanks to [ezmobius][2] for
281
- making it possible)
282
-
283
- Manually regenerating recommendations
284
- -------------------------------------
285
-
286
- If a catastrophe occurs and your Redis database is either destroyed or rendered
287
- unusable in some other way, there is hope. You can run the following from your
288
- application's console (assuming your user class is User):
289
-
290
- User.all.each do |user|
291
- user.update_similarities
292
- user.update_recommendations
293
- end
64
+ ``` bash
65
+ $ git clone git://github.com/defunkt/resque.git
66
+ $ cd resque
67
+ $ rake redis:install dtach:install
68
+ $ rake redis:start
69
+ ```
294
70
 
295
- But please try not to have to do this manually!
71
+ Redis will now be running on localhost:6379. After a second, you can hit `ctrl-\` to detach and keep Redis running in the background.
296
72
 
297
73
  Contributing to recommendable
298
74
  -----------------------------
299
75
 
300
- Read the [Contributing][3] wiki page first.
76
+ Read the [Contributing][contributing] wiki page first.
301
77
 
302
78
  Once you've made your great commits:
303
79
 
304
- 1. [Fork][4] recommendable
80
+ 1. [Fork][forking] recommendable
305
81
  2. Create a feature branch
306
82
  3. Write your code (and tests please)
307
83
  4. Push to your branch's origin
308
- 5. Create a [Pull Request][5] from your branch
84
+ 5. Create a [Pull Request][pull requests] from your branch
309
85
  6. That's it!
310
86
 
311
87
  Links
@@ -322,10 +98,11 @@ Copyright
322
98
  Copyright © 2012 David Celis. See LICENSE.txt for
323
99
  further details.
324
100
 
325
- [0]: http://davidcelis.com/blog/2012/02/01/why-i-hate-five-star-ratings/
326
- [1]: https://github.com/defunkt/resque
327
- [2]: https://github.com/ezmobius/redis-rb
328
- [3]: http://wiki.github.com/defunkt/resque/contributing
329
- [4]: http://help.github.com/forking/
330
- [5]: http://help.github.com/pull-requests/
331
- [6]: http://davidcelis.com/blog/2012/02/07/collaborative-filtering-with-likes-and-dislikes/
101
+ [stars]: http://davidcelis.com/blog/2012/02/01/why-i-hate-five-star-ratings/
102
+ [resque]: https://github.com/defunkt/resque
103
+ [contributing]: http://wiki.github.com/defunkt/resque/contributing
104
+ [forking]: http://help.github.com/forking/
105
+ [pull requests]: http://help.github.com/pull-requests/
106
+ [collaborative filtering]: http://davidcelis.com/blog/2012/02/07/collaborative-filtering-with-likes-and-dislikes/
107
+ [recommendable]: http://davidcelis.github.com/recommendable/
108
+ [documentation]: http://rubydoc.info/gems/recommendable/frames
data/TODO CHANGED
@@ -1,4 +1,7 @@
1
1
  = TODO
2
2
 
3
+ * method_missing for User#common_likes_with, User#common_liked_with
4
+ * Use Sidekiq instead?
5
+ * Move dependencies to gemspec instead of Gemfile
3
6
  * Support Mongoid (and potentially other ORMs)
4
7
  * Allow the option NOT to queue up on like/dislike/ignore?
@@ -1,9 +1,10 @@
1
1
  module Recommendable
2
2
  class Dislike < ActiveRecord::Base
3
3
  self.table_name = 'recommendable_dislikes'
4
+ attr_accessible :user_id, :dislikeable_id, :dislikeable_type
4
5
 
5
- belongs_to :user, :class_name => Recommendable.user_class.to_s
6
- belongs_to :dislikeable, :polymorphic => :true
6
+ belongs_to :user, :class_name => Recommendable.user_class.to_s, :foreign_key => :user_id
7
+ belongs_to :dislikeable, :polymorphic => true
7
8
 
8
9
  validates :user_id, :uniqueness => { :scope => [:dislikeable_id, :dislikeable_type],
9
10
  :message => "has already disliked this item" }
@@ -1,9 +1,10 @@
1
1
  module Recommendable
2
2
  class Ignore < ActiveRecord::Base
3
3
  self.table_name = 'recommendable_ignores'
4
+ attr_accessible :user_id, :ignoreable_id, :ignoreable_type
4
5
 
5
- belongs_to :user, :class_name => Recommendable.user_class.to_s
6
- belongs_to :ignoreable, :polymorphic => :true
6
+ belongs_to :user, :class_name => Recommendable.user_class.to_s, :foreign_key => :user_id
7
+ belongs_to :ignoreable, :polymorphic => true
7
8
 
8
9
  validates :user_id, :uniqueness => { :scope => [:ignoreable_id, :ignoreable_type],
9
10
  :message => "has already ignored this item" }
@@ -1,9 +1,10 @@
1
1
  module Recommendable
2
2
  class Like < ActiveRecord::Base
3
3
  self.table_name = 'recommendable_likes'
4
+ attr_accessible :user_id, :likeable_id, :likeable_type
4
5
 
5
- belongs_to :user, :class_name => Recommendable.user_class.to_s
6
- belongs_to :likeable, :polymorphic => :true
6
+ belongs_to :user, :class_name => Recommendable.user_class.to_s, :foreign_key => :user_id
7
+ belongs_to :likeable, :polymorphic => true
7
8
 
8
9
  validates :user_id, :uniqueness => { :scope => [:likeable_id, :likeable_type],
9
10
  :message => "has already liked this item" }