recommendable 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.markdown CHANGED
@@ -1,8 +1,26 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
- 0.1.2 (current version)
4
+ 0.1.4 (current version)
5
5
  -----------------------
6
+ * `acts_as_recommendable` is no longer needed in your models
7
+ * Instead of declaring `acts_as_recommended_to` in your User class, please use `recommends` instead, passing in a list of your recommendable models as a list of symbols (e.g. `recommends :movies, :books`)
8
+ * Your initializer should no longer declare the user class. This is no longer necessary and is deprecated.
9
+ * Fix an issue that caused the unnecessary need for eager loading models in development
10
+ * Removed aliases: `liked_records`, `liked_records_for`, `disliked_records`, and `disliked_records_for`
11
+ * Renamed methods:
12
+ * `has_ignored?` => `ignored?`
13
+ * `has_stashed?` => `stashed?`
14
+ * Code quality tweaks
15
+
16
+ 0.1.3 (current version)
17
+ -----------------------
18
+
19
+ * Improvements to speed of similarity calculations.
20
+ * Added an instance method to items that act_as_recommendable, `rated_by`. This returns an array of users that like or dislike the item.
21
+
22
+ 0.1.2
23
+ -----
6
24
 
7
25
  * Fix an issue that could cause similarity values between users to be incorrect.
8
26
  * `User#common_likes_with` and `User#common_dislikes_with` now return the actual Model instances by default. Accordingly, these methods are no longer private.
data/README.markdown CHANGED
@@ -1,8 +1,8 @@
1
1
  # recommendable [![Build Status](https://secure.travis-ci.org/davidcelis/recommendable.png)](http://travis-ci.org/davidcelis/recommendable)
2
2
 
3
3
  Recommendable is a Rails Engine to add Like/Dislike functionality to your
4
- application. It uses Redis to generate recommendations quickly through a
5
- collaborative filtering algorithm that I modified myself. Your users' tastes
4
+ application. It uses Redis to generate recommendations quickly through [a
5
+ collaborative filtering algorithm that I modified myself][6]. Your users' tastes
6
6
  are compared with one another and used to give them great recommendations!
7
7
  Yes, Redis is required. Scroll to the end of the README for more info on that.
8
8
 
@@ -30,7 +30,7 @@ Add the following to your Rails application's `Gemfile`:
30
30
  After your `bundle install`, you can then run:
31
31
 
32
32
  ``` bash
33
- $ rails g recommendable:install (--user-class=User)
33
+ $ rails g recommendable:install
34
34
  ```
35
35
 
36
36
  After running the installation generator, you should double check
@@ -59,29 +59,13 @@ In your Rails model that represents your application's user:
59
59
 
60
60
  ``` ruby
61
61
  class User < ActiveRecord::Base
62
- acts_as_recommended_to
62
+ recommends :movies, :shows, :other_things
63
63
 
64
64
  # ...
65
65
  end
66
66
  ```
67
67
 
68
- Then, from any Rails model you wish your `user` to be able to `like` or `dislike`:
69
-
70
- ``` ruby
71
- class Movie < ActiveRecord::Base
72
- acts_as_recommendable
73
-
74
- # ...
75
- end
76
-
77
- class Show < ActiveRecord::Base
78
- acts_as_recommendable
79
-
80
- # ...
81
- end
82
- ````
83
-
84
- And that's it!
68
+ Just pass in a list of classes and that's it!
85
69
 
86
70
  ### Liking/Disliking
87
71
 
@@ -122,7 +106,7 @@ wish to return a set of liked or disliked objects for only one of those
122
106
  models.
123
107
 
124
108
  ``` ruby
125
- current_user.liked_for(Movie)
109
+ current_user.liked_for(Movie) # or "movie", or :movie
126
110
  #=> [#<Movie name: '2001: A Space Odyssey', year: 1968>, #<Movie name: 'Back to the Future', :year => 1985>]
127
111
  current_user.disliked_for(Show)
128
112
  #=> []
@@ -163,7 +147,7 @@ current_user.like(movie_to_watch_later)
163
147
  #=> true
164
148
  current_user.stash(movie_to_watch_later)
165
149
  #=> nil
166
- current_user.has_stashed?(movie_to_watch_later)
150
+ current_user.stashed?(movie_to_watch_later)
167
151
  #=> false
