recommendable 0.1.6 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +10 -18
- data/README.markdown +0 -3
- data/lib/generators/recommendable/templates/initializer.rb +1 -1
- data/lib/recommendable/acts_as_recommendable.rb +5 -5
- data/lib/recommendable/acts_as_recommended_to.rb +98 -74
- data/lib/recommendable/engine.rb +1 -1
- data/lib/recommendable/version.rb +1 -1
- data/spec/models/user_spec.rb +30 -0
- metadata +24 -24
data/Gemfile.lock
CHANGED
@@ -30,6 +30,8 @@ GEM
|
|
30
30
|
multi_json (~> 1.0)
|
31
31
|
arel (3.0.0)
|
32
32
|
builder (3.0.0)
|
33
|
+
celluloid (0.10.0)
|
34
|
+
connection_pool (0.9.1)
|
33
35
|
erubis (2.7.0)
|
34
36
|
hike (1.2.1)
|
35
37
|
i18n (0.6.0)
|
@@ -48,8 +50,6 @@ GEM
|
|
48
50
|
rack (1.4.0)
|
49
51
|
rack-cache (1.1)
|
50
52
|
rack (>= 0.4)
|
51
|
-
rack-protection (1.2.0)
|
52
|
-
rack
|
53
53
|
rack-ssl (1.3.2)
|
54
54
|
rack
|
55
55
|
rack-test (0.6.1)
|
@@ -73,20 +73,15 @@ GEM
|
|
73
73
|
rdoc (3.12)
|
74
74
|
json (~> 1.4)
|
75
75
|
redis (2.2.2)
|
76
|
-
redis-namespace (1.0
|
76
|
+
redis-namespace (1.1.0)
|
77
77
|
redis (< 3.0.0)
|
78
|
-
resque (1.19.0)
|
79
|
-
multi_json (~> 1.0)
|
80
|
-
redis-namespace (~> 1.0.2)
|
81
|
-
sinatra (>= 0.9.2)
|
82
|
-
vegas (~> 0.1.2)
|
83
|
-
resque-loner (1.2.0)
|
84
|
-
resque (~> 1.0)
|
85
78
|
shoulda (2.11.3)
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
79
|
+
sidekiq (1.0.0)
|
80
|
+
celluloid (~> 0.10.0)
|
81
|
+
connection_pool (~> 0.9.0)
|
82
|
+
multi_json
|
83
|
+
redis
|
84
|
+
redis-namespace
|
90
85
|
sprockets (2.1.2)
|
91
86
|
hike (~> 1.2)
|
92
87
|
rack (~> 1.0)
|
@@ -98,8 +93,6 @@ GEM
|
|
98
93
|
polyglot
|
99
94
|
polyglot (>= 0.3.1)
|
100
95
|
tzinfo (0.3.31)
|
101
|
-
vegas (0.1.11)
|
102
|
-
rack (>= 1.0.0)
|
103
96
|
yard (0.6.8)
|
104
97
|
|
105
98
|
PLATFORMS
|
@@ -111,8 +104,7 @@ DEPENDENCIES
|
|
111
104
|
minitest
|
112
105
|
rails (>= 3.1.0)
|
113
106
|
redis (~> 2.2.0)
|
114
|
-
resque (~> 1.19.0)
|
115
|
-
resque-loner (~> 1.2.0)
|
116
107
|
shoulda
|
108
|
+
sidekiq
|
117
109
|
sqlite3
|
118
110
|
yard (~> 0.6.0)
|
data/README.markdown
CHANGED
@@ -72,8 +72,6 @@ Redis will now be running on localhost:6379. After a second, you can hit `ctrl-\
|
|
72
72
|
|
73
73
|
Contributing to recommendable
|
74
74
|
-----------------------------
|
75
|
-
|
76
|
-
Read the [Contributing][contributing] wiki page first.
|
77
75
|
|
78
76
|
Once you've made your great commits:
|
79
77
|
|
@@ -100,7 +98,6 @@ further details.
|
|
100
98
|
|
101
99
|
[stars]: http://davidcelis.com/blog/2012/02/01/why-i-hate-five-star-ratings/
|
102
100
|
[resque]: https://github.com/defunkt/resque
|
103
|
-
[contributing]: http://wiki.github.com/defunkt/resque/contributing
|
104
101
|
[forking]: http://help.github.com/forking/
|
105
102
|
[pull requests]: http://help.github.com/pull-requests/
|
106
103
|
[collaborative filtering]: http://davidcelis.com/blog/2012/02/07/collaborative-filtering-with-likes-and-dislikes/
|
@@ -13,7 +13,7 @@ require "resque-loner"
|
|
13
13
|
# Resque also needs a connection to Redis. If you are currently initializing
|
14
14
|
# Resque somewhere else, leave this commented out. Otherwise, let it use the
|
15
15
|
# same Redis connection as Recommendable. If redis is running on localhost:6379,
|
16
|
-
#
|
16
|
+
# you can leave this commented out.
|
17
17
|
# Resque.redis = Recommendable.redis
|
18
18
|
|
19
19
|
# Tell Redis which database to use (usually between 0 and 15). The default of 0
|
@@ -11,8 +11,8 @@ module Recommendable
|
|
11
11
|
has_many :dislikes, :as => :dislikeable, :dependent => :destroy, :class_name => "Recommendable::Dislike"
|
12
12
|
has_many :ignores, :as => :ignoreable, :dependent => :destroy, :class_name => "Recommendable::Ignore"
|
13
13
|
has_many :stashes, :as => :stashable, :dependent => :destroy, :class_name => "Recommendable::StashedItem"
|
14
|
-
has_many :liked_by, :through => :likes, :source => :user, :foreign_key => :user_id
|
15
|
-
has_many :disliked_by, :through => :dislikes, :source => :user, :foreign_key => :user_id
|
14
|
+
has_many :liked_by, :through => :likes, :source => :user, :foreign_key => :user_id, :class_name => Recommendable.user_class.to_s
|
15
|
+
has_many :disliked_by, :through => :dislikes, :source => :user, :foreign_key => :user_id, :class_name => Recommendable.user_class.to_s
|
16
16
|
|
17
17
|
include LikeableMethods
|
18
18
|
include DislikeableMethods
|
@@ -31,10 +31,10 @@ module Recommendable
|
|
31
31
|
liked_by + disliked_by
|
32
32
|
end
|
33
33
|
|
34
|
-
def self.top
|
34
|
+
def self.top count = 1
|
35
35
|
ids = Recommendable.redis.zrevrange(self.score_set, 0, count - 1).map(&:to_i)
|
36
36
|
|
37
|
-
items = self.find
|
37
|
+
items = self.find ids
|
38
38
|
|
39
39
|
return items.sort do |x, y|
|
40
40
|
ids.index(x.id) <=> ids.index(y.id)
|
@@ -49,7 +49,7 @@ module Recommendable
|
|
49
49
|
z = 1.96
|
50
50
|
n = likes.count + dislikes.count
|
51
51
|
|
52
|
-
phat =
|
52
|
+
phat = likes.count / n.to_f
|
53
53
|
score = (phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n)
|
54
54
|
|
55
55
|
Recommendable.redis.zadd self.class.score_set, score, self.id
|
@@ -5,7 +5,7 @@ module Recommendable
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
module ClassMethods
|
8
|
-
def recommends
|
8
|
+
def recommends *things
|
9
9
|
acts_as_recommended_to
|
10
10
|
things.each { |thing| thing.to_s.classify.constantize.acts_as_recommendable }
|
11
11
|
end
|
@@ -28,8 +28,16 @@ module Recommendable
|
|
28
28
|
before_destroy :remove_from_similarities
|
29
29
|
before_destroy :remove_recommendations
|
30
30
|
|
31
|
-
def method_missing
|
32
|
-
if method.to_s =~
|
31
|
+
def method_missing method, *args, &block
|
32
|
+
if method.to_s =~ /^(liked|disliked)_(.+)_in_common_with$/
|
33
|
+
begin
|
34
|
+
super unless $2.classify.constantize.acts_as_recommendable?
|
35
|
+
|
36
|
+
self.send "#{$1}_in_common_with", *args, { :class => $2.classify.constantize }
|
37
|
+
rescue NameError
|
38
|
+
super
|
39
|
+
end
|
40
|
+
elsif method.to_s =~ /^(liked|disliked|ignored|stashed|recommended)_(.+)$/
|
33
41
|
begin
|
34
42
|
super unless $2.classify.constantize.acts_as_recommendable?
|
35
43
|
|
@@ -42,8 +50,9 @@ module Recommendable
|
|
42
50
|
end
|
43
51
|
end
|
44
52
|
|
45
|
-
def respond_to?
|
46
|
-
if method.to_s =~
|
53
|
+
def respond_to? method, include_private = false
|
54
|
+
if method.to_s =~ /^(liked|disliked|ignored|stashed|recommended)_(.+)$/ || \
|
55
|
+
method.to_s =~ /^common_(liked|disliked)_(.+)_with$/
|
47
56
|
begin
|
48
57
|
$2.classify.constantize.acts_as_recommendable?
|
49
58
|
rescue NameError
|
@@ -68,7 +77,7 @@ module Recommendable
|
|
68
77
|
# @param [Object] object the object you want self to like.
|
69
78
|
# @return true if object has been liked
|
70
79
|
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
71
|
-
def like
|
80
|
+
def like object
|
72
81
|
raise RecordNotRecommendableError unless object.recommendable?
|
73
82
|
return if likes? object
|
74
83
|
completely_unrecommend object
|
@@ -82,7 +91,7 @@ module Recommendable
|
|
82
91
|
#
|
83
92
|
# @param [Object] object the object you want to check
|
84
93
|
# @return true if self likes object, false if not
|
85
|
-
def likes?
|
94
|
+
def likes? object
|
86
95
|
likes.exists? :likeable_id => object.id, :likeable_type => object.class.to_s
|
87
96
|
end
|
88
97
|
|
@@ -90,7 +99,7 @@ module Recommendable
|
|
90
99
|
#
|
91
100
|
# @param [Object] object the object you want to remove from self's likes
|
92
101
|
# @return true if object is unliked, nil if nothing happened
|
93
|
-
def unlike
|
102
|
+
def unlike object
|
94
103
|
if likes.where(:likeable_id => object.id, :likeable_type => object.class.to_s).first.try(:destroy)
|
95
104
|
object.send :update_score
|
96
105
|
Resque.enqueue RecommendationRefresher, self.id
|
@@ -112,7 +121,7 @@ module Recommendable
|
|
112
121
|
#
|
113
122
|
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
114
123
|
# @return [Array] an array of ActiveRecord objects that self has liked belonging to klass
|
115
|
-
def liked_for
|
124
|
+
def liked_for klass
|
116
125
|
likes.where(:likeable_type => klass).includes(:likeable).map(&:likeable)
|
117
126
|
end
|
118
127
|
|
@@ -122,7 +131,7 @@ module Recommendable
|
|
122
131
|
# @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.
|
123
132
|
# @note You should not need to use this method. (see {#liked_for})
|
124
133
|
# @private
|
125
|
-
def likes_for
|
134
|
+
def likes_for klass
|
126
135
|
likes.where :likeable_type => klass.to_s.classify
|
127
136
|
end
|
128
137
|
end
|
@@ -136,7 +145,7 @@ module Recommendable
|
|
136
145
|
# @param [Object] object the object you want self to dislike.
|
137
146
|
# @return true if object has been disliked
|
138
147
|
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
139
|
-
def dislike
|
148
|
+
def dislike object
|
140
149
|
raise RecordNotRecommendableError unless object.recommendable?
|
141
150
|
return if dislikes? object
|
142
151
|
completely_unrecommend object
|
@@ -150,15 +159,15 @@ module Recommendable
|
|
150
159
|
#
|
151
160
|
# @param [Object] object the object you want to check
|
152
161
|
# @return true if self dislikes object, false if not
|
153
|
-
def dislikes?
|
154
|
-
dislikes.exists?
|
162
|
+
def dislikes? object
|
163
|
+
dislikes.exists? :dislikeable_id => object.id, :dislikeable_type => object.class.to_s
|
155
164
|
end
|
156
165
|
|
157
166
|
# Destroys a Recommendable::Dislike currently associating self with object
|
158
167
|
#
|
159
168
|
# @param [Object] object the object you want to remove from self's dislikes
|
160
169
|
# @return true if object is removed from self's dislikes, nil if nothing happened
|
161
|
-
def undislike
|
170
|
+
def undislike object
|
162
171
|
if dislikes.where(:dislikeable_id => object.id, :dislikeable_type => object.class.to_s).first.try(:destroy)
|
163
172
|
object.send :update_score
|
164
173
|
Resque.enqueue RecommendationRefresher, self.id
|
@@ -170,7 +179,7 @@ module Recommendable
|
|
170
179
|
|
171
180
|
# @return [Array] an array of ActiveRecord objects that self has disliked
|
172
181
|
def disliked
|
173
|
-
Recommendable.recommendable_classes.flat_map { |klass| disliked_for
|
182
|
+
Recommendable.recommendable_classes.flat_map { |klass| disliked_for klass }
|
174
183
|
end
|
175
184
|
|
176
185
|
private
|
@@ -180,7 +189,7 @@ module Recommendable
|
|
180
189
|
#
|
181
190
|
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
182
191
|
# @return [Array] an array of ActiveRecord objects that self has disliked belonging to klass
|
183
|
-
def disliked_for
|
192
|
+
def disliked_for klass
|
184
193
|
dislikes.where(:dislikeable_type => klass).includes(:dislikeable).map(&:dislikeable)
|
185
194
|
end
|
186
195
|
|
@@ -190,7 +199,7 @@ module Recommendable
|
|
190
199
|
# @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.
|
191
200
|
# @note You should not need to use this method. (see {#disliked_for})
|
192
201
|
# @private
|
193
|
-
def dislikes_for
|
202
|
+
def dislikes_for klass
|
194
203
|
dislikes.where :dislikeable_type => klass.to_s.classify
|
195
204
|
end
|
196
205
|
end
|
@@ -204,7 +213,7 @@ module Recommendable
|
|
204
213
|
# @param [Object] object the object you want self to stash.
|
205
214
|
# @return true if object has been stashed
|
206
215
|
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
207
|
-
def stash
|
216
|
+
def stash object
|
208
217
|
raise RecordNotRecommendableError unless object.recommendable?
|
209
218
|
return if rated?(object) || stashed?(object)
|
210
219
|
unignore object
|
@@ -217,7 +226,7 @@ module Recommendable
|
|
217
226
|
#
|
218
227
|
# @param [Object] object the object you want to check
|
219
228
|
# @return true if self has stashed object, false if not
|
220
|
-
def stashed?
|
229
|
+
def stashed? object
|
221
230
|
stashed_items.exists? :stashable_id => object.id, :stashable_type => object.class.to_s
|
222
231
|
end
|
223
232
|
|
@@ -225,7 +234,7 @@ module Recommendable
|
|
225
234
|
#
|
226
235
|
# @param [Object] object the object you want to remove from self's stash
|
227
236
|
# @return true if object is stashed, nil if nothing happened
|
228
|
-
def unstash
|
237
|
+
def unstash object
|
229
238
|
true if stashed_items.where(:stashable_id => object.id, :stashable_type => object.class.to_s).first.try(:destroy)
|
230
239
|
end
|
231
240
|
|
@@ -243,7 +252,7 @@ module Recommendable
|
|
243
252
|
#
|
244
253
|
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
245
254
|
# @return [Array] an array of ActiveRecord objects that self has stashed belonging to klass
|
246
|
-
def stashed_for
|
255
|
+
def stashed_for klass
|
247
256
|
stashed_items.where(:stashable_type => klass).includes(:stashable).map(&:stashable)
|
248
257
|
end
|
249
258
|
end
|
@@ -257,7 +266,7 @@ module Recommendable
|
|
257
266
|
# @param [Object] object the object you want self to ignore.
|
258
267
|
# @return true if object has been ignored
|
259
268
|
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
260
|
-
def ignore
|
269
|
+
def ignore object
|
261
270
|
raise RecordNotRecommendableError unless object.recommendable?
|
262
271
|
return if ignored? object
|
263
272
|
completely_unrecommend object
|
@@ -269,7 +278,7 @@ module Recommendable
|
|
269
278
|
#
|
270
279
|
# @param [Object] object the object you want to check
|
271
280
|
# @return true if self has ignored object, false if not
|
272
|
-
def ignored?
|
281
|
+
def ignored? object
|
273
282
|
ignores.exists? :ignoreable_id => object.id, :ignoreable_type => object.class.to_s
|
274
283
|
end
|
275
284
|
|
@@ -277,7 +286,7 @@ module Recommendable
|
|
277
286
|
#
|
278
287
|
# @param [Object] object the object you want to remove from self's ignores
|
279
288
|
# @return true if object is removed from self's ignores, nil if nothing happened
|
280
|
-
def unignore
|
289
|
+
def unignore object
|
281
290
|
true if ignores.where(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s).first.try(:destroy)
|
282
291
|
end
|
283
292
|
|
@@ -295,7 +304,7 @@ module Recommendable
|
|
295
304
|
#
|
296
305
|
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
297
306
|
# @return [Array] an array of ActiveRecord objects that self has ignored belonging to klass
|
298
|
-
def ignored_for
|
307
|
+
def ignored_for klass
|
299
308
|
ignores.where(:ignoreable_type => klass).includes(:ignoreable).map(&:ignoreable)
|
300
309
|
end
|
301
310
|
end
|
@@ -305,7 +314,7 @@ module Recommendable
|
|
305
314
|
#
|
306
315
|
# @param [Object] object the object you want to check
|
307
316
|
# @return true if self has liked or disliked object, false if not
|
308
|
-
def rated?
|
317
|
+
def rated? object
|
309
318
|
likes?(object) || dislikes?(object)
|
310
319
|
end
|
311
320
|
|
@@ -323,7 +332,7 @@ module Recommendable
|
|
323
332
|
# @param [Hash] options the options for this query
|
324
333
|
# @option options [Fixnum] :count (10) The number of raters to return
|
325
334
|
# @return [Array] An array of instances of your user class
|
326
|
-
def similar_raters
|
335
|
+
def similar_raters options = {}
|
327
336
|
defaults = { :count => 10 }
|
328
337
|
options = defaults.merge options
|
329
338
|
|
@@ -335,6 +344,30 @@ module Recommendable
|
|
335
344
|
rater_ids.index(x.id) <=> rater_ids.index(y.id)
|
336
345
|
end
|
337
346
|
end
|
347
|
+
|
348
|
+
def liked_in_common_with rater, options = {}
|
349
|
+
options.merge!({ :return_records => true })
|
350
|
+
create_recommended_to_sets and rater.create_recommended_to_sets
|
351
|
+
liked = common_likes_with rater, options
|
352
|
+
destroy_recommended_to_sets and rater.destroy_recommended_to_sets
|
353
|
+
return liked
|
354
|
+
end
|
355
|
+
|
356
|
+
def disliked_in_common_with rater, options = {}
|
357
|
+
options.merge!({ :return_records => true })
|
358
|
+
create_recommended_to_sets and rater.create_recommended_to_sets
|
359
|
+
disliked = common_dislikes_with rater, options
|
360
|
+
destroy_recommended_to_sets and rater.destroy_recommended_to_sets
|
361
|
+
return disliked
|
362
|
+
end
|
363
|
+
|
364
|
+
def disagreed_on_with rater, options = {}
|
365
|
+
options.merge!({ :return_records => true })
|
366
|
+
create_recommended_to_sets and rater.create_recommended_to_sets
|
367
|
+
disagreements = disagreements_with rater, options
|
368
|
+
destroy_recommended_to_sets and rater.destroy_recommended_to_sets
|
369
|
+
return disagreements
|
370
|
+
end
|
338
371
|
|
339
372
|
# Get a list of recommendations for self. The whole point of this gem!
|
340
373
|
# Recommendations are returned in a descending order with the first index
|
@@ -343,11 +376,11 @@ module Recommendable
|
|
343
376
|
# @param [Hash] options the options for returning this list
|
344
377
|
# @option options [Fixnum] :count (10) the number of recommendations to get
|
345
378
|
# @return [Array] an array of ActiveRecord objects that are recommendable
|
346
|
-
def recommendations
|
379
|
+
def recommendations options = {}
|
347
380
|
return [] if likes.count + dislikes.count == 0
|
348
381
|
|
349
382
|
unioned_predictions = "#{self.class}:#{id}:predictions"
|
350
|
-
Recommendable.redis.zunionstore unioned_predictions, Recommendable.recommendable_classes.map { |klass| predictions_set_for
|
383
|
+
Recommendable.redis.zunionstore unioned_predictions, Recommendable.recommendable_classes.map { |klass| predictions_set_for klass }
|
351
384
|
|
352
385
|
recommendations = Recommendable.redis.zrevrange(unioned_predictions, 0, 10).map do |object|
|
353
386
|
klass, id = object.split(":")
|
@@ -363,7 +396,7 @@ module Recommendable
|
|
363
396
|
#
|
364
397
|
# @param [Class, String, Symbol] klass the class to receive recommendations for. Can be the class constant, or a String/Symbol representation of the class name.
|
365
398
|
# @return [Array] an array of ActiveRecord objects that are recommendable
|
366
|
-
def recommended_for
|
399
|
+
def recommended_for klass
|
367
400
|
return [] if likes_for(klass).count + dislikes_for(klass).count == 0 || Recommendable.redis.zcard(predictions_set_for(klass)) == 0
|
368
401
|
|
369
402
|
recommendations = []
|
@@ -384,7 +417,7 @@ module Recommendable
|
|
384
417
|
#
|
385
418
|
# @param [Object] object the object to fetch the probability for
|
386
419
|
# @return [Float] the likelihood of self liking the passed object
|
387
|
-
def probability_of_liking
|
420
|
+
def probability_of_liking object
|
388
421
|
Recommendable.redis.zscore predictions_set_for(object.class), object.redis_key
|
389
422
|
end
|
390
423
|
|
@@ -394,10 +427,12 @@ module Recommendable
|
|
394
427
|
# @param [Object] object the object to fetch the probability for
|
395
428
|
# @return [Float] the likelihood of self disliking the passed object
|
396
429
|
# @see {#probability_of_liking}
|
397
|
-
def probability_of_disliking
|
430
|
+
def probability_of_disliking object
|
398
431
|
-probability_of_liking(object)
|
399
432
|
end
|
400
433
|
|
434
|
+
protected
|
435
|
+
|
401
436
|
# Makes a call to Redis and intersects the sets of likes belonging to self
|
402
437
|
# and rater.
|
403
438
|
#
|
@@ -406,11 +441,9 @@ module Recommendable
|
|
406
441
|
# @option options [Class, String, Symbol] :class ('nil') Restrict the intersection to a single recommendable type. By default, all recomendable types are considered
|
407
442
|
# @option options [true, false] :return_records (true) Return the actual Model instances
|
408
443
|
# @return [Array] Typically, an array of ActiveRecord objects (unless :return_records is false)
|
409
|
-
def common_likes_with
|
410
|
-
defaults = { :class => nil,
|
411
|
-
|
412
|
-
options = defaults.merge(options)
|
413
|
-
create_recommended_to_sets and rater.create_recommended_to_sets if options[:return_records]
|
444
|
+
def common_likes_with rater, options = {}
|
445
|
+
defaults = { :class => nil, :return_records => false }
|
446
|
+
options = defaults.merge options
|
414
447
|
|
415
448
|
if options[:class]
|
416
449
|
in_common = Recommendable.redis.sinter likes_set_for(options[:class]), rater.likes_set_for(options[:class])
|
@@ -420,14 +453,13 @@ module Recommendable
|
|
420
453
|
things = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
|
421
454
|
|
422
455
|
if options[:return_records]
|
423
|
-
klass.to_s.classify.constantize.find
|
456
|
+
klass.to_s.classify.constantize.find things
|
424
457
|
else
|
425
|
-
things.map {|id| "#{klass.to_s.classify}:#{id}"}
|
458
|
+
things.map { |id| "#{klass.to_s.classify}:#{id}" }
|
426
459
|
end
|
427
460
|
end
|
428
461
|
end
|
429
462
|
|
430
|
-
destroy_recommended_to_sets and rater.destroy_recommended_to_sets if options[:return_records]
|
431
463
|
in_common
|
432
464
|
end
|
433
465
|
|
@@ -439,11 +471,9 @@ module Recommendable
|
|
439
471
|
# @option options [Class, String, Symbol] :class ('nil') Restrict the intersection to a single recommendable type. By default, all recomendable types are considered
|
440
472
|
# @option options [true, false] :return_records (true) Return the actual Model instances
|
441
473
|
# @return [Array] Typically, an array of ActiveRecord objects (unless :return_records is false)
|
442
|
-
def common_dislikes_with
|
443
|
-
defaults = { :class => nil,
|
444
|
-
|
445
|
-
options = defaults.merge(options)
|
446
|
-
create_recommended_to_sets and rater.create_recommended_to_sets if options[:return_records]
|
474
|
+
def common_dislikes_with rater, options = {}
|
475
|
+
defaults = { :class => nil, :return_records => false }
|
476
|
+
options = defaults.merge options
|
447
477
|
|
448
478
|
if options[:class]
|
449
479
|
in_common = Recommendable.redis.sinter dislikes_set_for(options[:class]), rater.dislikes_set_for(options[:class])
|
@@ -455,12 +485,11 @@ module Recommendable
|
|
455
485
|
if options[:return_records]
|
456
486
|
klass.to_s.classify.constantize.find(things)
|
457
487
|
else
|
458
|
-
things.map {|id| "#{klass.to_s.classify}:#{id}"}
|
488
|
+
things.map { |id| "#{klass.to_s.classify}:#{id}" }
|
459
489
|
end
|
460
490
|
end
|
461
491
|
end
|
462
492
|
|
463
|
-
destroy_recommended_to_sets and rater.destroy_recommended_to_sets if options[:return_records]
|
464
493
|
in_common
|
465
494
|
end
|
466
495
|
|
@@ -474,11 +503,9 @@ module Recommendable
|
|
474
503
|
# @option options [Class, String, Symbol] :class ('nil') Restrict the intersections to a single recommendable type. By default, all recomendable types are considered
|
475
504
|
# @option options [true, false] :return_records (true) Return the actual Model instances
|
476
505
|
# @return [Array] Typically, an array of ActiveRecord objects (unless :return_records is false)
|
477
|
-
def disagreements_with
|
478
|
-
defaults = { :class => nil,
|
479
|
-
|
480
|
-
options = defaults.merge(options)
|
481
|
-
create_recommended_to_sets and rater.create_recommended_to_sets if options[:return_records]
|
506
|
+
def disagreements_with rater, options = {}
|
507
|
+
defaults = { :class => nil, :return_records => false }
|
508
|
+
options = defaults.merge options
|
482
509
|
|
483
510
|
if options[:class]
|
484
511
|
disagreements = Recommendable.redis.sinter(likes_set_for(options[:class]), rater.dislikes_set_for(options[:class]))
|
@@ -492,12 +519,11 @@ module Recommendable
|
|
492
519
|
if options[:return_records]
|
493
520
|
klass.to_s.classify.constantize.find(things)
|
494
521
|
else
|
495
|
-
things.map {|id| "#{options[:class].to_s.classify}:#{id}"}
|
522
|
+
things.map { |id| "#{options[:class].to_s.classify}:#{id}" }
|
496
523
|
end
|
497
524
|
end
|
498
525
|
end
|
499
526
|
|
500
|
-
destroy_recommended_to_sets and rater.destroy_recommended_to_sets if options[:return_records]
|
501
527
|
disagreements
|
502
528
|
end
|
503
529
|
|
@@ -508,23 +534,21 @@ module Recommendable
|
|
508
534
|
#
|
509
535
|
# param [Object] object the object to destroy Recommendable models for
|
510
536
|
# @private
|
511
|
-
def completely_unrecommend
|
512
|
-
unlike
|
513
|
-
undislike
|
514
|
-
unstash
|
515
|
-
unignore
|
516
|
-
unpredict
|
537
|
+
def completely_unrecommend object
|
538
|
+
unlike object
|
539
|
+
undislike object
|
540
|
+
unstash object
|
541
|
+
unignore object
|
542
|
+
unpredict object
|
517
543
|
end
|
518
544
|
|
519
|
-
protected
|
520
|
-
|
521
545
|
# @private
|
522
|
-
def likes_set_for
|
546
|
+
def likes_set_for klass
|
523
547
|
"#{self.class}:#{id}:likes:#{klass}"
|
524
548
|
end
|
525
549
|
|
526
550
|
# @private
|
527
|
-
def dislikes_set_for
|
551
|
+
def dislikes_set_for klass
|
528
552
|
"#{self.class}:#{id}:dislikes:#{klass}"
|
529
553
|
end
|
530
554
|
|
@@ -533,8 +557,8 @@ module Recommendable
|
|
533
557
|
# @private
|
534
558
|
def create_recommended_to_sets
|
535
559
|
Recommendable.recommendable_classes.each do |klass|
|
536
|
-
likes_for(klass).each {|like| Recommendable.redis.sadd likes_set_for(klass), like.likeable_id }
|
537
|
-
dislikes_for(klass).each {|dislike| Recommendable.redis.sadd dislikes_set_for(klass), dislike.dislikeable_id }
|
560
|
+
likes_for(klass).each { |like| Recommendable.redis.sadd likes_set_for(klass), like.likeable_id }
|
561
|
+
dislikes_for(klass).each { |dislike| Recommendable.redis.sadd dislikes_set_for(klass), dislike.dislikeable_id }
|
538
562
|
end
|
539
563
|
end
|
540
564
|
|
@@ -580,7 +604,7 @@ module Recommendable
|
|
580
604
|
#
|
581
605
|
# @private
|
582
606
|
def update_recommendations
|
583
|
-
Recommendable.recommendable_classes.each {|klass| update_recommendations_for klass}
|
607
|
+
Recommendable.recommendable_classes.each { |klass| update_recommendations_for klass }
|
584
608
|
end
|
585
609
|
|
586
610
|
# Used internally to update self's prediction values across a single
|
@@ -588,10 +612,10 @@ module Recommendable
|
|
588
612
|
#
|
589
613
|
# @param [Class] klass the recommendable type to update predictions for
|
590
614
|
# @private
|
591
|
-
def update_recommendations_for
|
615
|
+
def update_recommendations_for klass
|
592
616
|
klass.find_each do |object|
|
593
617
|
next if rated?(object) || !object.been_rated? || ignored?(object) || stashed?(object)
|
594
|
-
prediction = predict
|
618
|
+
prediction = predict object
|
595
619
|
Recommendable.redis.zadd(predictions_set_for(object.class), prediction, object.redis_key) if prediction
|
596
620
|
end
|
597
621
|
end
|
@@ -606,14 +630,14 @@ module Recommendable
|
|
606
630
|
# @param [Object] object the object to check the likeliness of liking
|
607
631
|
# @return [Float] the probability that self will like object
|
608
632
|
# @private
|
609
|
-
def predict
|
633
|
+
def predict object
|
610
634
|
liked_by, disliked_by = object.send :create_recommendable_sets
|
611
635
|
rated_by = Recommendable.redis.scard(liked_by) + Recommendable.redis.scard(disliked_by)
|
612
636
|
similarity_sum = 0.0
|
613
637
|
prediction = 0.0
|
614
638
|
|
615
|
-
Recommendable.redis.smembers(liked_by).inject(similarity_sum) {|sum, r| sum += Recommendable.redis.zscore(similarity_set, r).to_f }
|
616
|
-
Recommendable.redis.smembers(disliked_by).inject(similarity_sum) {|sum, r| sum -= Recommendable.redis.zscore(similarity_set, r).to_f }
|
639
|
+
Recommendable.redis.smembers(liked_by).inject(similarity_sum) { |sum, r| sum += Recommendable.redis.zscore(similarity_set, r).to_f }
|
640
|
+
Recommendable.redis.smembers(disliked_by).inject(similarity_sum) { |sum, r| sum -= Recommendable.redis.zscore(similarity_set, r).to_f }
|
617
641
|
|
618
642
|
prediction = similarity_sum / rated_by.to_f
|
619
643
|
|
@@ -657,7 +681,7 @@ module Recommendable
|
|
657
681
|
end
|
658
682
|
|
659
683
|
# @private
|
660
|
-
def unpredict
|
684
|
+
def unpredict object
|
661
685
|
Recommendable.redis.zrem predictions_set_for(object.class), object.redis_key
|
662
686
|
end
|
663
687
|
|
@@ -667,7 +691,7 @@ module Recommendable
|
|
667
691
|
end
|
668
692
|
|
669
693
|
# @private
|
670
|
-
def predictions_set_for
|
694
|
+
def predictions_set_for klass
|
671
695
|
"#{self.class}:#{id}:predictions:#{klass}"
|
672
696
|
end
|
673
697
|
end
|
data/lib/recommendable/engine.rb
CHANGED
data/spec/models/user_spec.rb
CHANGED
@@ -254,6 +254,36 @@ class UserSpec < MiniTest::Spec
|
|
254
254
|
Recommendable.redis.del "User:#{@frank.id}:similarities"
|
255
255
|
Recommendable.redis.del "User:#{@frank.id}:predictions:Movie"
|
256
256
|
end
|
257
|
+
|
258
|
+
it "should have common likes with a friend" do
|
259
|
+
@dave.like @movie1
|
260
|
+
@dave.like @movie2
|
261
|
+
@dave.like @movie4
|
262
|
+
|
263
|
+
@frank.like @movie2
|
264
|
+
@frank.like @movie3
|
265
|
+
@frank.like @movie4
|
266
|
+
|
267
|
+
@dave.liked_movies_in_common_with(@frank).must_include @movie2
|
268
|
+
@dave.liked_movies_in_common_with(@frank).must_include @movie4
|
269
|
+
@dave.liked_movies_in_common_with(@frank).wont_include @movie1
|
270
|
+
@dave.liked_movies_in_common_with(@frank).wont_include @movie3
|
271
|
+
end
|
272
|
+
|
273
|
+
it "should have common dislikes with a friend" do
|
274
|
+
@dave.dislike @movie1
|
275
|
+
@dave.dislike @movie3
|
276
|
+
@dave.like @movie4
|
277
|
+
|
278
|
+
@frank.dislike @movie2
|
279
|
+
@frank.dislike @movie3
|
280
|
+
@frank.dislike @movie4
|
281
|
+
|
282
|
+
@dave.disliked_movies_in_common_with(@frank).wont_include @movie2
|
283
|
+
@dave.disliked_movies_in_common_with(@frank).wont_include @movie4
|
284
|
+
@dave.disliked_movies_in_common_with(@frank).wont_include @movie1
|
285
|
+
@dave.disliked_movies_in_common_with(@frank).must_include @movie3
|
286
|
+
end
|
257
287
|
|
258
288
|
it "should get populated sorted sets for similarities and recommendations" do
|
259
289
|
@dave.like(@movie1)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: recommendable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-04-
|
12
|
+
date: 2012-04-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sqlite3
|
16
|
-
requirement: &
|
16
|
+
requirement: &70124007208240 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70124007208240
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: minitest
|
27
|
-
requirement: &
|
27
|
+
requirement: &70124007206840 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70124007206840
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: shoulda
|
38
|
-
requirement: &
|
38
|
+
requirement: &70124007205260 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70124007205260
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: yard
|
49
|
-
requirement: &
|
49
|
+
requirement: &70124007203840 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 0.6.0
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70124007203840
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: bundler
|
60
|
-
requirement: &
|
60
|
+
requirement: &70124007201400 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 1.0.0
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70124007201400
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: jeweler
|
71
|
-
requirement: &
|
71
|
+
requirement: &70124007200440 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ~>
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: 1.6.4
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70124007200440
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: rcov
|
82
|
-
requirement: &
|
82
|
+
requirement: &70124007199640 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,10 +87,10 @@ dependencies:
|
|
87
87
|
version: '0'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *70124007199640
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: rails
|
93
|
-
requirement: &
|
93
|
+
requirement: &70124007198320 !ruby/object:Gem::Requirement
|
94
94
|
none: false
|
95
95
|
requirements:
|
96
96
|
- - ! '>='
|
@@ -98,10 +98,10 @@ dependencies:
|
|
98
98
|
version: 3.0.0
|
99
99
|
type: :runtime
|
100
100
|
prerelease: false
|
101
|
-
version_requirements: *
|
101
|
+
version_requirements: *70124007198320
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
103
|
name: redis
|
104
|
-
requirement: &
|
104
|
+
requirement: &70124007196840 !ruby/object:Gem::Requirement
|
105
105
|
none: false
|
106
106
|
requirements:
|
107
107
|
- - ~>
|
@@ -109,10 +109,10 @@ dependencies:
|
|
109
109
|
version: 2.2.0
|
110
110
|
type: :runtime
|
111
111
|
prerelease: false
|
112
|
-
version_requirements: *
|
112
|
+
version_requirements: *70124007196840
|
113
113
|
- !ruby/object:Gem::Dependency
|
114
114
|
name: resque
|
115
|
-
requirement: &
|
115
|
+
requirement: &70124007195680 !ruby/object:Gem::Requirement
|
116
116
|
none: false
|
117
117
|
requirements:
|
118
118
|
- - ! '>='
|
@@ -120,10 +120,10 @@ dependencies:
|
|
120
120
|
version: 1.19.0
|
121
121
|
type: :runtime
|
122
122
|
prerelease: false
|
123
|
-
version_requirements: *
|
123
|
+
version_requirements: *70124007195680
|
124
124
|
- !ruby/object:Gem::Dependency
|
125
125
|
name: resque-loner
|
126
|
-
requirement: &
|
126
|
+
requirement: &70124007194380 !ruby/object:Gem::Requirement
|
127
127
|
none: false
|
128
128
|
requirements:
|
129
129
|
- - ~>
|
@@ -131,7 +131,7 @@ dependencies:
|
|
131
131
|
version: 1.2.0
|
132
132
|
type: :runtime
|
133
133
|
prerelease: false
|
134
|
-
version_requirements: *
|
134
|
+
version_requirements: *70124007194380
|
135
135
|
description: Allow a model (typically User) to Like and/or Dislike models in your
|
136
136
|
app. Generate recommendations quickly using redis.
|
137
137
|
email: david@davidcelis.com
|