/
acts_as_recommendable.rb
190 lines (148 loc) · 6.76 KB
/
acts_as_recommendable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
module Recommendable
module ActsAsRecommendable
extend ActiveSupport::Concern
module ClassMethods
def acts_as_recommendable
class_eval do
Recommendable.recommendable_classes << self
has_many :recommendable_likes, :as => :likeable, :dependent => :destroy,
:class_name => "Recommendable::Like"
has_many :recommendable_dislikes, :as => :dislikeable, :dependent => :destroy,
:class_name => "Recommendable::Dislike"
has_many :recommendable_ignores, :as => :ignorable, :dependent => :destroy,
:class_name => "Recommendable::Ignore"
has_many :recommendable_stashes, :as => :stashable, :dependent => :destroy,
:class_name => "Recommendable::Stash"
has_many :liked_by, :through => :recommendable_likes, :source => :user,
:foreign_key => :user_id, :class_name => Recommendable.user_class.to_s
has_many :disliked_by, :through => :recommendable_dislikes, :source => :user,
:foreign_key => :user_id, :class_name => Recommendable.user_class.to_s
include LikeableMethods
include DislikeableMethods
before_destroy :remove_from_scores, :remove_from_recommendations
def self.acts_as_recommendable?() true end
def been_rated?
recommendable_likes.count + recommendable_dislikes.count > 0
end
# Returns an array of users that have liked or disliked this item.
# @return [Array] an array of users
def rated_by
liked_by + disliked_by
end
def self.top count = 1, options = {}
defaults = { :offset => 0 }
options = defaults.merge options
ids = Recommendable.configuration.redis.zrevrange(self.score_set, options[:offset], options[:offset] + count - 1).map(&:to_i)
items = self.find ids
return items.first if count == 1
return items.sort_by { |item| ids.index(item.id) }
end
private
def update_score
return 0 unless been_rated?
z = 1.96
n = recommendable_likes.count + recommendable_dislikes.count
phat = recommendable_likes.count / n.to_f
score = (phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n)
Recommendable.configuration.redis.zadd self.class.score_set, score, self.id
true
end
def remove_from_scores
Recommendable.configuration.redis.zrem self.class.score_set, self.id
true
end
def remove_from_recommendations
Recommendable.user_class.find_each do |user|
user.send :completely_unrecommend, self
end
end
# Used for setup purposes. Calls convenience methods to create sets
# in redis of users that both like and dislike this object.
# @return [Array] an array containing the liked_by set and the disliked_by set
# @private
def create_recommendable_sets
[create_liked_by_set, create_disliked_by_set]
end
# Used for teardown purposes. Destroys the sets in redis created by
# {#create_recommendable_sets}
# @private
def destroy_recommendable_sets(force = false)
return unless Recommendable.configuration.expire_keys_in == :destroy || force
Recommendable.configuration.redis.del "#{self.class.base_class}:#{id}:liked_by"
Recommendable.configuration.redis.del "#{self.class.base_class}:#{id}:disliked_by"
end
# Returns an array of IDs of users that have liked or disliked this item.
# @return [Array] an array of user IDs
# @private
def rates_by
recommendable_likes.map(&:user_id) + recommendable_dislikes.map(&:user_id)
end
def self.score_set
"#{self}:sorted"
end
private :recommendable_likes, :recommendable_dislikes,
:recommendable_ignores, :recommendable_stashes
end
end
def acts_as_recommendable?() false end
def sti?
self.base_class != self && self.base_class.table_name == self.table_name
end
private
end
# Instance methods.
def recommendable?() self.class.acts_as_recommendable? end
def redis_key() "#{self.class.base_class}:#{id}" end
protected :redis_key
module LikeableMethods
# Retrieve the number of likes this object has received. Cached in Redis.
# @return [Fixnum] the number of times this object has been liked
def like_count
Recommendable.configuration.redis.get("#{redis_key}:like_count").to_i
end
private
# Updates the cache for how many times this object has been liked.
# @private
def update_like_count
Recommendable.configuration.redis.set "#{redis_key}:like_count", liked_by.count
end
# Used for setup purposes. Creates a set in redis containing users that
# have liked this object.
# @private
# @return [String] the key in Redis pointing to the set
def create_liked_by_set
set = "#{redis_key}:liked_by"
liked_by.each { |rater| Recommendable.configuration.redis.sadd set, rater.id }
if Recommendable.configuration.expire_keys_in.is_a?(Numeric)
Recommendable.configuration.redis.expire(set, Recommendable.configuration.expire_keys_in)
end
return set
end
end
module DislikeableMethods
# Retrieve the number of dislikes this object has received. Cached in Redis.
# @return [Fixnum] the number of times this object has been disliked
def dislike_count
Recommendable.configuration.redis.get("#{redis_key}:dislike_count").to_i
end
private
# Updates the cache for how many times this object has been disliked.
# @private
def update_dislike_count
Recommendable.configuration.redis.set "#{redis_key}:dislike_count", disliked_by.count
end
# Used for setup purposes. Creates a set in redis containing users that
# have disliked this object.
# @private
# @return [String] the key in Redis pointing to the set
def create_disliked_by_set
set = "#{redis_key}:disliked_by"
disliked_by.each { |rater| Recommendable.configuration.redis.sadd set, rater.id }
if Recommendable.configuration.expire_keys_in.is_a?(Numeric)
Recommendable.configuration.redis.expire(set, Recommendable.configuration.expire_keys_in)
end
return set
end
end
end
end