168
152
  ```
169
153
 
@@ -344,3 +328,4 @@ further details.
344
328
  [3]: http://wiki.github.com/defunkt/resque/contributing
345
329
  [4]: http://help.github.com/forking/
346
330
  [5]: http://help.github.com/pull-requests/
331
+ [6]: http://davidcelis.com/blog/2012/02/07/collaborative-filtering-with-likes-and-dislikes/
@@ -2,7 +2,7 @@ Description:
2
2
  This generator will install Recommendable's initializer.rb file and migrate the Like and Dislike tables into your database unless specified.
3
3
 
4
4
  Example:
5
- rails generate recommendable:install --user-class=YourUserClass
5
+ rails generate recommendable:install
6
6
 
7
7
  This will create:
8
8
  config/initializers/recommendable.rb
@@ -3,7 +3,6 @@ require 'rails/generators'
3
3
  module Recommendable
4
4
  module Generators
5
5
  class InstallGenerator < Rails::Generators::Base
6
- argument :user_model, :type => :string, :default => "User", :desc => "Your user model that will be liking and disliking objects."
7
6
  argument :redis_host, :type => :string, :default => "localhost", :desc => "The hostname your redis server is running on."
8
7
  argument :redis_port, :type => :string, :default => "6379", :desc => "The port your redis server is running on."
9
8
  class_option :redis_socket, :type => :string, :desc => "Indicates the UNIX socket your redis server is running on (if it is)."
@@ -36,12 +35,6 @@ module Recommendable
36
35
  def finished
37
36
  puts "Done! Recommendable has been successfully installed. Please configure it in config/intializers/recommendable.rb"
38
37
  end
39
-
40
- private
41
-
42
- def user_class
43
- user_model.camelize
44
- end
45
38
  end
46
39
  end
47
- end
40
+ end
@@ -2,9 +2,6 @@ require "redis"
2
2
  require "resque"
3
3
  require "resque-loner"
4
4
 
5
- # What class will be liking/disliking objects and receiving recommendations?
6
- Recommendable.user_class = "<%= user_class %>"
7
-
8
5
  # Recommendable requires a connection to a running redis-server. Either create
9
6
  # a new instance based on a host/port or UNIX socket, or pass in an existing
10
7
  # Redis client instance.
@@ -15,4 +12,4 @@ Recommendable.user_class = "<%= user_class %>"
15
12
 
16
13
  # Tell Redis which database to use (usually between 0 and 15). The default of 0
17
14
  # is most likely okay unless you have another application using that database.
18
- Recommendable.redis.select "0"
15
+ Recommendable.redis.select "0"
@@ -17,9 +17,10 @@ module Recommendable
17
17
  include LikeableMethods
18
18
  include DislikeableMethods
19
19
 
20
- def self.acts_as_recommendable? ; true ; end
20
+ def self.acts_as_recommendable?() true end
21
21
 
22
- def has_been_rated?
22
+
23
+ def been_rated?
23
24
  likes.count + dislikes.count > 0
24
25
  end
25
26
 
@@ -28,13 +29,8 @@ module Recommendable
28
29
  def rated_by
29
30
  liked_by + disliked_by
30
31
  end
31
-
32
- # Returns an array of IDs of users that have liked or disliked this item.
33
- # @return [Array] an array of user IDs
34
- # @private
35
- def rates_by
36
- likes.map(&:user_id) + dislikes.map(&:user_id)
37
- end
32
+
33
+ private
38
34
 
39
35
  # Used for setup purposes. Calls convenience methods to create sets
40
36
  # in redis of users that both like and dislike this object.
@@ -52,22 +48,30 @@ module Recommendable
52
48
  Recommendable.redis.del "#{self.class}:#{id}:disliked_by"
53
49
  end
54
50
 
55
- private :likes, :dislikes, :ignores, :stashes, :rates_by,
56
- :create_recommendable_sets, :destroy_recommendable_sets
51
+ # Returns an array of IDs of users that have liked or disliked this item.
52
+ # @return [Array] an array of user IDs
53
+ # @private
54
+ def rates_by
55
+ likes.map(&:user_id) + dislikes.map(&:user_id)
56
+ end
57
+
58
+ private :likes, :dislikes, :ignores, :stashes
57
59
  end
58
60
  end
59
61
 
60
- def acts_as_recommendable? ; false ; end
62
+ def acts_as_recommendable?() false end
61
63
  end
62
64
 
63
65
  # Instance methods.
64
- def recommendable? ; self.class.acts_as_recommendable? ; end
66
+ def recommendable?() self.class.acts_as_recommendable? end
65
67
 
66
- def redis_key ; "#{self.class}:#{id}" ; end
68
+ def redis_key() "#{self.class}:#{id}" end
67
69
 
68
70
  protected :redis_key
69
71
 
70
72
  module LikeableMethods
73
+ private
74
+
71
75
  # Used for setup purposes. Creates a set in redis containing users that
72
76
  # have liked this object.
73
77
  # @private
@@ -77,11 +81,11 @@ module Recommendable
77
81
  liked_by.each {|rater| Recommendable.redis.sadd set, rater.id}
78
82
  return set
79
83
  end
80
-
81
- private :create_liked_by_set
82
84
  end
83
85
 
84
86
  module DislikeableMethods
87
+ private
88
+
85
89
  # Used for setup purposes. Creates a set in redis containing users that
86
90
  # have disliked this object.
87
91
  # @private
@@ -91,8 +95,6 @@ module Recommendable
91
95
  disliked_by.each {|rater| Recommendable.redis.sadd set, rater.id}
92
96
  return set
93
97
  end
94
-
95
- private :create_disliked_by_set
96
98
  end
97
99
  end
98
100
  end
@@ -5,8 +5,15 @@ module Recommendable
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  module ClassMethods
8
+ def recommends(*things)
9
+ acts_as_recommended_to
10
+ things.each { |thing| thing.to_s.classify.constantize.acts_as_recommendable }
11
+ end
12
+
8
13
  def acts_as_recommended_to
9
14
  class_eval do
15
+ Recommendable.user_class = self
16
+
10
17
  has_many :likes, :class_name => "Recommendable::Like", :dependent => :destroy
11
18
  has_many :dislikes, :class_name => "Recommendable::Dislike", :dependent => :destroy
12
19
  has_many :ignores, :class_name => "Recommendable::Ignore", :dependent => :destroy
@@ -18,18 +25,11 @@ module Recommendable
18
25
  include IgnoreMethods
19
26
  include RecommendationMethods
20
27
 
21
- def self.acts_as_recommended_to? ; true ; end
22
-
23
28
  private :likes, :dislikes, :ignores, :stashed_items
24
29
  end
25
30
  end
26
-
27
- def acts_as_recommended_to? ; false ; end
28
31
  end
29
32
 
30
- # Instance method.
31
- def can_rate? ; self.class.acts_as_recommended_to? ; end
32
-
33
33
  module LikeMethods
34
34
  # Creates a Recommendable::Like to associate self to a passed object. If
35
35
  # self is currently found to have disliked object, the corresponding
@@ -74,29 +74,26 @@ module Recommendable
74
74
  likes.map {|like| like.likeable}
75
75
  end
76
76
 
77
- alias_method :liked_records, :liked
78
-
79
- # Get a list of Recommendable::Likes with a `#likeable_type` of the passed
80
- # class.
81
- #
82
- # @param [Class, String, Symbol] klass the class for which you would like to return self's likes. Can be the class constant, or a String/Symbol representation of the class name.
83
- # @note You should not need to use this method. (see {#liked_for})
84
- # @private
85
- def likes_for(klass)
86
- likes.where(:likeable_type => klassify(klass).to_s)
87
- end
88
-
89
77
  # Get a list of records belonging to a passed class that self currently
