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 +20 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +1 -1
- data/LICENSE.txt +2 -0
- data/README.markdown +31 -254
- data/TODO +3 -0
- data/app/models/recommendable/dislike.rb +3 -2
- data/app/models/recommendable/ignore.rb +3 -2
- data/app/models/recommendable/like.rb +3 -2
- data/app/models/recommendable/stashed_item.rb +2 -1
- data/app/workers/recommendable/recommendation_refresher.rb +2 -3
- data/lib/generators/recommendable/templates/initializer.rb +6 -0
- data/lib/recommendable/acts_as_recommendable.rb +39 -7
- data/lib/recommendable/acts_as_recommended_to.rb +105 -82
- data/lib/recommendable/engine.rb +1 -0
- data/lib/recommendable/version.rb +1 -1
- data/recommendable.gemspec +1 -1
- data/spec/models/movie_spec.rb +69 -0
- data/spec/models/user_spec.rb +29 -1
- metadata +26 -25
data/CHANGELOG.markdown
CHANGED
@@ -1,8 +1,27 @@
|
|
1
1
|
Changelog
|
2
2
|
=========
|
3
3
|
|
4
|
-
0.1.
|
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
data/Gemfile.lock
CHANGED
data/LICENSE.txt
CHANGED
data/README.markdown
CHANGED
@@ -1,22 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# Recommendable [![Build Status](https://secure.travis-ci.org/davidcelis/recommendable.png)](http://travis-ci.org/davidcelis/recommendable)
|
2
2
|
|
3
|
-
Recommendable is
|
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
|
14
|
+
After bundling, run the installation generator:
|
31
15
|
|
32
16
|
``` bash
|
33
17
|
$ rails g recommendable:install
|
34
18
|
```
|
35
19
|
|
36
|
-
After
|
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.
|
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
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
255
|
-
|
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
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
-
|
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][
|
76
|
+
Read the [Contributing][contributing] wiki page first.
|
301
77
|
|
302
78
|
Once you've made your great commits:
|
303
79
|
|
304
|
-
1. [Fork][
|
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][
|
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
|
-
[
|
326
|
-
[
|
327
|
-
[
|
328
|
-
[
|
329
|
-
[
|
330
|
-
[
|
331
|
-
[
|
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,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 =>
|
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 =>
|
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 =>
|
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" }
|