recommendable 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ Changelog
2
+ =========
3
+
4
+ 0.1.2 (current version)
5
+ -----------------------
6
+
7
+ * Fix an issue that could cause similarity values between users to be incorrect.
8
+ * `User#common_likes_with` and `User#common_dislikes_with` now return the actual Model instances by default. Accordingly, these methods are no longer private.
9
+
10
+ 0.1.1
11
+ -----
12
+ * Introduce the "stashed item" feature. This gives the ability for users to save an item in a list to try later, while at the same time removing it from their list of recommendations. This lets your users keep getting new recommendations without necessarily having to rate what they know they want to try.
13
+
14
+ 0.1.0
15
+ -----
16
+ * Welcome to recommendable!
data/README.markdown CHANGED
@@ -29,7 +29,9 @@ Add the following to your Rails application's `Gemfile`:
29
29
 
30
30
  After your `bundle install`, you can then run:
31
31
 
32
- $ rails g recommendable:install (--user-class=User)
32
+ ``` bash
33
+ $ rails g recommendable:install (--user-class=User)
34
+ ```
33
35
 
34
36
  After running the installation generator, you should double check
35
37
  `config/initializers/recommendable.rb` for options on configuring your Redis
@@ -42,7 +44,9 @@ to wait. In fact, you can run multiple resque workers if you wish.
42
44
 
43
45
  Assuming you have `redis-server` running...
44
46
 
45
- $ QUEUE=recommendable rake environment resque:work
47
+ ``` bash
48
+ $ QUEUE=recommendable rake environment resque:work
49
+ ```
46
50
 
47
51
  You can run this command multiple times if you wish to start more than one
48
52
  worker. This is the standard rake task for starting a Resque worker so, for
@@ -194,6 +198,8 @@ Movie.find_by_title('Star Wars: Episode I - The Phantom Menace').disliked_by
194
198
  #=> [#<User username: 'davidcelis'>]
195
199
  current_user.common_likes_with(friend)
196
200
  #=> [#<Movie title: '2001: A Space Odyssey', year: 1968>]
201
+ current_user.common_likes_with(friend, :class => Show)
202
+ #=> []
197
203
  ```
198
204
 
199
205
  `common_dislikes_with` and `disagreements_with` are available for similar use.
@@ -322,7 +328,7 @@ Links
322
328
  -----
323
329
  * Code: `git clone git://github.com/davidcelis/recommendable.git`
324
330
  * Home: <http://github.com/davidcelis/recommendable>
325
- * Docs: <http://rubydoc.info/gems/recommendable/0.1.0/frames>
331
+ * Docs: <http://rubydoc.info/gems/recommendable/frames>
326
332
  * Bugs: <http://github.com/davidcelis/recommendable/issues>
327
333
  * Gems: <http://rubygems.org/gems/recommendable>
328
334
 
data/TODO CHANGED
@@ -1,6 +1,5 @@
1
1
  = TODO
2
2
 
3
- == Next update
4
-
5
- * Make public methods to return likes/dislikes that are common between two users
3
+ * Recalculate similarity values ONLY for users that have liked/disliked the item
4
+ that self just liked/disliked
6
5
  * Allow the option NOT to queue up on like/dislike/ignore?
@@ -3,9 +3,10 @@ module Recommendable
3
3
  include Resque::Plugins::UniqueJob
4
4
  @queue = :recommendable
5
5
 
6
- def self.perform(user_id)
6
+ def self.perform(user_id, other_ids)
7
7
  user = Recommendable.user_class.find(user_id)
8
- user.send :update_similarities
8
+ return if other_ids.empty?
9
+ user.send :update_similarities, other_ids
9
10
  user.send :update_recommendations
10
11
  end
11
12
  end
@@ -22,6 +22,19 @@ module Recommendable
22
22
  def has_been_rated?
23
23
  likes.count + dislikes.count > 0
24
24
  end
25
+
26
+ # Returns an array of users that have liked or disliked this item.
27
+ # @return [Array] an array of users
28
+ def rated_by
29
+ liked_by + disliked_by
30
+ 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
25
38
 
26
39
  # Used for setup purposes. Calls convenience methods to create sets
27
40
  # in redis of users that both like and dislike this object.