90
78
  # likes.
91
79
  #
92
80
  # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
93
81
  # @return [Array] an array of ActiveRecord objects that self has liked belonging to klass
94
82
  def liked_for(klass)
95
- klassify(klass).find likes_for(klass).map(&:likeable_id)
83
+ klass.to_s.classify.constantize.find likes_for(klass).map(&:likeable_id)
96
84
  end
97
85
 
98
- alias_method :liked_records_for, :liked_for
99
- private :likes_for
86
+ private
87
+
88
+ # Get a list of Recommendable::Likes with a `#likeable_type` of the passed
89
+ # class.
90
+ #
91
+ # @param [Class, String, Symbol] klass the class for which you would like to return self's likes. Can be the class constant, or a String/Symbol representation of the class name.
92
+ # @note You should not need to use this method. (see {#liked_for})
93
+ # @private
94
+ def likes_for(klass)
95
+ likes.where(:likeable_type => klass.to_s.classify)
96
+ end
100
97
  end
101
98
 
102
99
  module DislikeMethods
@@ -143,7 +140,16 @@ module Recommendable
143
140
  dislikes.map {|dislike| dislike.dislikeable}
144
141
  end
145
142
 
146
- alias_method :disliked_records, :disliked
143
+ # Get a list of records belonging to a passed class that self currently
144
+ # dislikes.
145
+ #
146
+ # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
147
+ # @return [Array] an array of ActiveRecord objects that self has disliked belonging to klass
148
+ def disliked_for(klass)
149
+ klass.to_s.classify.constantize.find dislikes_for(klass).map(&:dislikeable_id)
150
+ end
151
+
152
+ private
147
153
 
148
154
  # Get a list of Recommendable::Dislikes with a `#dislikeable_type` of the
149
155
  # passed class.
@@ -152,20 +158,8 @@ module Recommendable
152
158
  # @note You should not need to use this method. (see {#disliked_for})
153
159
  # @private
154
160
  def dislikes_for(klass)
155
- dislikes.where(:dislikeable_type => klassify(klass).to_s)
156
- end
157
-
158
- # Get a list of records belonging to a passed class that self currently
159
- # dislikes.
160
- #
161
- # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
162
- # @return [Array] an array of ActiveRecord objects that self has disliked belonging to klass
163
- def disliked_for(klass)
164
- klassify(klass).find dislikes_for(klass).map(&:dislikeable_id)
161
+ dislikes.where(:dislikeable_type => klass.to_s.classify)
165
162
  end
166
-
167
- alias_method :disliked_records_for, :disliked_for
168
- private :dislikes_for
169
163
  end
170
164
 
171
165
  module StashMethods
@@ -179,7 +173,7 @@ module Recommendable
179
173
  # @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
180
174
  def stash(object)
181
175
  raise RecordNotRecommendableError unless object.recommendable?
182
- return if has_rated?(object) || has_stashed?(object)
176
+ return if rated?(object) || stashed?(object)
183
177
  unignore(object)
184
178
  unpredict(object)
185
179
  stashed_items.create!(:stashable_id => object.id, :stashable_type => object.class.to_s)
@@ -190,7 +184,7 @@ module Recommendable
190
184
  #
191
185
  # @param [Object] object the object you want to check
192
186
  # @return true if self has stashed object, false if not
193
- def has_stashed?(object)
187
+ def stashed?(object)
194
188
  stashed_items.exists?(:stashable_id => object.id, :stashable_type => object.class.to_s)
195
189
  end
196
190
 
@@ -203,35 +197,32 @@ module Recommendable
203
197
  end
204
198
 
205
199
  # Get a list of records that self has currently stashed for later
206
-
200
+ #
207
201
  # @return [Array] an array of ActiveRecord objects that self has stashed
208
202
  def stashed
209
203
  stashed_items.map {|item| item.stashable}
210
204
  end
211
205
 
