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 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.3)
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
- sinatra (1.3.2)
87
- rack (~> 1.3, >= 1.3.6)
88
- rack-protection (~> 1.2)
89
- tilt (~> 1.3, >= 1.3.3)
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
- # You can leave this commented out.
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(count=1)
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(ids)
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 = 1.0 * likes.count / n.to_f
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(*things)
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(method, *args, &block)
32
- if method.to_s =~ /(liked|disliked|ignored|stashed|recommended)_(.+)/
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?(method)
46
- if method.to_s =~ /(liked|disliked|ignored|stashed|recommended)_(.+)/
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(object)
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?(object)
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(object)
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(klass)
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(klass)
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(object)
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?(object)
154
- dislikes.exists?(:dislikeable_id => object.id, :dislikeable_type => object.class.to_s)
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(object)
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(klass) }
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(klass)
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(klass)
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(object)
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?(object)
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(object)
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(klass)
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(object)
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?(object)
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(object)
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(klass)
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?(object)
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(options = {})
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(options = {})
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(klass) }
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(klass)
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(object)
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(object)
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(rater, options = {})
410
- defaults = { :class => nil,
411
- :return_records => true }
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(things)
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(rater, options = {})
443
- defaults = { :class => nil,
444
- :return_records => true }
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(rater, options = {})
478
- defaults = { :class => nil,
479
- :return_records => true }
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(object)
512
- unlike(object)
513
- undislike(object)
514
- unstash(object)
515
- unignore(object)
516
- unpredict(object)
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(klass)
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(klass)
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(klass)
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(object)
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(object)
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(object)
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(klass)
694
+ def predictions_set_for klass
671
695
  "#{self.class}:#{id}:predictions:#{klass}"
672
696
  end
673
697
  end
@@ -7,7 +7,7 @@ module Recommendable
7
7
  attr_accessor :root
8
8
 
9
9
  def root
10
- @root ||= Pathname.new(File.expand_path('../../', __FILE__))
10
+ @root ||= Pathname.new File.expand_path('../../', __FILE__)
11
11
  end
12
12
  end
13
13
  end
@@ -1,3 +1,3 @@
1
1
  module Recommendable
2
- VERSION = '0.1.6'
2
+ VERSION = '0.1.8'
3
3
  end
@@ -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.6
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-04 00:00:00.000000000 Z
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: &70234153626020 !ruby/object:Gem::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: *70234153626020
24
+ version_requirements: *70124007208240
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: minitest
27
- requirement: &70234153623920 !ruby/object:Gem::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: *70234153623920
35
+ version_requirements: *70124007206840
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: shoulda
38
- requirement: &70234153622740 !ruby/object:Gem::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: *70234153622740
46
+ version_requirements: *70124007205260
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: yard
49
- requirement: &70234153621620 !ruby/object:Gem::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: *70234153621620
57
+ version_requirements: *70124007203840
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: bundler
60
- requirement: &70234153619880 !ruby/object:Gem::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: *70234153619880
68
+ version_requirements: *70124007201400
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: jeweler
71
- requirement: &70234153618120 !ruby/object:Gem::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: *70234153618120
79
+ version_requirements: *70124007200440
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: rcov
82
- requirement: &70234153616840 !ruby/object:Gem::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: *70234153616840
90
+ version_requirements: *70124007199640
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: rails
93
- requirement: &70234153616140 !ruby/object:Gem::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: *70234153616140
101
+ version_requirements: *70124007198320
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: redis
104
- requirement: &70234153615260 !ruby/object:Gem::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: *70234153615260
112
+ version_requirements: *70124007196840
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: resque
115
- requirement: &70234153613760 !ruby/object:Gem::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: *70234153613760
123
+ version_requirements: *70124007195680
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: resque-loner
126
- requirement: &70234153611760 !ruby/object:Gem::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: *70234153611760
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