recommendable 1.1.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data/lib/recommendable.rb +38 -26
  2. data/lib/recommendable/configuration.rb +47 -0
  3. data/lib/recommendable/helpers.rb +3 -9
  4. data/lib/recommendable/helpers/calculations.rb +150 -0
  5. data/lib/recommendable/helpers/queriers.rb +23 -0
  6. data/lib/recommendable/helpers/redis_key_mapper.rb +29 -0
  7. data/lib/recommendable/orm/active_record.rb +6 -0
  8. data/lib/recommendable/orm/data_mapper.rb +7 -0
  9. data/lib/recommendable/orm/mongo_mapper.rb +8 -0
  10. data/lib/recommendable/orm/mongoid.rb +7 -0
  11. data/lib/recommendable/ratable.rb +83 -0
  12. data/lib/recommendable/ratable/dislikable.rb +26 -0
  13. data/lib/recommendable/ratable/likable.rb +26 -0
  14. data/lib/recommendable/rater.rb +109 -0
  15. data/lib/recommendable/rater/bookmarker.rb +120 -0
  16. data/lib/recommendable/rater/disliker.rb +122 -0
  17. data/lib/recommendable/rater/hider.rb +120 -0
  18. data/lib/recommendable/rater/liker.rb +122 -0
  19. data/lib/recommendable/rater/recommender.rb +68 -0
  20. data/lib/recommendable/version.rb +5 -4
  21. data/lib/recommendable/workers/delayed_job.rb +16 -0
  22. data/lib/recommendable/workers/rails.rb +16 -0
  23. data/lib/recommendable/workers/resque.rb +13 -0
  24. data/lib/recommendable/workers/sidekiq.rb +13 -0
  25. metadata +62 -131
  26. data/.gitignore +0 -57
  27. data/.travis.yml +0 -3
  28. data/CHANGELOG.markdown +0 -159
  29. data/Gemfile +0 -3
  30. data/Gemfile.lock +0 -112
  31. data/LICENSE.txt +0 -22
  32. data/README.markdown +0 -135
  33. data/Rakefile +0 -26
  34. data/TODO +0 -7
  35. data/app/models/recommendable/dislike.rb +0 -19
  36. data/app/models/recommendable/ignore.rb +0 -19
  37. data/app/models/recommendable/like.rb +0 -19
  38. data/app/models/recommendable/stash.rb +0 -19
  39. data/app/workers/recommendable/delayed_job_worker.rb +0 -17
  40. data/app/workers/recommendable/rails_worker.rb +0 -17
  41. data/app/workers/recommendable/resque_worker.rb +0 -14
  42. data/app/workers/recommendable/sidekiq_worker.rb +0 -14
  43. data/config/routes.rb +0 -3
  44. data/db/migrate/20120124193723_create_likes.rb +0 -17
  45. data/db/migrate/20120124193728_create_dislikes.rb +0 -17
  46. data/db/migrate/20120127092558_create_ignores.rb +0 -17
  47. data/db/migrate/20120131173909_create_stashes.rb +0 -17
  48. data/lib/generators/recommendable/USAGE +0 -8
  49. data/lib/generators/recommendable/install_generator.rb +0 -40
  50. data/lib/generators/recommendable/templates/initializer.rb +0 -28
  51. data/lib/recommendable/acts_as_recommendable.rb +0 -176
  52. data/lib/recommendable/acts_as_recommended_to.rb +0 -774
  53. data/lib/recommendable/engine.rb +0 -14
  54. data/lib/recommendable/exceptions.rb +0 -4
  55. data/lib/recommendable/railtie.rb +0 -6
  56. data/lib/sidekiq/middleware/client/unique_jobs.rb +0 -37
  57. data/lib/sidekiq/middleware/server/unique_jobs.rb +0 -17
  58. data/lib/tasks/recommendable_tasks.rake +0 -1
  59. data/recommendable.gemspec +0 -30
  60. data/script/rails +0 -8
  61. data/spec/configuration_spec.rb +0 -9
  62. data/spec/dummy/README.rdoc +0 -261
  63. data/spec/dummy/Rakefile +0 -7
  64. data/spec/dummy/app/assets/javascripts/application.js +0 -15
  65. data/spec/dummy/app/assets/stylesheets/application.css +0 -13
  66. data/spec/dummy/app/controllers/application_controller.rb +0 -3
  67. data/spec/dummy/app/helpers/application_helper.rb +0 -2
  68. data/spec/dummy/app/mailers/.gitkeep +0 -0
  69. data/spec/dummy/app/models/.gitkeep +0 -0
  70. data/spec/dummy/app/models/bully.rb +0 -2
  71. data/spec/dummy/app/models/movie.rb +0 -2
  72. data/spec/dummy/app/models/php_framework.rb +0 -2
  73. data/spec/dummy/app/models/user.rb +0 -3
  74. data/spec/dummy/app/views/layouts/application.html.erb +0 -14
  75. data/spec/dummy/config.ru +0 -4
  76. data/spec/dummy/config/application.rb +0 -56
  77. data/spec/dummy/config/boot.rb +0 -10
  78. data/spec/dummy/config/database.yml +0 -25
  79. data/spec/dummy/config/environment.rb +0 -5
  80. data/spec/dummy/config/environments/development.rb +0 -37
  81. data/spec/dummy/config/environments/production.rb +0 -67
  82. data/spec/dummy/config/environments/test.rb +0 -37
  83. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
  84. data/spec/dummy/config/initializers/inflections.rb +0 -15
  85. data/spec/dummy/config/initializers/mime_types.rb +0 -5
  86. data/spec/dummy/config/initializers/recommendable.rb +0 -14
  87. data/spec/dummy/config/initializers/secret_token.rb +0 -7
  88. data/spec/dummy/config/initializers/session_store.rb +0 -8
  89. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
  90. data/spec/dummy/config/locales/en.yml +0 -5
  91. data/spec/dummy/config/routes.rb +0 -4
  92. data/spec/dummy/db/migrate/20120128005553_create_likes.recommendable.rb +0 -18
  93. data/spec/dummy/db/migrate/20120128005554_create_dislikes.recommendable.rb +0 -18
  94. data/spec/dummy/db/migrate/20120128005555_create_ignores.recommendable.rb +0 -18
  95. data/spec/dummy/db/migrate/20120128020228_create_users.rb +0 -9
  96. data/spec/dummy/db/migrate/20120128020413_create_movies.rb +0 -10
  97. data/spec/dummy/db/migrate/20120128024632_create_php_frameworks.rb +0 -9
  98. data/spec/dummy/db/migrate/20120128024804_create_bullies.rb +0 -9
  99. data/spec/dummy/db/migrate/20120131195416_create_stashes.recommendable.rb +0 -19
  100. data/spec/dummy/db/schema.rb +0 -89
  101. data/spec/dummy/lib/assets/.gitkeep +0 -0
  102. data/spec/dummy/log/.gitkeep +0 -0
  103. data/spec/dummy/public/404.html +0 -26
  104. data/spec/dummy/public/422.html +0 -26
  105. data/spec/dummy/public/500.html +0 -25
  106. data/spec/dummy/public/favicon.ico +0 -0
  107. data/spec/dummy/recommendable_dummy_development +0 -0
  108. data/spec/dummy/recommendable_dummy_test +0 -0
  109. data/spec/dummy/script/rails +0 -6
  110. data/spec/factories.rb +0 -16
  111. data/spec/models/dislike_spec.rb +0 -41
  112. data/spec/models/ignore_spec.rb +0 -27
  113. data/spec/models/like_spec.rb +0 -42
  114. data/spec/models/movie_spec.rb +0 -82
  115. data/spec/models/stash_spec.rb +0 -27
  116. data/spec/models/user_benchmark_spec.rb +0 -49
  117. data/spec/models/user_spec.rb +0 -443
  118. data/spec/spec_helper.rb +0 -28
