recommendable 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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" }