212
- alias_method :stashed_records, :stashed
213
-
214
- # Get a list of Recommendable::StashedItems with a stashable_type of the
215
- # passed class.
216
- #
217
- # @param [Class, String, Symbol] klass the class for which you would like to return self's stashed items. Can be the class constant, or a String/Symbol representation of the class name.
218
- # @note You should not need to use this method. (see {#stashed_for})
219
- # @private
220
- def stash_for(klass)
221
- stashed_items.where(:stashable_type => klassify(klass).to_s)
222
- end
223
-
224
206
  # Get a list of records belonging to a passed class that self currently
225
207
  # has stashed away for later.
226
208
  #
227
209
  # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
228
210
  # @return [Array] an array of ActiveRecord objects that self has stashed belonging to klass
229
211
  def stashed_for(klass)
230
- klassify(klass).find stash_for(klass).map(&:stashable_id)
212
+ klass.to_s.classify.constantize.find stash_for(klass).map(&:stashable_id)
231
213
  end
232
214
 
233
- alias_method :stashed_records_for, :stashed_for
234
- private :stash_for
215
+ private
216
+
217
+ # Get a list of Recommendable::StashedItems with a stashable_type of the
218
+ # passed class.
219
+ #
220
+ # @param [Class, String, Symbol] klass the class for which you would like to return self's stashed items. Can be the class constant, or a String/Symbol representation of the class name.
221
+ # @note You should not need to use this method. (see {#stashed_for})
222
+ # @private
223
+ def stash_for(klass)
224
+ stashed_items.where(:stashable_type => klass.to_s.classify)
225
+ end
235
226
  end
236
227
 
237
228
  module IgnoreMethods
@@ -245,7 +236,7 @@ module Recommendable
245
236
  # @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
246
237
  def ignore(object)
247
238
  raise RecordNotRecommendableError unless object.recommendable?
248
- return if has_ignored?(object)
239
+ return if ignored?(object)
249
240
  completely_unrecommend(object)
250
241
  ignores.create!(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s)
251
242
  true
@@ -255,7 +246,7 @@ module Recommendable
255
246
  #
256
247
  # @param [Object] object the object you want to check
257
248
  # @return true if self has ignored object, false if not
258
- def has_ignored?(object)
249
+ def ignored?(object)
259
250
  ignores.exists?(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s)
260
251
  end
261
252
 
@@ -274,7 +265,16 @@ module Recommendable
274
265
  ignores.map {|ignore| ignore.ignoreable}
275
266
  end
276
267
 
277
- alias_method :ignored_records, :ignored
268
+ # Get a list of records belonging to a passed class that self is
269
+ # currently ignoring.
270
+ #
271
+ # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
272
+ # @return [Array] an array of ActiveRecord objects that self has ignored belonging to klass
273
+ def ignored_for(klass)
274
+ klass.to_s.classify.constantize.find ignores_for(klass).map(&:ignoreable_id)
275
+ end
276
+
277
+ private
278
278
 
279
279
  # Get a list of Recommendable::Ignores with a `#ignoreable_type` of the
280
280
  # passed class.
@@ -283,20 +283,8 @@ module Recommendable
283
283
  # @note You should not need to use this method. (see {#ignored_for})
284
284
  # @private
285
285
  def ignores_for(klass)
286
- ignores.where(:ignoreable_type => klassify(klass).to_s)
286
+ ignores.where(:ignoreable_type => klass.to_s.classify)
287
287
  end
288
-
289
- # Get a list of records belonging to a passed class that self is
290
- # currently ignoring.
291
- #
292
- # @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
293
- # @return [Array] an array of ActiveRecord objects that self has ignored belonging to klass
294
- def ignored_for(klass)
295
- klassify(klass).find ignores_for(klass).map(&:ignoreable_id)
296
- end
297
-
298
- alias_method :ignored_records_for, :ignored_for
299
- private :ignores_for
300
288
  end
301
289
 
302
290
  module RecommendationMethods
@@ -304,14 +292,14 @@ module Recommendable
304
292
  #
305
293
  # @param [Object] object the object you want to check
306
294
  # @return true if self has liked or disliked object, false if not
307
- def has_rated?(object)
295
+ def rated?(object)
308
296
  likes?(object) || dislikes?(object)
309
297
  end
310
298
 
311
299
  # Checks to see if self has liked or disliked any objects yet.
312
300
  #
313
301
  # @return true if self has liked or disliked anything, false if not
314
- def has_rated_anything?
302
+ def rated_anything?
315
303
  likes.count > 0 || dislikes.count > 0
316
304
  end
317
305
 
@@ -378,7 +366,7 @@ module Recommendable
378
366
  prediction = Recommendable.redis.zrevrange(predictions_set_for(klass), i, i).first
379
367
  return recommendations unless prediction # User might not have enough recommendations to return
380
368
 
381
- object = klassify(klass).find(prediction.split(":")[1])
369
+ object = klass.to_s.classify.constantize.find(prediction.split(":")[1])
382
370
  recommendations << object
383
371
  i += 1
384
372
  end
@@ -404,28 +392,6 @@ module Recommendable
404
392
  -probability_of_liking(object)
405
393
  end
406
394
 
