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 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