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 +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 [](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" }
|