@@ -39,7 +52,7 @@ module Recommendable
39
52
  Recommendable.redis.del "#{self.class}:#{id}:disliked_by"
40
53
  end
41
54
 
42
- private :likes, :dislikes, :ignores, :stashes,
55
+ private :likes, :dislikes, :ignores, :stashes, :rates_by,
43
56
  :create_recommendable_sets, :destroy_recommendable_sets
44
57
  end
45
58
  end
@@ -42,12 +42,9 @@ module Recommendable
42
42
  def like(object)
43
43
  raise RecordNotRecommendableError unless object.recommendable?
44
44
  return if likes?(object)
45
- undislike(object)
46
- unstash(object)
47
- unignore(object)
48
- unpredict(object)
45
+ completely_unrecommend(object)
49
46
  likes.create!(:likeable_id => object.id, :likeable_type => object.class.to_s)
50
- Resque.enqueue RecommendationRefresher, self.id
47
+ Resque.enqueue RecommendationRefresher, self.id, object.send(:rates_by)
51
48
  true
52
49
  end
53
50
 
@@ -65,7 +62,7 @@ module Recommendable
65
62
  # @return true if object is unliked, nil if nothing happened
66
63
  def unlike(object)
67
64
  if likes.where(:likeable_id => object.id, :likeable_type => object.class.to_s).first.try(:destroy)
68
- Resque.enqueue RecommendationRefresher, self.id
65
+ Resque.enqueue RecommendationRefresher, self.id, object.send(:rates_by)
69
66
  true
70
67
  end
71
68
  end
@@ -114,12 +111,9 @@ module Recommendable
114
111
  def dislike(object)
115
112
  raise RecordNotRecommendableError unless object.recommendable?
116
113
  return if dislikes?(object)
117
- unlike(object)
118
- unstash(object)
119
- unignore(object)
120
- unpredict(object)
114
+ completely_unrecommend(object)
121
115
  dislikes.create!(:dislikeable_id => object.id, :dislikeable_type => object.class.to_s)
122
- Resque.enqueue RecommendationRefresher, self.id
116
+ Resque.enqueue RecommendationRefresher, self.id, object.send(:rates_by)
123
117
  true
124
118
  end
125
119
 
@@ -137,7 +131,7 @@ module Recommendable
137
131
  # @return true if object is removed from self's dislikes, nil if nothing happened
138
132
  def undislike(object)
139
133
  if dislikes.where(:dislikeable_id => object.id, :dislikeable_type => object.class.to_s).first.try(:destroy)
140
- Resque.enqueue RecommendationRefresher, self.id
134
+ Resque.enqueue RecommendationRefresher, self.id, object.send(:rates_by)
141
135
  true
142
136
  end
143
137
  end
@@ -252,10 +246,7 @@ module Recommendable
252
246
  def ignore(object)
253
247
  raise RecordNotRecommendableError unless object.recommendable?
254
248
  return if has_ignored?(object)
255
- unlike(object)
256
- undislike(object)
257
- unstash(object)
258
- unpredict(object)
249
+ completely_unrecommend(object)
259
250
  ignores.create!(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s)
260
251
  true
261
252
  end
@@ -309,10 +300,6 @@ module Recommendable
309
300
  end
310
301
 
311
302
  module RecommendationMethods
312
- def self.acts_as_recommended_to? ; true ; end
313
-
314
- def can_receive_recommendations? ; self.class.acts_as_recommended_to? ; end
315
-
316
303
  # Checks to see if self has already liked or disliked a passed object.
317
304
  #
318
305
  # @param [Object] object the object you want to check
@@ -362,15 +349,13 @@ module Recommendable
362
349
 
363
350
  unioned_predictions = "#{self.class}:#{id}:predictions"
364
351
  Recommendable.redis.zunionstore unioned_predictions, Recommendable.recommendable_classes.map {|klass| predictions_set_for(klass)}
365
- return [] if Recommendable.redis.zcard(unioned_predictions) == 0
366
352
 
367
353
  recommendations = Recommendable.redis.zrevrange(unioned_predictions, 0, options[:count]).map do |object|
368
354
  klass, id = object.split(":")
369
355
  klass.constantize.find(id)
