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