407
- # Checks how similar a passed rater is with self. This method calculates
408
- # a numeric similarity value that can fall between -1.0 and 1.0. A value of
409
- # 1.0 indicates that rater has the exact same likes and dislikes as self
410
- # while a value of -1.0 indicates that rater dislikes every object that self
411
- # likes and likes every object that self dislikes. A value of 0.0 would
412
- # indicate that the two users share no likes or dislikes.
413
- #
414
- # @param [Object] rater an ActiveRecord object declared to `act_as_recommendable_to`
415
- # @return [Float] the numeric similarity between self and rater
416
- # @note The returned value relies on which user the method is called on. current_user.similarity_with(rater) will not equal rater.similarity_with(current_user) unless their sets of likes and dislikes are identical. current_user.similarity_with(rater) will return 1.0 even if rater has several likes/dislikes that `current_user` does not.
417
- # @private
418
- def similarity_with(rater)
419
- rater.create_recommended_to_sets
420
- agreements = common_likes_with(rater, :return_records => false).size
421
- agreements += common_dislikes_with(rater, :return_records => false).size
422
- disagreements = disagreements_with(rater, :return_records => false).size
423
-
424
- similarity = (agreements - disagreements).to_f / (likes.count + dislikes.count)
425
- rater.destroy_recommended_to_sets
426
-
427
- return similarity
428
- end
429
395
  # Makes a call to Redis and intersects the sets of likes belonging to self
430
396
  # and rater.
431
397
  #
@@ -441,15 +407,15 @@ module Recommendable
441
407
 
442
408
  if options[:class]
443
409
  in_common = Recommendable.redis.sinter likes_set_for(options[:class]), rater.likes_set_for(options[:class])
444
- klassify(options[:class]).find in_common if options[:return_records]
410
+ options[:class].to_s.classify.constantize.find in_common if options[:return_records]
445
411
  else
446
412
  Recommendable.recommendable_classes.flat_map do |klass|
447
413
  in_common = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
448
414
 
449
415
  if options[:return_records]
450
- klassify(klass).find in_common
416
+ klass.to_s.classify.constantize.find in_common
451
417
  else
452
- in_common.map {|id| "#{klassify(klass)}:#{id}"}
418
+ in_common.map {|id| "#{klass.to_s.classify}:#{id}"}
453
419
  end
454
420
  end
455
421
  end
@@ -470,15 +436,15 @@ module Recommendable
470
436
 
471
437
  if options[:class]
472
438
  in_common = Recommendable.redis.sinter dislikes_set_for(options[:class]), rater.dislikes_set_for(options[:class])
473
- klassify(options[:class]).find in_common if options[:return_records]
439
+ options[:class].to_s.classify.constantize.find in_common if options[:return_records]
474
440
  else
475
441
  Recommendable.recommendable_classes.flat_map do |klass|
476
442
  in_common = Recommendable.redis.sinter(dislikes_set_for(klass), rater.dislikes_set_for(klass))
477
443
 
478
444
  if options[:return_records]
479
- klassify(klass).find in_common
445
+ klass.to_s.classify.constantize.find in_common
480
446
  else
481
- in_common.map {|id| "#{klassify(klass)}:#{id}"}
447
+ in_common.map {|id| "#{klass.to_s.classify}:#{id}"}
482
448
  end
483
449
  end
484
450
  end
@@ -502,21 +468,116 @@ module Recommendable
502
468
  if options[:class]
503
469
  disagreements = Recommendable.redis.sinter(likes_set_for(options[:class]), rater.likes_set_for(options[:class]))
504
470
  disagreements += Recommendable.redis.sinter(dislikes_set_for(options[:class]), rater.dislikes_set_for(options[:class]))
505
- klassify(options[:class]).find disagreements if options[:return_records]
471
+ options[:class].to_s.classify.constantize.find disagreements if options[:return_records]
506
472
  else
507
473
  Recommendable.recommendable_classes.flat_map do |klass|
508
474
  disagreements = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
509
475
  disagreements += Recommendable.redis.sinter(dislikes_set_for(klass), rater.dislikes_set_for(klass))
510
476
 
511
477
  if options[:return_records]
512
- klassify(klass).find disagreements
478
+ klass.to_s.classify.constantize.find disagreements
513
479
  else
514
- disagreements.map {|id| "#{klassify(klass)}:#{id}"}
480
+ disagreements.map {|id| "#{klass.to_s.classify}:#{id}"}
515
481
  end
516
482
  end
517
483
  end
518
484
  end
485
+
486
+ # Used internally during liking/disliking/stashing/ignoring objects. This
487
+ # will prep an object to be liked, disliked, etc. by making sure that self
488
+ # doesn't already have this item in their list of likes, dislikes, stashed
489
+ # items or ignored items.
490
+ #
491
+ # param [Object] object the object to destroy Recommendable models for
492
+ # @private
493
+ def completely_unrecommend(object)
494
+ unlike(object)
495
+ undislike(object)
496
+ unstash(object)
497
+ unignore(object)
498
+ unpredict(object)
499
+ end
500
+
501
+ protected
502
+
503
+ # @private
504
+ def likes_set_for(klass)
505
+ "#{self.class}:#{id}:likes:#{klass}"
506
+ end
519
507
 
