recommendable 1.1.7 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/recommendable.rb +38 -26
- data/lib/recommendable/configuration.rb +47 -0
- data/lib/recommendable/helpers.rb +3 -9
- data/lib/recommendable/helpers/calculations.rb +150 -0
- data/lib/recommendable/helpers/queriers.rb +23 -0
- data/lib/recommendable/helpers/redis_key_mapper.rb +29 -0
- data/lib/recommendable/orm/active_record.rb +6 -0
- data/lib/recommendable/orm/data_mapper.rb +7 -0
- data/lib/recommendable/orm/mongo_mapper.rb +8 -0
- data/lib/recommendable/orm/mongoid.rb +7 -0
- data/lib/recommendable/ratable.rb +83 -0
- data/lib/recommendable/ratable/dislikable.rb +26 -0
- data/lib/recommendable/ratable/likable.rb +26 -0
- data/lib/recommendable/rater.rb +109 -0
- data/lib/recommendable/rater/bookmarker.rb +120 -0
- data/lib/recommendable/rater/disliker.rb +122 -0
- data/lib/recommendable/rater/hider.rb +120 -0
- data/lib/recommendable/rater/liker.rb +122 -0
- data/lib/recommendable/rater/recommender.rb +68 -0
- data/lib/recommendable/version.rb +5 -4
- data/lib/recommendable/workers/delayed_job.rb +16 -0
- data/lib/recommendable/workers/rails.rb +16 -0
- data/lib/recommendable/workers/resque.rb +13 -0
- data/lib/recommendable/workers/sidekiq.rb +13 -0
- metadata +62 -131
- data/.gitignore +0 -57
- data/.travis.yml +0 -3
- data/CHANGELOG.markdown +0 -159
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -112
- data/LICENSE.txt +0 -22
- data/README.markdown +0 -135
- data/Rakefile +0 -26
- data/TODO +0 -7
- data/app/models/recommendable/dislike.rb +0 -19
- data/app/models/recommendable/ignore.rb +0 -19
- data/app/models/recommendable/like.rb +0 -19
- data/app/models/recommendable/stash.rb +0 -19
- data/app/workers/recommendable/delayed_job_worker.rb +0 -17
- data/app/workers/recommendable/rails_worker.rb +0 -17
- data/app/workers/recommendable/resque_worker.rb +0 -14
- data/app/workers/recommendable/sidekiq_worker.rb +0 -14
- data/config/routes.rb +0 -3
- data/db/migrate/20120124193723_create_likes.rb +0 -17
- data/db/migrate/20120124193728_create_dislikes.rb +0 -17
- data/db/migrate/20120127092558_create_ignores.rb +0 -17
- data/db/migrate/20120131173909_create_stashes.rb +0 -17
- data/lib/generators/recommendable/USAGE +0 -8
- data/lib/generators/recommendable/install_generator.rb +0 -40
- data/lib/generators/recommendable/templates/initializer.rb +0 -28
- data/lib/recommendable/acts_as_recommendable.rb +0 -176
- data/lib/recommendable/acts_as_recommended_to.rb +0 -774
- data/lib/recommendable/engine.rb +0 -14
- data/lib/recommendable/exceptions.rb +0 -4
- data/lib/recommendable/railtie.rb +0 -6
- data/lib/sidekiq/middleware/client/unique_jobs.rb +0 -37
- data/lib/sidekiq/middleware/server/unique_jobs.rb +0 -17
- data/lib/tasks/recommendable_tasks.rake +0 -1
- data/recommendable.gemspec +0 -30
- data/script/rails +0 -8
- data/spec/configuration_spec.rb +0 -9
- data/spec/dummy/README.rdoc +0 -261
- data/spec/dummy/Rakefile +0 -7
- data/spec/dummy/app/assets/javascripts/application.js +0 -15
- data/spec/dummy/app/assets/stylesheets/application.css +0 -13
- data/spec/dummy/app/controllers/application_controller.rb +0 -3
- data/spec/dummy/app/helpers/application_helper.rb +0 -2
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/bully.rb +0 -2
- data/spec/dummy/app/models/movie.rb +0 -2
- data/spec/dummy/app/models/php_framework.rb +0 -2
- data/spec/dummy/app/models/user.rb +0 -3
- data/spec/dummy/app/views/layouts/application.html.erb +0 -14
- data/spec/dummy/config.ru +0 -4
- data/spec/dummy/config/application.rb +0 -56
- data/spec/dummy/config/boot.rb +0 -10
- data/spec/dummy/config/database.yml +0 -25
- data/spec/dummy/config/environment.rb +0 -5
- data/spec/dummy/config/environments/development.rb +0 -37
- data/spec/dummy/config/environments/production.rb +0 -67
- data/spec/dummy/config/environments/test.rb +0 -37
- data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/spec/dummy/config/initializers/inflections.rb +0 -15
- data/spec/dummy/config/initializers/mime_types.rb +0 -5
- data/spec/dummy/config/initializers/recommendable.rb +0 -14
- data/spec/dummy/config/initializers/secret_token.rb +0 -7
- data/spec/dummy/config/initializers/session_store.rb +0 -8
- data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/spec/dummy/config/locales/en.yml +0 -5
- data/spec/dummy/config/routes.rb +0 -4
- data/spec/dummy/db/migrate/20120128005553_create_likes.recommendable.rb +0 -18
- data/spec/dummy/db/migrate/20120128005554_create_dislikes.recommendable.rb +0 -18
- data/spec/dummy/db/migrate/20120128005555_create_ignores.recommendable.rb +0 -18
- data/spec/dummy/db/migrate/20120128020228_create_users.rb +0 -9
- data/spec/dummy/db/migrate/20120128020413_create_movies.rb +0 -10
- data/spec/dummy/db/migrate/20120128024632_create_php_frameworks.rb +0 -9
- data/spec/dummy/db/migrate/20120128024804_create_bullies.rb +0 -9
- data/spec/dummy/db/migrate/20120131195416_create_stashes.recommendable.rb +0 -19
- data/spec/dummy/db/schema.rb +0 -89
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +0 -26
- data/spec/dummy/public/422.html +0 -26
- data/spec/dummy/public/500.html +0 -25
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/recommendable_dummy_development +0 -0
- data/spec/dummy/recommendable_dummy_test +0 -0
- data/spec/dummy/script/rails +0 -6
- data/spec/factories.rb +0 -16
- data/spec/models/dislike_spec.rb +0 -41
- data/spec/models/ignore_spec.rb +0 -27
- data/spec/models/like_spec.rb +0 -42
- data/spec/models/movie_spec.rb +0 -82
- data/spec/models/stash_spec.rb +0 -27
- data/spec/models/user_benchmark_spec.rb +0 -49
- data/spec/models/user_spec.rb +0 -443
- data/spec/spec_helper.rb +0 -28
data/lib/recommendable.rb
CHANGED
@@ -1,34 +1,46 @@
|
|
1
|
-
require '
|
2
|
-
|
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 '
|
9
|
-
require '
|
10
|
-
|
4
|
+
require 'recommendable/configuration'
|
5
|
+
require 'recommendable/helpers'
|
6
|
+
|
7
|
+
require 'recommendable/rater'
|
8
|
+
require 'recommendable/ratable'
|
11
9
|
|
12
10
|
module Recommendable
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
def self.recommendable_classes
|
17
|
-
@@recommendable_classes ||= []
|
18
|
-
end
|
11
|
+
class << self
|
12
|
+
def redis() config.redis end
|
19
13
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
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,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
|