370
356
  end
371
357
 
372
- Recommendable.redis.del unioned_predictions
373
- return recommendations
358
+ Recommendable.redis.del(unioned_predictions) and return recommendations
374
359
  end
375
360
 
376
361
  # Get a list of recommendations for self on a single recommendable type.
@@ -385,16 +370,16 @@ module Recommendable
385
370
  defaults = { :count => 10 }
386
371
  options = defaults.merge options
387
372
 
388
- recommendations = []
389
- return recommendations if likes_for(klass).count + dislikes_for(klass).count == 0 || Recommendable.redis.zcard(predictions_set_for(klass)) == 0
373
+ return [] if likes_for(klass).count + dislikes_for(klass).count == 0 || Recommendable.redis.zcard(predictions_set_for(klass)) == 0
390
374
 
375
+ recommendations = []
391
376
  i = 0
392
377
  until recommendations.size == options[:count]
393
378
  prediction = Recommendable.redis.zrevrange(predictions_set_for(klass), i, i).first
394
379
  return recommendations unless prediction # User might not have enough recommendations to return
395
380
 
396
381
  object = klassify(klass).find(prediction.split(":")[1])
397
- recommendations << object unless has_ignored?(object)
382
+ recommendations << object
398
383
  i += 1
399
384
  end
400
385
 
@@ -414,7 +399,7 @@ module Recommendable
414
399
  #
415
400
  # @param [Object] object the object to fetch the probability for
416
401
  # @return [Float] the likelihood of self disliking the passed object
417
- # @see #probability of liking
402
+ # @see {#probability_of_liking}
418
403
  def probability_of_disliking(object)
419
404
  -probability_of_liking(object)
420
405
  end
@@ -434,7 +419,7 @@ module Recommendable
434
419
  rater.create_recommended_to_sets
435
420
  agreements = common_likes_with(rater, :return_records => false).size
436
421
  agreements += common_dislikes_with(rater, :return_records => false).size
437
- disagreements = disagreements_with(rater).size
422
+ disagreements = disagreements_with(rater, :return_records => false).size
438
423
 
439
424
  similarity = (agreements - disagreements).to_f / (likes.count + dislikes.count)
440
425
  rater.destroy_recommended_to_sets
@@ -462,7 +447,7 @@ module Recommendable
462
447
  in_common = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
463
448
 
464
449
  if options[:return_records]
465
- klassify(klass).find in_common if options[:return_records]
450
+ klassify(klass).find in_common
466
451
  else
467
452
  in_common.map {|id| "#{klassify(klass)}:#{id}"}
468
453
  end
@@ -491,7 +476,7 @@ module Recommendable
491
476
  in_common = Recommendable.redis.sinter(dislikes_set_for(klass), rater.dislikes_set_for(klass))
492
477
 
493
478
  if options[:return_records]
494
- klassify(klass).find in_common if options[:return_records]
479
+ klassify(klass).find in_common
495
480
  else
496
481
  in_common.map {|id| "#{klassify(klass)}:#{id}"}
497
482
  end
@@ -562,13 +547,14 @@ module Recommendable
562
547
  # other users. This is called in the Resque job to refresh recommendations.
563
548
  #
564
549
  # @private
565
- def update_similarities
550
+ def update_similarities(rater_ids = nil)
566
551
  return unless has_rated_anything?
567
552
  create_recommended_to_sets
553
+ rater_ids ||= Recommendable.user_class.select(:id).map!(&:id)
568
554
 
569
- Recommendable.user_class.find_each do |rater|
555
+ Recommendable.user_class.find(rater_ids).each do |rater|
570
556
  next if self == rater || !rater.can_rate?
571
- Recommendable.redis.zadd similarity_set, similarity_with(rater), "#{rater.id}"
557
+ Recommendable.redis.zadd similarity_set, similarity_with(rater), rater.id
572
558
  end
573
559
 
574
560
  destroy_recommended_to_sets
@@ -644,6 +630,21 @@ module Recommendable
644
630
  end
645
631
  end
646
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
+
647
648
  protected :likes_set_for, :dislikes_set_for, :create_recommended_to_sets,
648
649
  :destroy_recommended_to_sets
649
650
 
