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,34 +1,46 @@
1
- require 'recommendable/engine'
2
- require 'recommendable/helpers'
3
- require 'recommendable/acts_as_recommended_to'
4
- require 'recommendable/acts_as_recommendable'
5
- require 'recommendable/exceptions'
6
- require 'recommendable/railtie' if defined?(Rails)
1
+ require 'active_support'
2
+
7
3
  require 'recommendable/version'
8
- require 'hooks'
9
- require 'sidekiq/middleware/client/unique_jobs' if defined?(Sidekiq)
10
- require 'sidekiq/middleware/server/unique_jobs' if defined?(Sidekiq)
4
+ require 'recommendable/configuration'
5
+ require 'recommendable/helpers'
6
+
7
+ require 'recommendable/rater'
8
+ require 'recommendable/ratable'
11
9
 
12
10
  module Recommendable
13
- mattr_accessor :redis, :user_class
14
- mattr_writer :recommendable_classes
15
-
16
- def self.recommendable_classes
17
- @@recommendable_classes ||= []
18
- end
11
+ class << self
12
+ def redis() config.redis end
19
13
 
20
- def self.enqueue(user_id)
21
- if defined? Sidekiq
22
- SidekiqWorker.perform_async user_id
23
- elsif defined? Resque
24
- Resque.enqueue ResqueWorker, user_id
25
- elsif defined? Delayed::Job
26
- Delayed::Job.enqueue DelayedJobWorker.new(user_id)
27
- elsif defined? Rails::Queueing
28
- unless Rails.queue.any? { |w| w.user_id == user_id }
29
- Rails.queue.push RailsWorker.new(user_id)
30
- Rails.application.queue_consumer.start
14
+ def query(klass, ids)
15
+ Recommendable::Helpers::Queriers.send(Recommendable.config.orm, klass, ids)
16
+ rescue NoMethodError
17
+ warn 'Your ORM is not currently supported. Please open an issue at https://github.com/davidcelis/recommendable'
18
+ end
19
+
20
+ def enqueue(user_id)
21
+ user_id = user_id.id if user_id.is_a?(Recommendable.config.user_class)
22
+
23
+ if defined?(::Sidekiq)
24
+ require 'recommendable/workers/sidekiq'
25
+ Recommendable::Workers::Sidekiq.perform_async(user_id)
26
+ elsif defined?(::Resque)
27
+ require 'recommendable/workers/resque'
28
+ Resque.enqueue(Recommendable::Workers::Resque, user_id)
29
+ elsif defined?(::Delayed::Job)
30
+ require 'recommendable/workers/delayed_job'
31
+ Delayed::Job.enqueue(Recommendable::Workers::DelayedJob.new(user_id))
32
+ elsif defined?(::Rails::Queueing)
33
+ require 'recommendable/workers/rails'
34
+ unless Rails.queue.any? { |w| w.user_id == user_id }
35
+ Rails.queue.push(Recommendable::Workers::Rails.new(user_idid))
36
+ Rails.application.queue_consumer.start
37
+ end
31
38
  end
32
39
  end
33
40
  end
34
41
  end