508
+ # @private
509
+ def dislikes_set_for(klass)
510
+ "#{self.class}:#{id}:dislikes:#{klass}"
511
+ end
512
+
513
+ # Used for setup purposes. Creates and populates sets in redis containing
514
+ # self's likes and dislikes.
515
+ # @private
516
+ def create_recommended_to_sets
517
+ Recommendable.recommendable_classes.each do |klass|
518
+ likes_for(klass).each {|like| Recommendable.redis.sadd likes_set_for(klass), like.likeable_id }
519
+ dislikes_for(klass).each {|dislike| Recommendable.redis.sadd dislikes_set_for(klass), dislike.dislikeable_id }
520
+ end
521
+ end
522
+
523
+ # Used for teardown purposes. Destroys the redis sets containing self's
524
+ # likes and dislikes, as they are only used during the process of
525
+ # updating recommendations and similarity values.
526
+ # @private
527
+ def destroy_recommended_to_sets
528
+ Recommendable.recommendable_classes.each do |klass|
529
+ Recommendable.redis.del likes_set_for(klass)
530
+ Recommendable.redis.del dislikes_set_for(klass)
531
+ end
532
+ end
533
+
534
+ private
535
+
536
+ # Checks how similar a passed rater is with self. This method calculates
537
+ # a numeric similarity value that can fall between -1.0 and 1.0. A value of
538
+ # 1.0 indicates that rater has the exact same likes and dislikes as self
539
+ # while a value of -1.0 indicates that rater dislikes every object that self
540
+ # likes and likes every object that self dislikes. A value of 0.0 would
541
+ # indicate that the two users share no likes or dislikes.
542
+ #
543
+ # @param [Object] rater an ActiveRecord object declared to `act_as_recommendable_to`
544
+ # @return [Float] the numeric similarity between self and rater
545
+ # @note The returned value relies on which user the method is called on. current_user.similarity_with(rater) will not equal rater.similarity_with(current_user) unless their sets of likes and dislikes are identical. current_user.similarity_with(rater) will return 1.0 even if rater has several likes/dislikes that `current_user` does not.
546
+ # @private
547
+ def similarity_with(rater)
548
+ rater.create_recommended_to_sets
549
+ agreements = common_likes_with(rater, :return_records => false).size
550
+ agreements += common_dislikes_with(rater, :return_records => false).size
551
+ disagreements = disagreements_with(rater, :return_records => false).size
552
+
553
+ similarity = (agreements - disagreements).to_f / (likes.count + dislikes.count)
554
+ rater.destroy_recommended_to_sets
555
+
556
+ return similarity
557
+ end
558
+
559
+ # Used internally to update self's prediction values across all
560
+ # recommendable types. This is called in the Resque job to refresh
561
+ # recommendations.
562
+ #
563
+ # @private
564
+ def update_recommendations
565
+ Recommendable.recommendable_classes.each {|klass| update_recommendations_for klass}
566
+ end
567
+
568
+ # Used internally to update self's prediction values across a single
569
+ # recommendable type. Convenience method for {#update_recommendations}
570
+ #
571
+ # @param [Class] klass the recommendable type to update predictions for
572
+ # @private
573
+ def update_recommendations_for(klass)
574
+ klass.find_each do |object|
575
+ next if rated?(object) || !object.been_rated? || ignored?(object) || stashed?(object)
576
+ prediction = predict(object)
577
+ Recommendable.redis.zadd(predictions_set_for(object.class), prediction, object.redis_key) if prediction
578
+ end
579
+ end
580
+
520
581
  # Predict how likely it is that self will like a passed in object. This
521
582
  # probability is not based on percentage. 0.0 indicates that self will
522
583
  # neither like nor dislike the passed object. Values that approach Infinity
@@ -548,52 +609,23 @@ module Recommendable
548
609
  #
549
610
  # @private
550
611
  def update_similarities(rater_ids = nil)
551
- return unless has_rated_anything?
612
+ return unless rated_anything?
552
613
  create_recommended_to_sets
553
614
  rater_ids ||= Recommendable.user_class.select(:id).map!(&:id)
554
615
 
555
616
  Recommendable.user_class.find(rater_ids).each do |rater|
556
- next if self == rater || !rater.can_rate?
617
+ next if self == rater
557
618
  Recommendable.redis.zadd similarity_set, similarity_with(rater), rater.id
558
619
  end
559
620
 
560
621
  destroy_recommended_to_sets
561
622
  end
562
623
 
563
- # Used internally to update self's prediction values across all
564
- # recommendable types. This is called in the Resque job to refresh
565
- # recommendations.
566
- #
567
- # @private
568
- def update_recommendations
569
- Recommendable.recommendable_classes.each do |klass|
570
- update_recommendations_for(klass)
571
- end
572
- end
573
-
574
- # Used internally to update self's prediction values across a single
575
- # recommendable type. Convenience method for {#update_recommendations}
576
- #
577
- # @param [Class] klass the recommendable type to update predictions for
578
624
  # @private
579
- def update_recommendations_for(klass)
580
- klass.find_each do |object|
581
- next if has_rated?(object) || !object.has_been_rated? || has_ignored?(object) || has_stashed?(object)
582
- prediction = predict(object)
583
- Recommendable.redis.zadd(predictions_set_for(object.class), prediction, object.redis_key) if prediction
584
- end
625
+ def unpredict(object)
626
+ Recommendable.redis.zrem predictions_set_for(object.class), object.redis_key
585
627
  end
586
628
 
587
- # @private
588
- def likes_set_for(klass)
589
- "#{self.class}:#{id}:likes:#{klass}"
590
- end
591
-
592
- # @private
593
- def dislikes_set_for(klass)
594
- "#{self.class}:#{id}:dislikes:#{klass}"
595
- end
596
-
597
629
  # @private
598
630
  def similarity_set
599
631
  "#{self.class}:#{id}:similarities"
@@ -603,58 +635,6 @@ module Recommendable
603
635
  def predictions_set_for(klass)