@@ -1,774 +0,0 @@
1
- require 'active_support/concern'
2
-
3
- module Recommendable
4
- module ActsAsRecommendedTo
5
- include Recommendable::Helpers
6
- extend ActiveSupport::Concern
7
-
8
- module ClassMethods
9
- def recommends *things
10
- acts_as_recommended_to
11
- things.each { |thing| thing.to_s.classify.constantize.acts_as_recommendable }
12
- end
13
-
14
- def acts_as_recommended_to
15
- class_eval do
16
- Recommendable.user_class = self
17
-
18
- has_many :likes, :class_name => "Recommendable::Like", :dependent => :destroy, :foreign_key => :user_id
19
- has_many :dislikes, :class_name => "Recommendable::Dislike", :dependent => :destroy, :foreign_key => :user_id
20
- has_many :ignores, :class_name => "Recommendable::Ignore", :dependent => :destroy, :foreign_key => :user_id
21
- has_many :stashed_items, :class_name => "Recommendable::Stash", :dependent => :destroy, :foreign_key => :user_id
22
-
23
- include LikeMethods
24
- include DislikeMethods
25
- include StashMethods
26
- include IgnoreMethods
27
- include RecommendationMethods
28
- include Hooks
29
-
30
- before_destroy :remove_from_similarities, :remove_recommendations
31
-
32
- define_hooks :before_like, :after_like, :before_unlike, :after_unlike,
33
- :before_dislike, :after_dislike, :before_undislike, :after_undislike,
34
- :before_stash, :after_stash, :before_unstash, :after_unstash,
35
- :before_ignore, :after_ignore, :before_unignore, :after_unignore
36
-
37
- %w(like dislike ignore).each do |action|
38
- send "before_#{action}", lambda { |obj| completely_unrecommend obj }
39
- end
40
-
41
- %w(like unlike dislike undislike).each do |action|
42
- send "after_#{action}", lambda { |obj|
43
- obj.send(:update_score)
44
- obj.send "update_#{action.gsub('un', '')}_count"
45
- Recommendable.enqueue(self.id)
46
- }
47
- end
48
-
49
- before_stash { |obj| unignore(obj) and unpredict(obj) }
50
-
51
- def method_missing method, *args, &block
52
- if method.to_s =~ /^(liked|disliked)_(.+)_in_common_with$/
53
- begin
54
- super unless $2.classify.constantize.acts_as_recommendable?
55
-
56
- self.send "#{$1}_in_common_with", *args, { :class => $2.classify.constantize }
57
- rescue NameError
58
- super
59
- end
60
- elsif method.to_s =~ /^(liked|disliked|ignored|stashed|recommended)_(.+)$/
61
- begin
62
- super unless $2.classify.constantize.acts_as_recommendable?
63
-
64
- self.send "#{$1}_for", $2.classify.constantize, *args
65
- rescue NameError
66
- super
67
- end
68
- else
69
- super
70
- end
71
- end
72
-
73
- def respond_to? method, include_private = false
74
- if method.to_s =~ /^(liked|disliked|ignored|stashed|recommended)_(.+)$/ || method.to_s =~ /^common_(liked|disliked)_(.+)_with$/
75
- begin
76
- $2.classify.constantize.acts_as_recommendable?
77
- rescue NameError
78
- false
79
- end
80
- else
81
- super
82
- end
83
- end
84
-
85
- private :likes, :dislikes, :ignores, :stashed_items
86
- end
87
- end
88
- end
89
-
90
- module LikeMethods
91
- # Creates a Recommendable::Like to associate self to a passed object. If
92
- # self is currently found to have disliked object, the corresponding
93
- # Recommendable::Dislike will be destroyed. It will also be removed from
94
- # the user's stash or ignores.
95
- #
96
- # @param [Object] object the object you want self to like.
97
- # @return true if object has been liked
98
- # @raise [UnrecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
99
- def like object
100
- raise UnrecommendableError unless object.recommendable?
101
- return if likes? object
102
-
103
- run_hook :before_like, object
104
- likes.create! :likeable_id => object.id, :likeable_type => object.class
105
- run_hook :after_like, object
106
-
107
- true
108
- end
109
-
110
- # Checks to see if self has already liked a passed object.
111
- #
112
- # @param [Object] object the object you want to check
113
- # @return true if self likes object, false if not
114
- def likes? object
115
- likes.exists? :likeable_id => object.id, :likeable_type => object.class.base_class.to_s
116
- end
117
-
118
- # Destroys a Recommendable::Like currently associating self with object
119
- #
120
- # @param [Object] object the object you want to remove from self's likes
121
- # @return true if object is unliked, nil if nothing happened
122
- def unlike object
123
- like = likes.where(:likeable_id => object.id, :likeable_type => object.class.base_class.to_s)
124
- if like.exists?
125
- run_hook :before_unlike, object
126
- like.first.destroy
127
- run_hook :after_unlike, object
128
- true
129
- end
130
- end
131
-
132
- # Get a list of records that self currently likes
133
-
134
- # @return [Array] an array of ActiveRecord objects that self has liked
135
- def liked
136
- Recommendable.recommendable_classes.map { |klass| liked_for klass }.flatten
137
- end
138
-
139
- private
140
-
141
- # Get a list of records belonging to a passed class that self currently
142
- # likes.
143
- #
144
- # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
145
- # @return [ActiveRecord::Relation] an ActiveRecord::Relation of records that self has liked
146
- def liked_for klass
147
- ids = if klass.sti?
148
- likes.joins(manual_join(klass, 'like')).map(&:likeable_id)
149
- else
150
- likes.where(:likeable_type => klass.to_s).map(&:likeable_id)
151
- end
152
-
153
- klass.where('ID IN (?)', ids)
154
- end
155
-
156
- # Get a list of Recommendable::Likes with a `#likeable_type` of the passed
157
- # class.
158
- #
159
- # @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.
160
- # @note You should not need to use this method. (see {#liked_for})
161
- # @private
162
- def likes_for klass
163
- if klass.sti?
164
- likes.joins manual_join(klass, 'like')
165
- else
166
- likes.where(:likeable_type => klass.to_s)
167
- end
168
- end
169
- end
170
-
171
- module DislikeMethods
172
- # Creates a Recommendable::Dislike to associate self to a passed object. If
173
- # self is currently found to have liked object, the corresponding
174
- # Recommendable::Like will be destroyed. It will also be removed from the
175
- # user's stash or list of ignores.
176
- #
177
- # @param [Object] object the object you want self to dislike.
178
- # @return true if object has been disliked
179
- # @raise [UnrecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
180
- def dislike object
181
- raise UnrecommendableError unless object.recommendable?
182
- return if dislikes? object
183
-
184
- run_hook :before_dislike, object
185
- dislikes.create! :dislikeable_id => object.id, :dislikeable_type => object.class
186
- run_hook :after_dislike, object
187
-
188
- true
189
- end
190
-
191
- # Checks to see if self has already disliked a passed object.
192
- #
193
- # @param [Object] object the object you want to check
194
- # @return true if self dislikes object, false if not
195
- def dislikes? object
196
- dislikes.exists? :dislikeable_id => object.id, :dislikeable_type => object.class.base_class.to_s
197
- end
198
-
199
- # Destroys a Recommendable::Dislike currently associating self with object
200
- #
201
- # @param [Object] object the object you want to remove from self's dislikes
202
- # @return true if object is removed from self's dislikes, nil if nothing happened
203
- def undislike object
204
- dislike = dislikes.where(:dislikeable_id => object.id, :dislikeable_type => object.class.base_class.to_s)
205
- if dislike.exists?
206
- run_hook :before_undislike, object
207
- dislike.first.destroy
208
- run_hook :after_undislike, object
209
- true
210
- end
211
- end
212
-
213
- # Get a list of records that self currently dislikes
214
-
215
- # @return [Array] an array of ActiveRecord objects that self has disliked
216
- def disliked
217
- Recommendable.recommendable_classes.map { |klass| disliked_for klass }.flatten
218
- end
219
-
220
- private
221
-
222
- # Get a list of records belonging to a passed class that self currently
223
- # dislikes.
224
- #
225
- # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
226
- # @return [ActiveRecord::Relation] an ActiveRecord::Relation of records that self has disliked
227
- def disliked_for klass
228
- ids = if klass.sti?
229
- dislikes.joins(manual_join(klass, 'dislike')).map(&:dislikeable_id)
230
- else
231
- dislikes.where(:dislikeable_type => klass.to_s).map(&:dislikeable_id)
232
- end
233
-
234
- klass.where('ID IN (?)', ids)
235
- end
236
-
237
- # Get a list of Recommendable::Dislikes with a `#dislikeable_type` of the
238
- # passed class.
239
- #
240
- # @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.
241
- # @note You should not need to use this method. (see {#disliked_for})
242
- # @private
243
- def dislikes_for klass
244
- if klass.sti?
245
- dislikes.joins manual_join(klass, 'dislike')
246
- else
247
- dislikes.where(:dislikeable_type => klass.to_s)
248
- end
249
- end
250
- end
251
-
252
- module StashMethods
253
- # Creates a Recommendable::Stash to associate self to a passed object.
254
- # This will remove the item from this user's recommendations.
255
- # If self is currently found to have liked or disliked the object, nothing
256
- # will happen. It will, however, be unignored.
257
- #
258
- # @param [Object] object the object you want self to stash.
259
- # @return true if object has been stashed
260
- # @raise [UnrecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
261
- def stash object
262
- raise UnrecommendableError unless object.recommendable?
263
- return if rated?(object) || stashed?(object)
264
-
265
- run_hook :before_stash, object
266
- stashed_items.create! :stashable_id => object.id, :stashable_type => object.class
267
- run_hook :after_stash, object
268
-
269
- true
270
- end
271
-
272
- # Checks to see if self has already stashed a passed object for later.
273
- #
274
- # @param [Object] object the object you want to check
275
- # @return true if self has stashed object, false if not
276
- def stashed? object
277
- stashed_items.exists? :stashable_id => object.id, :stashable_type => object.class.base_class.to_s
278
- end
279
-
280
- # Destroys a Recommendable::Stash currently associating self with object
281
- #
282
- # @param [Object] object the object you want to remove from self's stash
283
- # @return true if object is stashed, nil if nothing happened
284
- def unstash object
285
- stash = stashed_items.where(:stashable_id => object.id, :stashable_type => object.class.base_class.to_s)
286
- if stash.exists?
287
- run_hook :before_unstash, object
288
- stash.first.destroy
289
- run_hook :after_unstash, object
290
- true
291
- end
292
- end
293
-
294
- # Get a list of records that self has currently stashed for later
295
- #
296
- # @return [Array] an array of ActiveRecord objects that self has stashed
297
- def stashed
298
- Recommendable.recommendable_classes.map { |klass| stashed_for klass }.flatten
299
- end
300
-
301
- private
302
-
303
- # Get a list of records belonging to a passed class that self currently
304
- # has stashed away for later.
305
- #
306
- # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
307
- # @return [ActiveRecord::Relation] an ActiveRecord::Relation of records that self has stashed
308
- def stashed_for klass
309
- ids = if klass.sti?
310
- stashed_items.joins(manual_join(klass, 'stash')).map(&:stashable_id)
311
- else
312
- stashed_items.where(:stashable_type => klass.to_s).map(&:stashable_id)
313
- end
314
-
315
- klass.where('ID IN (?)', ids)
316
- end
317
- end
318
-
319
- module IgnoreMethods
320
- # Creates a Recommendable::Ignore to associate self to a passed object. If
321
- # self is currently found to have liked or dislikedobject, the
322
- # corresponding Recommendable::Like or Recommendable::Dislike will be
323
- # destroyed.
324
- #
325
- # @param [Object] object the object you want self to ignore.
326
- # @return true if object has been ignored
327
- # @raise [UnrecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
328
- def ignore object
329
- raise UnrecommendableError unless object.recommendable?
330
- return if ignored? object
331
-
332
- run_hook :before_ignore, object
333
- ignores.create! :ignorable_id => object.id, :ignorable_type => object.class
334
- run_hook :after_ignore, object
335
-
336
- true
337
- end
338
-
339
- # Checks to see if self has already ignored a passed object.
340
- #
341
- # @param [Object] object the object you want to check
342
- # @return true if self has ignored object, false if not
343
- def ignored? object
344
- ignores.exists? :ignorable_id => object.id, :ignorable_type => object.class.base_class.to_s
345
- end
346
-
347
- # Destroys a Recommendable::Ignore currently associating self with object
348
- #
349
- # @param [Object] object the object you want to remove from self's ignores
350
- # @return true if object is removed from self's ignores, nil if nothing happened
351
- def unignore object
352
- ignore = ignores.where(:ignorable_id => object.id, :ignorable_type => object.class.base_class.to_s)
353
- if ignore.exists?
354
- run_hook :before_unignore, object
355
- ignore.first.destroy
356
- run_hook :after_unignore, object
357
- true
358
- end
359
- end
360
-
361
- # Get a list of records that self is currently ignoring
362
-
363
- # @return [Array] an array of ActiveRecord objects that self has ignored
364
- def ignored
365
- Recommendable.recommendable_classes.map { |klass| ignored_for klass }.flatten
366
- end
367
-
368
- private
369
-
370
- # Get a list of records belonging to a passed class that self is
371
- # currently ignoring.
372
- #
373
- # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
374
- # @return [ActiveRecord::Relation] an ActiveRecord::Relation of records that self has ignored
375
- def ignored_for klass
376
- ids = if klass.sti?
377
- ignores.joins(manual_join(klass, 'ignore')).map(&:ignorable_id)
378
- else
379
- ignores.where(:ignorable_type => klass.to_s).map(&:ignorable_id)
380
- end
381
-
382
- klass.where('ID IN (?)', ids)
383
- end
384
- end
385
-
386
- module RecommendationMethods
387
- # Checks to see if self has already liked or disliked a passed object.
388
- #
389
- # @param [Object] object the object you want to check
390
- # @return true if self has liked or disliked object, false if not
391
- def rated? object
392
- likes?(object) || dislikes?(object)
393
- end
394
-
395
- # Checks to see if self has liked or disliked any objects yet.
396
- #
397
- # @return true if self has liked or disliked anything, false if not
398
- def rated_anything?
399
- likes.count > 0 || dislikes.count > 0
400
- end
401
-
402
- # Get a list of raters that have been found to be the most similar to
403
- # self. They are sorted in a descending fashion with the most similar
404
- # rater in the first index.
405
- #
406
- # @param [Hash] options the options for this query
407
- # @option options [Fixnum] :count (10) The number of raters to return
408
- # @return [Array] An array of instances of your user class
409
- def similar_raters options = {}
410
- defaults = { :count => 10 }
411
- options = defaults.merge options
412
-
413
- rater_ids = Recommendable.redis.zrevrange(similarity_set, 0, options[:count] - 1).map(&:to_i)
414
- raters = Recommendable.user_class.find rater_ids
415
-
416
- # The query loses the ordering, so...
417
- return raters.sort_by { |rater| rater_ids.index(rater.id) }
418
- end
419
-
420
- def liked_in_common_with rater, options = {}
421
- options.merge!({ :return_records => true })
422
- create_recommended_to_sets and rater.create_recommended_to_sets
423
- liked = common_likes_with rater, options
424
- destroy_recommended_to_sets and rater.destroy_recommended_to_sets
425
- return liked
426
- end
427
-
428
- def disliked_in_common_with rater, options = {}
429
- options.merge!({ :return_records => true })
430
- create_recommended_to_sets and rater.create_recommended_to_sets
431
- disliked = common_dislikes_with rater, options
432
- destroy_recommended_to_sets and rater.destroy_recommended_to_sets
433
- return disliked
434
- end
435
-
436
- def disagreed_on_with rater, options = {}
437
- options.merge!({ :return_records => true })
438
- create_recommended_to_sets and rater.create_recommended_to_sets
439
- disagreements = disagreements_with rater, options
440
- destroy_recommended_to_sets and rater.destroy_recommended_to_sets
441
- return disagreements
442
- end
443
-
444
- # Get a list of recommendations for self. The whole point of this gem!
445
- # Recommendations are returned in a descending order with the first index
446
- # being the object that self has been found most likely to enjoy.
447
- #
448
- # @param [Fixnum] count the number of recmomendations to return
449
- # @return [Array] an array of ActiveRecord objects that are recommendable
450
- def recommendations count = 10
451
- return [] if likes.count + dislikes.count == 0
452
-
453
- unioned_predictions = "#{self.class}:#{id}:predictions"
454
- Recommendable.redis.zunionstore unioned_predictions, Recommendable.recommendable_classes.map { |klass| predictions_set_for klass }
455
-
456
- recommendations = Recommendable.redis.zrevrange(unioned_predictions, 0, count - 1).map do |object|
457
- klass, id = object.split(":")
458
- klass.constantize.find(id)
459
- end
460
-
461
- recommendations = recommendations.first if count == 1
462
-
463
- Recommendable.redis.del(unioned_predictions) and return recommendations
464
- end
465
-
466
- # Get a list of recommendations for self on a single recommendable type.
467
- # Recommendations are returned in a descending order with the first index
468
- # being the object that self has been found most likely to enjoy.
469
- #
470
- # @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.
471
- # @return [ActiveRecord::Relation] an ActiveRecord::Relation of recommendations
472
- def recommended_for klass, count = 10
473
- return [] if likes_for(klass.base_class).count + dislikes_for(klass.base_class).count == 0 || Recommendable.redis.zcard(predictions_set_for(klass)) == 0
474
- ids = []
475
-
476
- (0...count).each do |i|
477
- prediction = Recommendable.redis.zrevrange(predictions_set_for(klass), i, i).first
478
- break unless prediction
479
-
480
- ids << prediction.split(":").last
481
- end
482
-
483
- return klass.to_s.classify.constantize.where('ID IN (?)', ids)
484
- end
485
-
486
- # Return the value calculated by {#predict} on self for a passed object.
487
- #
488
- # @param [Object] object the object to fetch the probability for
489
- # @return [Float] the likelihood of self liking the passed object
490
- def probability_of_liking object
491
- Recommendable.redis.zscore predictions_set_for(object.class), object.redis_key
492
- end
493
-
494
- # Return the negation of the value calculated by {#predict} on self
495
- # for a passed object.
496
- #
497
- # @param [Object] object the object to fetch the probability for
498
- # @return [Float] the likelihood of self disliking the passed object
499
- # @see {#probability_of_liking}
500
- def probability_of_disliking object
501
- -probability_of_liking(object)
502
- end
503
-
504
- protected
505
-
506
- # Makes a call to Redis and intersects the sets of likes belonging to self
507
- # and rater.
508
- #
509
- # @param [Object] rater the person whose set of likes you wish to intersect with that of self
510
- # @param [Hash] options the options for this intersection
511
- # @option options [Class, String, Symbol] :class ('nil') Restrict the intersection to a single recommendable type. By default, all recomendable types are considered
512
- # @option options [true, false] :return_records (true) Return the actual Model instances
513
- # @return [Array] Typically, an array of ActiveRecord objects (unless :return_records is false)
514
- def common_likes_with rater, options = {}
515
- defaults = { :class => nil, :return_records => false }
516
- options = defaults.merge options
517
-
518
- if options[:class]
519
- in_common = Recommendable.redis.sinter likes_set_for(options[:class]), rater.likes_set_for(options[:class])
520
- in_common = options[:class].to_s.classify.constantize.where('ID IN (?)', in_common) if options[:return_records]
521
- else
522
- in_common = Recommendable.recommendable_classes.map do |klass|
523
- things = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
524
-
525
- if options[:return_records]
526
- klass.to_s.classify.constantize.find things
527
- else
528
- things.map { |id| "#{klass.to_s.classify}:#{id}" }
529
- end
530
- end
531
-
532
- in_common.flatten!
533
- end
534
-
535
- return in_common
536
- end
537
-
538
- # Makes a call to Redis and intersects the sets of dislikes belonging to
539
- # self and rater.
540
- #
541
- # @param [Object] rater the person whose set of dislikes you wish to intersect with that of self
542
- # @param [Hash] options the options for this intersection
543
- # @option options [Class, String, Symbol] :class ('nil') Restrict the intersection to a single recommendable type. By default, all recomendable types are considered
544
- # @option options [true, false] :return_records (true) Return the actual Model instances
545
- # @return [Array] Typically, an array of ActiveRecord objects (unless :return_records is false)
546
- def common_dislikes_with rater, options = {}
547
- defaults = { :class => nil, :return_records => false }
548
- options = defaults.merge options
549
-
550
- if options[:class]
551
- in_common = Recommendable.redis.sinter dislikes_set_for(options[:class]), rater.dislikes_set_for(options[:class])
552
- in_common = options[:class].to_s.classify.constantize.where('ID IN (?)', in_common) if options[:return_records]
553
- else
554
- in_common = Recommendable.recommendable_classes.map do |klass|
555
- things = Recommendable.redis.sinter(dislikes_set_for(klass), rater.dislikes_set_for(klass))
556
-
557
- if options[:return_records]
558
- klass.to_s.classify.constantize.find(things)
559
- else
560
- things.map { |id| "#{klass.to_s.classify}:#{id}" }
561
- end
562
- end
563
-
564
- in_common.flatten!
565
- end
566
-
567
- in_common
568
- end
569
-
570
- # Makes a call to Redis and intersects self's set of likes with rater's
571
- # set of dislikes and vise versa. The idea here is that if self likes
572
- # an object that rater dislikes, it is a disagreement and should count
573
- # negatively towards their similarity.
574
- #
575
- # @param [Object] rater the person whose sets you wish to intersect with those of self
576
- # @param [Hash] options the options for this intersection
577
- # @option options [Class, String, Symbol] :class ('nil') Restrict the intersections to a single recommendable type. By default, all recomendable types are considered
578
- # @option options [true, false] :return_records (true) Return the actual Model instances
579
- # @return [Array] Typically, an array of ActiveRecord objects (unless :return_records is false)
580
- def disagreements_with rater, options = {}
581
- defaults = { :class => nil, :return_records => false }
582
- options = defaults.merge options
583
-
584
- if options[:class]
585
- disagreements = Recommendable.redis.sinter(likes_set_for(options[:class]), rater.dislikes_set_for(options[:class]))
586
- disagreements += Recommendable.redis.sinter(dislikes_set_for(options[:class]), rater.likes_set_for(options[:class]))
587
- disagreements = options[:class].to_s.classify.constantize.where('ID IN (?)', disagreements) if options[:return_records]
588
- else
589
- disagreements = Recommendable.recommendable_classes.map do |klass|
590
- things = Recommendable.redis.sinter(likes_set_for(klass), rater.dislikes_set_for(klass))
591
- things += Recommendable.redis.sinter(dislikes_set_for(klass), rater.likes_set_for(klass))
592
-
593
- if options[:return_records]
594
- klass.to_s.classify.constantize.find(things)
595
- else
596
- things.map { |id| "#{options[:class].to_s.classify}:#{id}" }
597
- end
598
- end
599
-
600
- disagreements.flatten!
601
- end
602
-
603
- disagreements
604
- end
605
-
606
- # Used internally during liking/disliking/stashing/ignoring objects. This
607
- # will prep an object to be liked, disliked, etc. by making sure that self
608
- # doesn't already have this item in their list of likes, dislikes, stashed
609
- # items or ignored items.
610
- #
611
- # param [Object] object the object to destroy Recommendable models for
612
- # @private
613
- def completely_unrecommend object
614
- unlike(object) || undislike(object) || unstash(object) || unignore(object)
615
- unpredict(object)
616
- end
617
-
618
- # @private
619
- def likes_set_for klass
620
- "#{self.class}:#{id}:likes:#{klass}"
621
- end
622
-
623
- # @private
624
- def dislikes_set_for klass
625
- "#{self.class}:#{id}:dislikes:#{klass}"
626
- end
627
-
628
- # Used for setup purposes. Creates and populates sets in redis containing
629
- # self's likes and dislikes.
630
- # @private
631
- def create_recommended_to_sets
632
- Recommendable.recommendable_classes.each do |klass|
633
- likes_for(klass).each { |like| Recommendable.redis.sadd likes_set_for(klass), like.likeable_id }
634
- dislikes_for(klass).each { |dislike| Recommendable.redis.sadd dislikes_set_for(klass), dislike.dislikeable_id }
635
- end
636
- end
637
-
638
- # Used for teardown purposes. Destroys the redis sets containing self's
639
- # likes and dislikes, as they are only used during the process of
640
- # updating recommendations and similarity values.
641
- # @private
642
- def destroy_recommended_to_sets
643
- Recommendable.recommendable_classes.each do |klass|
644
- Recommendable.redis.del likes_set_for(klass)
645
- Recommendable.redis.del dislikes_set_for(klass)
646
- end
647
- end
648
-
649
- private
650
-
651
- # Checks how similar a passed rater is with self. This method calculates
652
- # a numeric similarity value that can fall between -1.0 and 1.0. A value of
653
- # 1.0 indicates that rater has the exact same likes and dislikes as self
654
- # while a value of -1.0 indicates that rater dislikes every object that self
655
- # likes and likes every object that self dislikes. A value of 0.0 would
656
- # indicate that the two users share no likes or dislikes.
657
- #
658
- # @param [Object] rater an ActiveRecord object declared to `act_as_recommendable_to`
659
- # @return [Float] the numeric similarity between self and rater
660
- # @note The returned value relies on which user the method is called on. current_user.similarity_with(rater) will not equal rater.similarity_with(current_user) unless their sets of likes and dislikes are identical. current_user.similarity_with(rater) will return 1.0 even if rater has several likes/dislikes that `current_user` does not.
661
- # @private
662
- def similarity_with(rater)
663
- rater.create_recommended_to_sets
664
- agreements = common_likes_with(rater, :return_records => false).size
665
- agreements += common_dislikes_with(rater, :return_records => false).size
666
- disagreements = disagreements_with(rater, :return_records => false).size
667
-
668
- similarity = (agreements - disagreements).to_f / (likes.count + dislikes.count)
669
- rater.destroy_recommended_to_sets
670
-
671
- return similarity
672
- end
673
-
674
- # Used internally to update self's prediction values across all
675
- # recommendable types. This is called in the Resque job to refresh
676
- # recommendations.
677
- #
678
- # @private
679
- def update_recommendations
680
- Recommendable.recommendable_classes.each { |klass| update_recommendations_for klass }
681
- end
682
-
683
- # Used internally to update self's prediction values across a single
684
- # recommendable type. Convenience method for {#update_recommendations}
685
- #
686
- # @param [Class] klass the recommendable type to update predictions for
687
- # @private
688
- def update_recommendations_for klass
689
- klass.find_each do |object|
690
- next if rated?(object) || !object.been_rated? || ignored?(object) || stashed?(object)
691
- prediction = predict object
692
- Recommendable.redis.zadd(predictions_set_for(object.class), prediction, object.redis_key) if prediction
693
- end
694
- end
695
-
696
- # Predict how likely it is that self will like a passed in object. This
697
- # probability is not based on percentage. 0.0 indicates that self will
698
- # neither like nor dislike the passed object. Values that approach Infinity
699
- # indicate a rising probability of liking the passed object while values
700
- # approaching -Infinity indicate a rising probability of disliking the
701
- # passed object.
702
- #
703
- # @param [Object] object the object to check the likeliness of liking
704
- # @return [Float] the probability that self will like object
705
- # @private
706
- def predict object
707
- liked_by, disliked_by = object.send :create_recommendable_sets
708
- rated_by = Recommendable.redis.scard(liked_by) + Recommendable.redis.scard(disliked_by)
709
- similarity_sum = 0.0
710
- prediction = 0.0
711
-
712
- Recommendable.redis.smembers(liked_by).inject(similarity_sum) { |sum, r| sum += Recommendable.redis.zscore(similarity_set, r).to_f }
713
- Recommendable.redis.smembers(disliked_by).inject(similarity_sum) { |sum, r| sum -= Recommendable.redis.zscore(similarity_set, r).to_f }
714
-
715
- prediction = similarity_sum / rated_by.to_f
716
-
717
- object.send :destroy_recommendable_sets
718
-
719
- return prediction
720
- end
721
-
722
- # Used internally to update the similarity values between self and all
723
- # other users. This is called in the Resque job to refresh recommendations.
724
- #
725
- # @private
726
- def update_similarities
727
- return unless rated_anything?
728
- create_recommended_to_sets
729
-
730
- Recommendable.user_class.find_each do |rater|
731
- next if self == rater || !rater.rated_anything?
732
- Recommendable.redis.zadd similarity_set, similarity_with(rater), rater.id
733
- end
734
-
735
- destroy_recommended_to_sets
736
- end
737
-
738
- # @private
739
- def remove_from_similarities
740
- Recommendable.redis.del similarity_set
741
-
742
- Recommendable.user_class.find_each do |user|
743
- Recommendable.redis.zrem user.send(:similarity_set), self.id
744
- end
745
-
746
- true
747
- end
748
-
749
- # @private
750
- def remove_recommendations
751
- Recommendable.recommendable_classes.each do |klass|
752
- Recommendable.redis.del predictions_set_for(klass)
753
- end
754
-
755
- true
756
- end
757
-
758
- # @private
759
- def unpredict object
760
- Recommendable.redis.zrem predictions_set_for(object.class), object.redis_key
761
- end
762
-
763
- # @private
764
- def similarity_set
765
- "#{self.class}:#{id}:similarities"
766
- end
767
-
768
- # @private
769
- def predictions_set_for klass
770
- "#{self.class}:#{id}:predictions:#{klass}"
771
- end
772
- end
773
- end
774
- end