@@ -2,5 +2,14 @@ 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
5
14
  end
6
15
  end
@@ -1,3 +1,3 @@
1
1
  module Recommendable
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.3'
3
3
  end
@@ -4,7 +4,7 @@ require File.expand_path('lib/recommendable/version')
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "recommendable"
6
6
  s.version = Recommendable::VERSION
7
- s.date = "2012-01-28"
7
+ s.date = Time.now.strftime('%Y-%m-%d')
8
8
 
9
9
  s.authors = ["David Celis"]
10
10
  s.email = "david@davidcelis.com"
Binary file
@@ -35,8 +35,8 @@ class UserBenchmarkSpec < MiniTest::Unit::TestCase
35
35
  @users.sample.send(@actions.sample, m)
36
36
  end
37
37
 
38
- @user.update_similarities
39
- @user.update_recommendations
38
+ @user.send :update_similarities
39
+ @user.send :update_recommendations
40
40
  Recommendable.redis.flushdb
41
41
 
42
42
  User.delete_all
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.2
4
+ version: 0.1.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-28 00:00:00.000000000 Z
12
+ date: 2012-02-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sqlite3
16
- requirement: &70238498735420 !ruby/object:Gem::Requirement
16
+ requirement: &70109049948340 !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: *70238498735420
24
+ version_requirements: *70109049948340
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: minitest
27
- requirement: &70238498734880 !ruby/object:Gem::Requirement
27
+ requirement: &70109049964200 !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: *70238498734880
35
+ version_requirements: *70109049964200
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: shoulda
38
- requirement: &70238498734380 !ruby/object:Gem::Requirement
38
+ requirement: &70109049963440 !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: *70238498734380
46
+ version_requirements: *70109049963440
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: yard
49
- requirement: &70238498749840 !ruby/object:Gem::Requirement
49
+ requirement: &70109049962620 !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: *70238498749840
57
+ version_requirements: *70109049962620
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: bundler
60
- requirement: &70238498749020 !ruby/object:Gem::Requirement
60
+ requirement: &70109049961560 !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: *70238498749020
68
+ version_requirements: *70109049961560
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: jeweler
71
- requirement: &70238498748140 !ruby/object:Gem::Requirement
71
+ requirement: &70109049959600 !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: *70238498748140
79
+ version_requirements: *70109049959600
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: rcov
82
- requirement: &70238498746940 !ruby/object:Gem::Requirement
82
+ requirement: &70109049958960 !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: *70238498746940
90
+ version_requirements: *70109049958960
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: rails
93
- requirement: &70238498745320 !ruby/object:Gem::Requirement
93
+ requirement: &70109049957640 !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: *70238498745320
101
+ version_requirements: *70109049957640
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: redis
104
- requirement: &70238498744040 !ruby/object:Gem::Requirement
104
+ requirement: &70109049971980 !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: *70238498744040
112
+ version_requirements: *70109049971980
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: resque
115
- requirement: &70238498758460 !ruby/object:Gem::Requirement
115
+ requirement: &70109049971120 !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: *70238498758460
123
+ version_requirements: *70109049971120
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: resque-loner
126
- requirement: &70238498757620 !ruby/object:Gem::Requirement
126
+ requirement: &70109049969980 !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: *70238498757620
134
+ version_requirements: *70109049969980
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
@@ -141,17 +141,18 @@ extra_rdoc_files: []
141
141
  files:
142
142
  - .gitignore
143
143
  - .travis.yml
144
+ - CHANGELOG.markdown
144
145
  - Gemfile
145
146
  - Gemfile.lock
146
147
  - LICENSE.txt
147
148
  - README.markdown
148
149
  - Rakefile
149
150
  - TODO
150
- - app/jobs/recommendable/recommendation_refresher.rb
151
151
  - app/models/recommendable/dislike.rb
152
152
  - app/models/recommendable/ignore.rb
153
153
  - app/models/recommendable/like.rb
154
154
  - app/models/recommendable/stashed_item.rb
155
+ - app/workers/recommendable/recommendation_refresher.rb
155
156
  - config/routes.rb
156
157
  - db/migrate/20120124193723_create_likes.rb
157
158
  - db/migrate/20120124193728_create_dislikes.rb