604
636
  "#{self.class}:#{id}:predictions:#{klass}"
605
637
  end
606
-
607
- # @private
608
- def unpredict(object)
609
- Recommendable.redis.zrem predictions_set_for(object.class), object.redis_key
610
- end
611
-
612
- # Used for setup purposes. Creates and populates sets in redis containing
613
- # self's likes and dislikes.
614
- # @private
615
- def create_recommended_to_sets
616
- Recommendable.recommendable_classes.each do |klass|
617
- likes_for(klass).each {|like| Recommendable.redis.sadd likes_set_for(klass), like.likeable_id }
618
- dislikes_for(klass).each {|dislike| Recommendable.redis.sadd dislikes_set_for(klass), dislike.dislikeable_id }
619
- end
620
- end
621
-
622
- # Used for teardown purposes. Destroys the redis sets containing self's
623
- # likes and dislikes, as they are only used during the process of
624
- # updating recommendations and similarity values.
625
- # @private
626
- def destroy_recommended_to_sets
627
- Recommendable.recommendable_classes.each do |klass|
628
- Recommendable.redis.del likes_set_for(klass)
629
- Recommendable.redis.del dislikes_set_for(klass)
630
- end
631
- end
632
-
633
- # Used internally during liking/disliking/stashing/ignoring objects. This
634
- # will prep an object to be liked, disliked, etc. by making sure that self
635
- # doesn't already have this item in their list of likes, dislikes, stashed
636
- # items or ignored items.
637
- #
638
- # param [Object] object the object to destroy Recommendable models for
639
- # @private
640
- def completely_unrecommend(object)
641
- unlike(object)
642
- undislike(object)
643
- unstash(object)
644
- unignore(object)
645
- unpredict(object)
646
- end
647
-
648
- protected :likes_set_for, :dislikes_set_for, :create_recommended_to_sets,
649
- :destroy_recommended_to_sets
650
-
651
- private :similarity_set, :unpredict, :predictions_set_for,
652
- :update_recommendations_for, :update_recommendations,
653
- :update_similarities, :similarity_with, :predict
654
638
  end
655
639
  end
656
640
  end
657
-
658
- def klassify(klass)
659
- (klass.is_a?(String) || klass.is_a?(Symbol)) ? klass.to_s.camelize.constantize : klass
660
- end
@@ -2,14 +2,5 @@ module Recommendable
2
2
  class Railtie < Rails::Railtie
3
3
  ActiveRecord::Base.send :include, Recommendable::ActsAsRecommendedTo
4
4
  ActiveRecord::Base.send :include, Recommendable::ActsAsRecommendable
5
-
6
- # Force load models if in a non-development environment and not caching classes
7
- config.after_initialize do |app|
8
- force_load_models if !Rails.env.development? && !app.config.cache_classes
9
- end
10
-
11
- def self.force_load_models
12
- Dir["#{ Rails.root }/app/models/**/*.rb"].each { |m| load m }
13
- end
14
5
  end
15
6
  end
@@ -1,3 +1,3 @@
1
1
  module Recommendable
2
- VERSION = '0.1.3'
2
+ VERSION = '0.1.4'
3
3
  end
data/lib/recommendable.rb CHANGED
@@ -7,11 +7,7 @@ require 'recommendable/version'
7
7
 
8
8
  module Recommendable
9
9
  mattr_accessor :redis, :user_class
10
- mattr_writer :user_class, :recommendable_classes
11
-
12
- def self.user_class
13
- @@user_class.camelize.constantize
14
- end
10
+ mattr_writer :recommendable_classes
15
11
 
16
12
  def self.recommendable_classes
17
13
  @@recommendable_classes ||= []
@@ -64,14 +64,14 @@ class UserSpec < MiniTest::Spec
64
64
  it "should not be ignoring an item after liking it" do
65
65
  @user.ignore(@movie)
66
66
  @user.like(@movie).must_equal true
67
- @user.has_ignored?(@movie).must_equal false
67
+ @user.ignored?(@movie).must_equal false
68
68
  @user.likes?(@movie).must_equal true
69
69
  end
70
70
 
71
71
  it "should not have an item stashed after liking it" do
72
72
  @user.stash(@movie)
73
73
  @user.like(@movie).must_equal true
74
- @user.has_stashed?(@movie).must_equal false
74
+ @user.stashed?(@movie).must_equal false
75
75
  @user.likes?(@movie).must_equal true
76
76
  end
77
77
 
@@ -97,14 +97,14 @@ class UserSpec < MiniTest::Spec
97
97
  it "should not be ignoring an item after disliking it" do
98
98
  @user.ignore(@movie)
99
99
  @user.dislike(@movie).must_equal true
100
- @user.has_ignored?(@movie).must_equal false
100
+ @user.ignored?(@movie).must_equal false
101
101
  @user.dislikes?(@movie).must_equal true
102
102
  end
103
103
 
104
104
  it "should not have an item stashed after disliking it" do
105
105
  @user.stash(@movie)
106
106
  @user.dislike(@movie).must_equal true
107
- @user.has_stashed?(@movie).must_equal false
107
+ @user.stashed?(@movie).must_equal false
108
108
  @user.dislikes?(@movie).must_equal true
109
109
  end
110
110
 
@@ -116,21 +116,21 @@ class UserSpec < MiniTest::Spec
116
116
  @user.like(@movie)
117
117
  @user.ignore(@movie).must_equal true