42
+
43
+ require 'recommendable/orm/active_record' if defined?(ActiveRecord::Base)
44
+ require 'recommendable/orm/data_mapper' if defined?(DataMapper::Resource)
45
+ require 'recommendable/orm/mongoid' if defined?(Mongoid::Document)
46
+ require 'recommendable/orm/mongo_mapper' if defined?(MongoMapper::Document)
@@ -0,0 +1,47 @@
1
+ require 'redis'
2
+
3
+ module Recommendable
4
+ class Configuration
5
+ # The ORM you are using. Currently supported: `:activerecord`, `:mongoid`, and `:datamapper`
6
+ attr_accessor :orm
7
+
8
+ # Recommendable's connection to Redis
9
+ attr_accessor :redis
10
+
11
+ # A prefix for all keys Recommendable uses
12
+ attr_accessor :redis_namespace
13
+
14
+ # Whether or not to automatically enqueue users to have their recommendations
15
+ # refreshed after they like/dislike an item
16
+ attr_accessor :auto_enqueue
17
+
18
+ # The name of the queue that background jobs will be placed in
19
+ attr_accessor :queue_name
20
+
21
+ # The number of nearest neighbors (k-NN) to check when updating
22
+ # recommendations for a user. Set to `nil` if you want to check all
23
+ # neighbors as opposed to a subset of the nearest ones.
24
+ attr_accessor :nearest_neighbors
25
+
26
+ attr_accessor :ratable_classes, :user_class
27
+
28
+ # Default values
29
+ def initialize
30
+ @redis = Redis.new
31
+ @redis_namespace = :recommendable
32
+ @auto_enqueue = true
33
+ @queue_name = :recommendable
34
+ @ratable_classes = []
35
+ @nearest_neighbors = nil
36
+ end
37
+ end
38
+
39
+ class << self
40
+ attr_accessor :config
41
+
42
+ def configure
43
+ @config ||= Configuration.new
44
+ yield @config
45
+ end
46
+ end
47
+ end
@@ -1,9 +1,3 @@
1
- module Recommendable
2
- module Helpers
3
- def manual_join(klass, action)
4
- table = klass.base_class.table_name
5
- inheritance_column = klass.base_class.inheritance_column
6
- "JOIN #{table} ON recommendable_#{action.pluralize}.#{action}able_id = #{table}.id AND #{table}.#{inheritance_column} = '#{klass}'"
7
- end
8
- end
9
- end
1
+ require 'recommendable/helpers/redis_key_mapper'
2
+ require 'recommendable/helpers/calculations'
3
+ require 'recommendable/helpers/queriers'
@@ -0,0 +1,150 @@
1
+ module Recommendable
2
+ module Helpers
3
+ module Calculations
4
+ class << self
5
+ # Calculate a numeric similarity value that can fall between -1.0 and 1.0.
6
+ # A value of 1.0 indicates that both users have rated the same items in
7
+ # the same ways. A value of -1.0 indicates that both users have rated the
8
+ # same items in opposite ways.
9
+ #
10
+ # @param [Fixnum, String] user_id the ID of the first user
11
+ # @param [Fixnum, String] other_user_id the ID of another user
12
+ # @return [Float] the numeric similarity between this user and the passed user
13
+ # @note Similarity values are asymmetrical. `Calculations.similarity_between(user_id, other_user_id)` will not necessarily equal `Calculations.similarity_between(other_user_id, user_id)`
14
+ def similarity_between(user_id, other_user_id)
15
+ similarity = liked_count = disliked_count = 0
16
+ in_common = Recommendable.config.ratable_classes.each do |klass|
17
+ liked_set = Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, user_id)
18
+ other_liked_set = Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, other_user_id)
19
+ disliked_set = Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, user_id)
20
+ other_disliked_set = Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, other_user_id)
21
+
22
+ # Agreements
23
+ similarity += Recommendable.redis.sinter(liked_set, other_liked_set).size
24
+ similarity += Recommendable.redis.sinter(disliked_set, other_disliked_set).size
25
+
26
+ # Disagreements
27
+ similarity -= Recommendable.redis.sinter(liked_set, other_disliked_set).size
28
+ similarity -= Recommendable.redis.sinter(disliked_set, other_liked_set).size
29
+
30
+ liked_count += Recommendable.redis.scard(liked_set)
31
+ disliked_count += Recommendable.redis.scard(disliked_set)
32
+ end
33
+
34
+ similarity / (liked_count + disliked_count).to_f
35
+ end
36
+
37
+ # Used internally to update the similarity values between this user and all
38
+ # other users. This is called by the background worker.
39
+ def update_similarities_for(user_id)
40
+ user_id = user_id.to_s # For comparison. Redis returns all set members as strings.
41
+ similarity_set = Recommendable::Helpers::RedisKeyMapper.similarity_set_for(user_id)
42
+
43
+ # Only calculate similarities for users who have rated the items that
44
+ # this user has rated
45
+ relevant_user_ids = Recommendable.config.ratable_classes.inject([]) do |memo, klass|
46
+ liked_set = Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, user_id)
47
+ disliked_set = Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, user_id)
48
+ item_ids = Recommendable.redis.sunion(liked_set, disliked_set)
49
+
50
+ unless item_ids.empty?
51
+ sets = item_ids.map do |id|
52
+ liked_by_set = Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(klass, id)
53
+ disliked_by_set = Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(klass, id)
54
+
55
+ [liked_by_set, disliked_by_set]
56
+ end
57
+
58
+ memo | Recommendable.redis.sunion(sets.flatten)
59
+ else
60
+ memo
61
+ end
62
+ end
63
+
64
+ relevant_user_ids.each do |id|
65
+ next if id == user_id # Skip comparing with self.
66
+ Recommendable.redis.zadd(similarity_set, similarity_between(user_id, id), id)
67
+ end
68
+
69
+ true
70
+ end
71
+
72
+ # Used internally to update this user's prediction values across all
73
+ # recommendable types. This is called by the background worker.
74
+ #
75
+ # @private
76
+ def update_recommendations_for(user_id)
77
+ nearest_neighbors = Recommendable.config.nearest_neighbors || Recommendable.config.user_class.count
78
+ Recommendable.config.ratable_classes.each do |klass|
79
+ similarity_set = Recommendable::Helpers::RedisKeyMapper.similarity_set_for(user_id)
80
+ recommended_set = Recommendable::Helpers::RedisKeyMapper.recommended_set_for(klass, user_id)
81
+ similar_user_ids = Recommendable.redis.zrevrange(similarity_set, 0, nearest_neighbors - 1)
82
+
83
+ sets_to_union = similar_user_ids.inject([]) do |sets, id|
84
+ sets << Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, id)
85
+ end
86
+
87
+ next if sets_to_union.empty?
88
+ scores = Recommendable.redis.sunion(sets_to_union).map { |id| [predict_for(user_id, klass, id), id] }
89
+ next if scores.empty?
90
+ Recommendable.redis.zadd(recommended_set, scores)
91
+ end
92
+
93
+ true
94
+ end
95
+
96
+ # Predict how likely it is that a user will like an item. This probability
97
+ # is not based on percentage. 0.0 indicates that the user will neither like
98
+ # nor dislike the item. Values that approach Infinity indicate a rising
99
+ # likelihood of liking the item while values approaching -Infinity
100
+ # indicate a rising probability of disliking the item.
101
+ #
102
+ # @param [Fixnum, String] user_id the user's ID
103
+ # @param [Class] klass the item's class
104
+ # @param [Fixnum, String] item_id the item's ID
105
+ # @return [Float] the probability that the user will like the item
106
+ def predict_for(user_id, klass, item_id)
107
+ similarity_set = Recommendable::Helpers::RedisKeyMapper.similarity_set_for(user_id)
108
+ liked_by_set = Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(klass, item_id)
109
+ disliked_by_set = Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(klass, item_id)
110
+ similarity_sum = 0.0
111
+
112
+ Recommendable.redis.smembers(liked_by_set).inject(similarity_sum) do |sum, id|
113
+ sum += Recommendable.redis.zscore(similarity_set, id).to_f
114
+ end
115
+ Recommendable.redis.smembers(disliked_by_set).inject(similarity_sum) do |sum, id|
116
+ sum -= Recommendable.redis.zscore(similarity_set, id).to_f
117
+ end
118
+
119
+ liked_by_count = Recommendable.redis.scard(liked_by_set)
120
+ disliked_by_count = Recommendable.redis.scard(disliked_by_set)
121
+ prediction = similarity_sum / (liked_by_count + disliked_by_count).to_f
122
+ prediction.finite? ? prediction : 0.0
123
+ end
124
+
125
+ def update_score_for(klass, id)
126
+ score_set = Recommendable::Helpers::RedisKeyMapper.score_set_for(klass)
127
+ liked_by_set = Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(klass, id)
128
+ disliked_by_set = Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(klass, id)
129
+ liked_by_count = Recommendable.redis.scard(liked_by_set)
130
+ disliked_by_count = Recommendable.redis.scard(disliked_by_set)
131
+
132
+ return 0.0 unless liked_by_count + disliked_by_count > 0
133
+
134
+ z = 1.96
135
+ n = liked_by_count + disliked_by_count
136
+ phat = liked_by_count / n.to_f
137
+
138
+ begin
139
+ score = (phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n)
140
+ rescue Math::DomainError
141
+ score = 0
142
+ end
143
+
144
+ Recommendable.redis.zadd(score_set, score, id)
145
+ true
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,23 @@
1
+ module Recommendable
2
+ module Helpers
3
+ module Queriers
4
+ class << self
5
+ def active_record(klass, ids)
6
+ klass.where(:id => ids)
7
+ end
8
+
9
+ def data_mapper(klass, ids)
10
+ klass.all(:id => ids)
11
+ end
12
+
13
+ def mongoid(klass, ids)
14
+ klass.where(:id => ids)
15
+ end
16
+
17
+ def mongo_mapper(klass, ids)
18
+ klass.where(:id => ids)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module Recommendable
2
+ module Helpers
3
+ module RedisKeyMapper
4
+ class << self
5
+ %w[liked disliked hidden bookmarked recommended].each do |action|
6
+ define_method "#{action}_set_for" do |klass, id|
7
+ [Recommendable.config.redis_namespace, Recommendable.config.user_class.to_s.tableize, id, "#{action}_#{klass.to_s.tableize}"].compact.join(':')
8
+ end
9
+ end
10
+
11
+ def similarity_set_for(id)
12
+ [Recommendable.config.redis_namespace, Recommendable.config.user_class.to_s.tableize, id, 'similarities'].compact.join(':')
13
+ end
14
+
15
+ def liked_by_set_for(klass, id)
16
+ [Recommendable.config.redis_namespace, klass.to_s.tableize, id, "liked_by"].compact.join(':')
17
+ end
18
+
19
+ def disliked_by_set_for(klass, id)
20
+ [Recommendable.config.redis_namespace, klass.to_s.tableize, id, "disliked_by"].compact.join(':')
21
+ end
22
+
23
+ def score_set_for(klass)
24
+ [Recommendable.config.redis_namespace, klass.to_s.tableize, 'scores'].join(':')
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Base.send(:include, Recommendable::Rater)
4
+ ActiveRecord::Base.send(:include, Recommendable::Ratable)
5
+
6
+ Recommendable.configure { |config| config.orm = :active_record }
@@ -0,0 +1,7 @@
1
+ require 'dm-core'
2
+
3
+ DataMapper::Model.append_extensions(Recommendable::Rater::ClassMethods)
4
+ DataMapper::Model.append_extensions(Recommendable::Ratable::ClassMethods)
5
+ DataMapper::Model.append_inclusions(Recommendable::Ratable::InstanceMethods)
6
+
7
+ Recommendable.configure { |config| config.orm = :data_mapper }
@@ -0,0 +1,8 @@
1
+ require 'mongo_mapper'
2
+
3
+ MongoMapper::Document.plugin(Recommendable::Rater)
4
+ MongoMapper::Document.plugin(Recommendable::Ratable)
5
+ MongoMapper::EmbeddedDocument.plugin(Recommendable::Rater)
6
+ MongoMapper::EmbeddedDocument.plugin(Recommendable::Ratable)
7
+
8
+ Recommendable.configure { |config| config.orm = :mongo_mapper }
@@ -0,0 +1,7 @@
1
+ require 'mongoid'
2
+
3
+ Mongoid::Document::ClassMethods.send(:include, Recommendable::Rater::ClassMethods)
4
+ Mongoid::Document::ClassMethods.send(:include, Recommendable::Ratable::ClassMethods)
5
+ Mongoid::Document.send(:include, Recommendable::Ratable::InstanceMethods)
6
+
7
+ Recommendable.configure { |config| config.orm = :mongoid }
@@ -0,0 +1,83 @@
1
+ require 'recommendable/ratable/likable'
2
+ require 'recommendable/ratable/dislikable'
3
+
4
+ module Recommendable
5
+ module Ratable
6
+ extend ActiveSupport::Concern
7
+
8
+ def recommendable?() self.class.recommendable? end
9
+
10
+ module ClassMethods
11
+ def make_recommendable!
12
+ Recommendable.configure { |config| config.ratable_classes << self }
13
+
14
+ class_eval do
15
+ include Likable
16
+ include Dislikable
17
+
18
+ if ancestors.include?(ActiveRecord::Base) || include?(Mongoid::Document) || include?(MongoMapper::Document) || include?(MongoMapper::EmbeddedDocument)
19
+ before_destroy :remove_from_recommendable!
20
+ elsif include?(DataMapper::Resource)
21
+ before :destroy, :remove_from_recommendable!
22
+ end
23
+
24
+ # Whether or not items belonging to this class can be recommended.
25
+ #
26
+ # @return true if a user class `recommends :this`
27
+ def self.recommendable?() true end
28
+
29
+ # Check to see if anybody has rated (liked or disliked) this object
30
+ #
31
+ # @return true if anybody has liked/disliked this
32
+ def rated?
33
+ liked_by_count > 0 || disliked_by_count > 0
34
+ end
35
+
36
+ # Query for the top-N items sorted by score
37
+ #
38
+ # @param [Fixnum] count the number of items to fetch (defaults to 1)
39
+ # @return [Array] the top items belonging to this class, sorted by score
40
+ def self.top(count = 1)
41
+ score_set = Recommendable::Helpers::RedisKeyMapper.score_set_for(self)
42
+ ids = Recommendable.redis.zrevrange(score_set, 0, count - 1)
43
+
44
+ Recommendable.query(self, ids).sort_by { |item| ids.index(item.id.to_s) }
45
+ end
46
+
47
+ private
48
+
49
+ # Completely removes this item from redis. Called from a before_destroy hook.
50
+ # @private
51
+ def remove_from_recommendable!
52
+ # Remove this item from the score zset
53
+ Recommendable.redis.zrem(Recommendable::Helpers::RedisKeyMapper.score_set_for(self.class), id)
54
+
55
+ # Remove this item's liked_by/disliked_by sets
56
+ %w[liked_by disliked_by].each do |action|
57
+ set = Recommendable::Helpers::RedisKeyMapper.send("#{action}_set_for", self.class, id)
58
+ Recommendable.redis.del(set)
59
+ end
60
+
61
+ # Remove this item from any user's like/dislike/hidden/bookmark sets
62
+ %w[liked disliked hidden bookmarked].each do |action|
63
+ set = Recommendable::Helpers::RedisKeyMapper.send("#{action}_set_for", self.class, id)
64
+ Recommendable.redis.keys(set).each do |set|
65
+ Recommendable.redis.srem(set, id)
66
+ end
67
+ end
68
+
69
+ # Remove this item from any user's recommendation zset
70
+ Recommendable.redis.keys(Recommendable::Helpers::RedisKeyMapper.recommended_set_for(self.class, '*')).each do |zset|
71
+ Recommendable.redis.zrem(zset, id)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ # Whether or not items belonging to this class can be recommended.
78
+ #
79
+ # @return true if a user class `recommends :this`
80
+ def recommendable?() false end
81
+ end
82
+ end
83
+ end