recommendable 0.1.6 → 0.1.8
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/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
|