118
118
  @user.likes?(@movie).must_equal false
119
- @user.has_ignored?(@movie).must_equal true
119
+ @user.ignored?(@movie).must_equal true
120
120
  end
121
121
 
122
122
  it "should not dislike an item after ignoring it" do
123
123
  @user.dislike(@movie)
124
124
  @user.ignore(@movie).must_equal true
125
125
  @user.dislikes?(@movie).must_equal false
126
- @user.has_ignored?(@movie).must_equal true
126
+ @user.ignored?(@movie).must_equal true
127
127
  end
128
128
 
129
129
  it "should not have an item stashed after ignoring it" do
130
130
  @user.stash(@movie)
131
131
  @user.ignore(@movie).must_equal true
132
- @user.has_stashed?(@movie).must_equal false
133
- @user.has_ignored?(@movie).must_equal true
132
+ @user.stashed?(@movie).must_equal false
133
+ @user.ignored?(@movie).must_equal true
134
134
  end
135
135
 
136
136
  it "should be able to stash a recommendable item" do
@@ -141,21 +141,21 @@ class UserSpec < MiniTest::Spec
141
141
  @user.like(@movie)
142
142
  @user.stash(@movie).must_be_nil
143
143
  @user.likes?(@movie).must_equal true
144
- @user.has_stashed?(@movie).must_equal false
144
+ @user.stashed?(@movie).must_equal false
145
145
  end
146
146
 
147
147
  it "should not stash a disliked item" do
148
148
  @user.dislike(@movie)
149
149
  @user.stash(@movie).must_be_nil
150
150
  @user.dislikes?(@movie).must_equal true
151
- @user.has_stashed?(@movie).must_equal false
151
+ @user.stashed?(@movie).must_equal false
152
152
  end
153
153
 
154
154
  it "should not have an item ignored after stashing it" do
155
155
  @user.ignore(@movie)
156
156
  @user.stash(@movie).must_equal true
157
- @user.has_ignored?(@movie).must_equal false
158
- @user.has_stashed?(@movie).must_equal true
157
+ @user.ignored?(@movie).must_equal false
158
+ @user.stashed?(@movie).must_equal true
159
159
  end
160
160
 
161
161
  it "should not be able to rate or ignore an item that is not recommendable. doing so should not be enough to create Redis keys" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recommendable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2012-02-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sqlite3
16
- requirement: &70109049948340 !ruby/object:Gem::Requirement
16
+ requirement: &70238356296840 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70109049948340
24
+ version_requirements: *70238356296840
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: minitest
27
- requirement: &70109049964200 !ruby/object:Gem::Requirement
27
+ requirement: &70238356312480 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70109049964200
35
+ version_requirements: *70238356312480
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: shoulda
38
- requirement: &70109049963440 !ruby/object:Gem::Requirement
38
+ requirement: &70238356311720 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70109049963440
46
+ version_requirements: *70238356311720
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: yard
49
- requirement: &70109049962620 !ruby/object:Gem::Requirement
49
+ requirement: &70238356310980 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 0.6.0
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70109049962620
57
+ version_requirements: *70238356310980
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: bundler
60
- requirement: &70109049961560 !ruby/object:Gem::Requirement
60
+ requirement: &70238356310160 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 1.0.0
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70109049961560
68
+ version_requirements: *70238356310160
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: jeweler
71
- requirement: &70109049959600 !ruby/object:Gem::Requirement
71
+ requirement: &70238356309340 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: 1.6.4
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70109049959600
79
+ version_requirements: *70238356309340
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: rcov
82
- requirement: &70109049958960 !ruby/object:Gem::Requirement
82
+ requirement: &70238356308780 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70109049958960
90
+ version_requirements: *70238356308780
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: rails
93
- requirement: &70109049957640 !ruby/object:Gem::Requirement
93
+ requirement: &70238356307740 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: 3.0.0
99
99
  type: :runtime
100
100
  prerelease: false
101
- version_requirements: *70109049957640
101
+ version_requirements: *70238356307740
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: redis
104
- requirement: &70109049971980 !ruby/object:Gem::Requirement
104
+ requirement: &70238356307200 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ~>
@@ -109,10 +109,10 @@ dependencies:
109
109
  version: 2.2.0
110
110
  type: :runtime
111
111
  prerelease: false
112
- version_requirements: *70109049971980
112
+ version_requirements: *70238356307200
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: resque
115
- requirement: &70109049971120 !ruby/object:Gem::Requirement
115
+ requirement: &70238356306660 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
118
  - - ~>
@@ -120,10 +120,10 @@ dependencies:
120
120
  version: 1.19.0
121
121
  type: :runtime
122
122
  prerelease: false
123
- version_requirements: *70109049971120
123
+ version_requirements: *70238356306660
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: resque-loner
126
- requirement: &70109049969980 !ruby/object:Gem::Requirement
126
+ requirement: &70238356305800 !ruby/object:Gem::Requirement
127
127
  none: false
128
128
  requirements:
129
129
  - - ~>
@@ -131,7 +131,7 @@ dependencies:
131
131
  version: 1.2.0
132
132
  type: :runtime
133
133
  prerelease: false
134
- version_requirements: *70109049969980
134
+ version_requirements: *70238356305800
135
135
  description: Allow a model (typically User) to Like and/or Dislike models in your
136
136
  app. Generate recommendations quickly using redis.
137
137
  email: david@davidcelis.com