recommendable 1.1.7 → 2.0.0

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