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