recommendable 0.1.2
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/.gitignore +57 -0
- data/.travis.yml +3 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +118 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +340 -0
- data/Rakefile +26 -0
- data/TODO +6 -0
- data/app/jobs/recommendable/recommendation_refresher.rb +12 -0
- data/app/models/recommendable/dislike.rb +9 -0
- data/app/models/recommendable/ignore.rb +9 -0
- data/app/models/recommendable/like.rb +9 -0
- data/app/models/recommendable/stashed_item.rb +9 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20120124193723_create_likes.rb +17 -0
- data/db/migrate/20120124193728_create_dislikes.rb +17 -0
- data/db/migrate/20120127092558_create_ignores.rb +17 -0
- data/db/migrate/20120131173909_create_stashed_items.rb +17 -0
- data/lib/generators/recommendable/USAGE +8 -0
- data/lib/generators/recommendable/install_generator.rb +47 -0
- data/lib/generators/recommendable/templates/initializer.rb +18 -0
- data/lib/recommendable.rb +19 -0
- data/lib/recommendable/acts_as_recommendable.rb +85 -0
- data/lib/recommendable/acts_as_recommended_to.rb +659 -0
- data/lib/recommendable/engine.rb +13 -0
- data/lib/recommendable/exceptions.rb +4 -0
- data/lib/recommendable/railtie.rb +6 -0
- data/lib/recommendable/version.rb +3 -0
- data/lib/tasks/recommendable_tasks.rake +1 -0
- data/recommendable.gemspec +32 -0
- data/script/rails +8 -0
- data/spec/configuration_spec.rb +9 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/bully.rb +2 -0
- data/spec/dummy/app/models/movie.rb +3 -0
- data/spec/dummy/app/models/php_framework.rb +2 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +56 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/recommendable.rb +16 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/dummy/db/migrate/20120128005553_create_likes.recommendable.rb +18 -0
- data/spec/dummy/db/migrate/20120128005554_create_dislikes.recommendable.rb +18 -0
- data/spec/dummy/db/migrate/20120128005555_create_ignores.recommendable.rb +18 -0
- data/spec/dummy/db/migrate/20120128020228_create_users.rb +9 -0
- data/spec/dummy/db/migrate/20120128020413_create_movies.rb +10 -0
- data/spec/dummy/db/migrate/20120128024632_create_php_frameworks.rb +9 -0
- data/spec/dummy/db/migrate/20120128024804_create_bullies.rb +9 -0
- data/spec/dummy/db/migrate/20120131195416_create_stashed_items.recommendable.rb +18 -0
- data/spec/dummy/db/schema.rb +89 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- 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 +6 -0
- data/spec/factories.rb +16 -0
- data/spec/models/dislike_spec.rb +27 -0
- data/spec/models/ignore_spec.rb +27 -0
- data/spec/models/like_spec.rb +28 -0
- data/spec/models/stashed_item_spec.rb +27 -0
- data/spec/models/user_benchmark_spec.rb +49 -0
- data/spec/models/user_spec.rb +320 -0
- data/spec/spec_helper.rb +28 -0
- metadata +254 -0
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
|
6
|
+
begin
|
7
|
+
Bundler.setup(:default, :development)
|
8
|
+
rescue Bundler::BundlerError => e
|
9
|
+
$stderr.puts e.message
|
10
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
11
|
+
exit e.status_code
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'rake'
|
15
|
+
|
16
|
+
require 'rake/testtask'
|
17
|
+
Rake::TestTask.new(:test) do |test|
|
18
|
+
test.libs << 'lib' << 'spec'
|
19
|
+
test.pattern = 'spec/**/*_spec.rb'
|
20
|
+
test.verbose = true
|
21
|
+
end
|
22
|
+
|
23
|
+
task :default => :test
|
24
|
+
|
25
|
+
require 'yard'
|
26
|
+
YARD::Rake::YardocTask.new
|
data/TODO
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module Recommendable
|
2
|
+
class RecommendationRefresher
|
3
|
+
include Resque::Plugins::UniqueJob
|
4
|
+
@queue = :recommendable
|
5
|
+
|
6
|
+
def self.perform(user_id)
|
7
|
+
user = Recommendable.user_class.find(user_id)
|
8
|
+
user.send :update_similarities
|
9
|
+
user.send :update_recommendations
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Recommendable
|
2
|
+
class Dislike < ActiveRecord::Base
|
3
|
+
belongs_to :user, :class_name => Recommendable.user_class.to_s
|
4
|
+
belongs_to :dislikeable, :polymorphic => :true
|
5
|
+
|
6
|
+
validates :user_id, :uniqueness => { :scope => [:dislikeable_id, :dislikeable_type],
|
7
|
+
:message => "has already disliked this item" }
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Recommendable
|
2
|
+
class Ignore < ActiveRecord::Base
|
3
|
+
belongs_to :user, :class_name => Recommendable.user_class.to_s
|
4
|
+
belongs_to :ignoreable, :polymorphic => :true
|
5
|
+
|
6
|
+
validates :user_id, :uniqueness => { :scope => [:ignoreable_id, :ignoreable_type],
|
7
|
+
:message => "has already ignored this item" }
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Recommendable
|
2
|
+
class Like < ActiveRecord::Base
|
3
|
+
belongs_to :user, :class_name => Recommendable.user_class.to_s
|
4
|
+
belongs_to :likeable, :polymorphic => :true
|
5
|
+
|
6
|
+
validates :user_id, :uniqueness => { :scope => [:likeable_id, :likeable_type],
|
7
|
+
:message => "has already liked this item" }
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Recommendable
|
2
|
+
class StashedItem < ActiveRecord::Base
|
3
|
+
belongs_to :user, :class_name => Recommendable.user_class.to_s
|
4
|
+
belongs_to :stashable, :polymorphic => :true
|
5
|
+
|
6
|
+
validates :user_id, :uniqueness => { :scope => [:stashable_id, :stashable_type],
|
7
|
+
:message => "has already stashed this item" }
|
8
|
+
end
|
9
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateLikes < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
create_table :likes do |t|
|
4
|
+
t.references :user
|
5
|
+
t.references :likeable, :polymorphic => true
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
|
9
|
+
add_index :likes, :likeable_id
|
10
|
+
add_index :likes, :likeable_type
|
11
|
+
add_index :likes, [:user_id, :likeable_id, :likeable_type], :unique => true, :name => "user_like_constraint"
|
12
|
+
end
|
13
|
+
|
14
|
+
def down
|
15
|
+
drop_table :likes
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateDislikes < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
create_table :dislikes do |t|
|
4
|
+
t.references :user
|
5
|
+
t.references :dislikeable, :polymorphic => true
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
|
9
|
+
add_index :dislikes, :dislikeable_id
|
10
|
+
add_index :dislikes, :dislikeable_type
|
11
|
+
add_index :dislikes, [:user_id, :dislikeable_id, :dislikeable_type], :unique => true, :name => "user_dislike_constraint"
|
12
|
+
end
|
13
|
+
|
14
|
+
def down
|
15
|
+
drop_table :dislikes
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateIgnores < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
create_table :ignores do |t|
|
4
|
+
t.references :user
|
5
|
+
t.references :ignoreable, :polymorphic => true
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
|
9
|
+
add_index :ignores, :ignoreable_id
|
10
|
+
add_index :ignores, :ignoreable_type
|
11
|
+
add_index :ignores, [:user_id, :ignoreable_id, :ignoreable_type], :unique => true, :name => "user_ignore_constraint"
|
12
|
+
end
|
13
|
+
|
14
|
+
def down
|
15
|
+
drop_table :ignores
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateStashedItems < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
create_table :stashed_items do |t|
|
4
|
+
t.references :user
|
5
|
+
t.references :stashable, :polymorphic => true
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
|
9
|
+
add_index :stashed_items, :stashable_id
|
10
|
+
add_index :stashed_items, :stashable_type
|
11
|
+
add_index :stashed_items, [:user_id, :stashable_id, :stashable_type], :unique => true, :name => "user_stashed_constraint"
|
12
|
+
end
|
13
|
+
|
14
|
+
def down
|
15
|
+
drop_table :stashed_items
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
Description:
|
2
|
+
This generator will install Recommendable's initializer.rb file and migrate the Like and Dislike tables into your database unless specified.
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails generate recommendable:install --user-class=YourUserClass
|
6
|
+
|
7
|
+
This will create:
|
8
|
+
config/initializers/recommendable.rb
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Recommendable
|
4
|
+
module Generators
|
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
|
+
argument :redis_host, :type => :string, :default => "localhost", :desc => "The hostname your redis server is running on."
|
8
|
+
argument :redis_port, :type => :string, :default => "6379", :desc => "The port your redis server is running on."
|
9
|
+
class_option :redis_socket, :type => :string, :desc => "Indicates the UNIX socket your redis server is running on (if it is)."
|
10
|
+
class_option :no_migrate, :type => :boolean, :default => false, :desc => "Skip migrations. The Like and Dislike tables will not be created."
|
11
|
+
|
12
|
+
source_root File.expand_path("../templates", __FILE__)
|
13
|
+
|
14
|
+
def add_recommendable_initializer
|
15
|
+
path = "#{Rails.root}/config/initializers/recommendable.rb"
|
16
|
+
if File.exists?(path)
|
17
|
+
puts "Skipping config/initializers/recommendable.rb creation; file already exists!"
|
18
|
+
else
|
19
|
+
puts "Adding Recommendable initializer (config/initializers/recommendable.rb)"
|
20
|
+
template "initializer.rb", path
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def install_migrations
|
25
|
+
puts "Copying migrations..."
|
26
|
+
Dir.chdir(Rails.root) { puts `rake recommendable:install:migrations` }
|
27
|
+
end
|
28
|
+
|
29
|
+
def run_migrations
|
30
|
+
unless options[:no_migrate]
|
31
|
+
puts "Running rake db:migrate"
|
32
|
+
puts `rake db:migrate`
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def finished
|
37
|
+
puts "Done! Recommendable has been successfully installed. Please configure it in config/intializers/recommendable.rb"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def user_class
|
43
|
+
user_model.camelize
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "redis"
|
2
|
+
require "resque"
|
3
|
+
require "resque-loner"
|
4
|
+
|
5
|
+
# What class will be liking/disliking objects and receiving recommendations?
|
6
|
+
Recommendable.user_class = "<%= user_class %>"
|
7
|
+
|
8
|
+
# Recommendable requires a connection to a running redis-server. Either create
|
9
|
+
# a new instance based on a host/port or UNIX socket, or pass in an existing
|
10
|
+
# Redis client instance.
|
11
|
+
<% if options.redis_socket %># <% end %>Recommendable.redis = Redis.new(:host => "<%= redis_host %>", :port => <%= redis_port %>)
|
12
|
+
|
13
|
+
# Connect to Redis via a UNIX socket instead
|
14
|
+
<% unless options.redis_socket %># <% end %>Recommendable.redis = Redis.new(:sock => "<%= options.redis_socket %>")
|
15
|
+
|
16
|
+
# Tell Redis which database to use (usually between 0 and 15). The default of 0
|
17
|
+
# is most likely okay unless you have another application using that database.
|
18
|
+
Recommendable.redis.select "0"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'recommendable/engine'
|
2
|
+
require 'recommendable/acts_as_recommended_to'
|
3
|
+
require 'recommendable/acts_as_recommendable'
|
4
|
+
require 'recommendable/exceptions'
|
5
|
+
require 'recommendable/railtie' if defined?(Rails)
|
6
|
+
require 'recommendable/version'
|
7
|
+
|
8
|
+
module Recommendable
|
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
|
15
|
+
|
16
|
+
def self.recommendable_classes
|
17
|
+
@@recommendable_classes ||= []
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Recommendable
|
2
|
+
module ActsAsRecommendable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def acts_as_recommendable
|
7
|
+
class_eval do
|
8
|
+
Recommendable.recommendable_classes << self
|
9
|
+
|
10
|
+
has_many :likes, :as => :likeable, :dependent => :destroy, :class_name => "Recommendable::Like"
|
11
|
+
has_many :dislikes, :as => :dislikeable, :dependent => :destroy, :class_name => "Recommendable::Dislike"
|
12
|
+
has_many :liked_by, :through => :likes, :source => :user
|
13
|
+
has_many :disliked_by, :through => :dislikes, :source => :user
|
14
|
+
has_many :ignores, :as => :ignoreable, :dependent => :destroy, :class_name => "Recommendable::Ignore"
|
15
|
+
has_many :stashes, :as => :stashable, :dependent => :destroy, :class_name => "Recommendable::StashedItem"
|
16
|
+
|
17
|
+
include LikeableMethods
|
18
|
+
include DislikeableMethods
|
19
|
+
|
20
|
+
def self.acts_as_recommendable? ; true ; end
|
21
|
+
|
22
|
+
def has_been_rated?
|
23
|
+
likes.count + dislikes.count > 0
|
24
|
+
end
|
25
|
+
|
26
|
+
# Used for setup purposes. Calls convenience methods to create sets
|
27
|
+
# in redis of users that both like and dislike this object.
|
28
|
+
# @return [Array] an array containing the liked_by set and the disliked_by set
|
29
|
+
# @private
|
30
|
+
def create_recommendable_sets
|
31
|
+
[create_liked_by_set, create_disliked_by_set]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Used for teardown purposes. Destroys the sets in redis created by
|
35
|
+
# {#create_recommendable_sets}
|
36
|
+
# @private
|
37
|
+
def destroy_recommendable_sets
|
38
|
+
Recommendable.redis.del "#{self.class}:#{id}:liked_by"
|
39
|
+
Recommendable.redis.del "#{self.class}:#{id}:disliked_by"
|
40
|
+
end
|
41
|
+
|
42
|
+
private :likes, :dislikes, :ignores, :stashes,
|
43
|
+
:create_recommendable_sets, :destroy_recommendable_sets
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def acts_as_recommendable? ; false ; end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Instance methods.
|
51
|
+
def recommendable? ; self.class.acts_as_recommendable? ; end
|
52
|
+
|
53
|
+
def redis_key ; "#{self.class}:#{id}" ; end
|
54
|
+
|
55
|
+
protected :redis_key
|
56
|
+
|
57
|
+
module LikeableMethods
|
58
|
+
# Used for setup purposes. Creates a set in redis containing users that
|
59
|
+
# have liked this object.
|
60
|
+
# @private
|
61
|
+
# @return [String] the key in Redis pointing to the set
|
62
|
+
def create_liked_by_set
|
63
|
+
set = "#{self.class}:#{id}:liked_by"
|
64
|
+
liked_by.each {|rater| Recommendable.redis.sadd set, rater.id}
|
65
|
+
return set
|
66
|
+
end
|
67
|
+
|
68
|
+
private :create_liked_by_set
|
69
|
+
end
|
70
|
+
|
71
|
+
module DislikeableMethods
|
72
|
+
# Used for setup purposes. Creates a set in redis containing users that
|
73
|
+
# have disliked this object.
|
74
|
+
# @private
|
75
|
+
# @return [String] the key in Redis pointing to the set
|
76
|
+
def create_disliked_by_set
|
77
|
+
set = "#{self.class}:#{id}:disliked_by"
|
78
|
+
disliked_by.each {|rater| Recommendable.redis.sadd set, rater.id}
|
79
|
+
return set
|
80
|
+
end
|
81
|
+
|
82
|
+
private :create_disliked_by_set
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,659 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Recommendable
|
4
|
+
module ActsAsRecommendedTo
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def acts_as_recommended_to
|
9
|
+
class_eval do
|
10
|
+
has_many :likes, :class_name => "Recommendable::Like", :dependent => :destroy
|
11
|
+
has_many :dislikes, :class_name => "Recommendable::Dislike", :dependent => :destroy
|
12
|
+
has_many :ignores, :class_name => "Recommendable::Ignore", :dependent => :destroy
|
13
|
+
has_many :stashed_items, :class_name => "Recommendable::StashedItem", :dependent => :destroy
|
14
|
+
|
15
|
+
include LikeMethods
|
16
|
+
include DislikeMethods
|
17
|
+
include StashMethods
|
18
|
+
include IgnoreMethods
|
19
|
+
include RecommendationMethods
|
20
|
+
|
21
|
+
def self.acts_as_recommended_to? ; true ; end
|
22
|
+
|
23
|
+
private :likes, :dislikes, :ignores, :stashed_items
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def acts_as_recommended_to? ; false ; end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Instance method.
|
31
|
+
def can_rate? ; self.class.acts_as_recommended_to? ; end
|
32
|
+
|
33
|
+
module LikeMethods
|
34
|
+
# Creates a Recommendable::Like to associate self to a passed object. If
|
35
|
+
# self is currently found to have disliked object, the corresponding
|
36
|
+
# Recommendable::Dislike will be destroyed. It will also be removed from
|
37
|
+
# the user's stash or ignores.
|
38
|
+
#
|
39
|
+
# @param [Object] object the object you want self to like.
|
40
|
+
# @return true if object has been liked
|
41
|
+
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
42
|
+
def like(object)
|
43
|
+
raise RecordNotRecommendableError unless object.recommendable?
|
44
|
+
return if likes?(object)
|
45
|
+
undislike(object)
|
46
|
+
unstash(object)
|
47
|
+
unignore(object)
|
48
|
+
unpredict(object)
|
49
|
+
likes.create!(:likeable_id => object.id, :likeable_type => object.class.to_s)
|
50
|
+
Resque.enqueue RecommendationRefresher, self.id
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks to see if self has already liked a passed object.
|
55
|
+
#
|
56
|
+
# @param [Object] object the object you want to check
|
57
|
+
# @return true if self likes object, false if not
|
58
|
+
def likes?(object)
|
59
|
+
likes.exists?(:likeable_id => object.id, :likeable_type => object.class.to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Destroys a Recommendable::Like currently associating self with object
|
63
|
+
#
|
64
|
+
# @param [Object] object the object you want to remove from self's likes
|
65
|
+
# @return true if object is unliked, nil if nothing happened
|
66
|
+
def unlike(object)
|
67
|
+
if likes.where(:likeable_id => object.id, :likeable_type => object.class.to_s).first.try(:destroy)
|
68
|
+
Resque.enqueue RecommendationRefresher, self.id
|
69
|
+
true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get a list of records that self currently likes
|
74
|
+
|
75
|
+
# @return [Array] an array of ActiveRecord objects that self has liked
|
76
|
+
def liked
|
77
|
+
likes.map {|like| like.likeable}
|
78
|
+
end
|
79
|
+
|
80
|
+
alias_method :liked_records, :liked
|
81
|
+
|
82
|
+
# Get a list of Recommendable::Likes with a `#likeable_type` of the passed
|
83
|
+
# class.
|
84
|
+
#
|
85
|
+
# @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.
|
86
|
+
# @note You should not need to use this method. (see {#liked_for})
|
87
|
+
# @private
|
88
|
+
def likes_for(klass)
|
89
|
+
likes.where(:likeable_type => klassify(klass).to_s)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get a list of records belonging to a passed class that self currently
|
93
|
+
# likes.
|
94
|
+
#
|
95
|
+
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
96
|
+
# @return [Array] an array of ActiveRecord objects that self has liked belonging to klass
|
97
|
+
def liked_for(klass)
|
98
|
+
klassify(klass).find likes_for(klass).map(&:likeable_id)
|
99
|
+
end
|
100
|
+
|
101
|
+
alias_method :liked_records_for, :liked_for
|
102
|
+
private :likes_for
|
103
|
+
end
|
104
|
+
|
105
|
+
module DislikeMethods
|
106
|
+
# Creates a Recommendable::Dislike to associate self to a passed object. If
|
107
|
+
# self is currently found to have liked object, the corresponding
|
108
|
+
# Recommendable::Like will be destroyed. It will also be removed from the
|
109
|
+
# user's stash or list of ignores.
|
110
|
+
#
|
111
|
+
# @param [Object] object the object you want self to dislike.
|
112
|
+
# @return true if object has been disliked
|
113
|
+
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
114
|
+
def dislike(object)
|
115
|
+
raise RecordNotRecommendableError unless object.recommendable?
|
116
|
+
return if dislikes?(object)
|
117
|
+
unlike(object)
|
118
|
+
unstash(object)
|
119
|
+
unignore(object)
|
120
|
+
unpredict(object)
|
121
|
+
dislikes.create!(:dislikeable_id => object.id, :dislikeable_type => object.class.to_s)
|
122
|
+
Resque.enqueue RecommendationRefresher, self.id
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
# Checks to see if self has already disliked a passed object.
|
127
|
+
#
|
128
|
+
# @param [Object] object the object you want to check
|
129
|
+
# @return true if self dislikes object, false if not
|
130
|
+
def dislikes?(object)
|
131
|
+
dislikes.exists?(:dislikeable_id => object.id, :dislikeable_type => object.class.to_s)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Destroys a Recommendable::Dislike currently associating self with object
|
135
|
+
#
|
136
|
+
# @param [Object] object the object you want to remove from self's dislikes
|
137
|
+
# @return true if object is removed from self's dislikes, nil if nothing happened
|
138
|
+
def undislike(object)
|
139
|
+
if dislikes.where(:dislikeable_id => object.id, :dislikeable_type => object.class.to_s).first.try(:destroy)
|
140
|
+
Resque.enqueue RecommendationRefresher, self.id
|
141
|
+
true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Get a list of records that self currently dislikes
|
146
|
+
|
147
|
+
# @return [Array] an array of ActiveRecord objects that self has disliked
|
148
|
+
def disliked
|
149
|
+
dislikes.map {|dislike| dislike.dislikeable}
|
150
|
+
end
|
151
|
+
|
152
|
+
alias_method :disliked_records, :disliked
|
153
|
+
|
154
|
+
# Get a list of Recommendable::Dislikes with a `#dislikeable_type` of the
|
155
|
+
# passed class.
|
156
|
+
#
|
157
|
+
# @param [Class, String, Symbol] klass the class for which you would like to return self's dislikes. Can be the class constant, or a String/Symbol representation of the class name.
|
158
|
+
# @note You should not need to use this method. (see {#disliked_for})
|
159
|
+
# @private
|
160
|
+
def dislikes_for(klass)
|
161
|
+
dislikes.where(:dislikeable_type => klassify(klass).to_s)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Get a list of records belonging to a passed class that self currently
|
165
|
+
# dislikes.
|
166
|
+
#
|
167
|
+
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
168
|
+
# @return [Array] an array of ActiveRecord objects that self has disliked belonging to klass
|
169
|
+
def disliked_for(klass)
|
170
|
+
klassify(klass).find dislikes_for(klass).map(&:dislikeable_id)
|
171
|
+
end
|
172
|
+
|
173
|
+
alias_method :disliked_records_for, :disliked_for
|
174
|
+
private :dislikes_for
|
175
|
+
end
|
176
|
+
|
177
|
+
module StashMethods
|
178
|
+
# Creates a Recommendable::StashedItem to associate self to a passed object.
|
179
|
+
# This will remove the item from this user's recommendations.
|
180
|
+
# If self is currently found to have liked or disliked the object, nothing
|
181
|
+
# will happen. It will, however, be unignored.
|
182
|
+
#
|
183
|
+
# @param [Object] object the object you want self to stash.
|
184
|
+
# @return true if object has been stashed
|
185
|
+
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
186
|
+
def stash(object)
|
187
|
+
raise RecordNotRecommendableError unless object.recommendable?
|
188
|
+
return if has_rated?(object) || has_stashed?(object)
|
189
|
+
unignore(object)
|
190
|
+
unpredict(object)
|
191
|
+
stashed_items.create!(:stashable_id => object.id, :stashable_type => object.class.to_s)
|
192
|
+
true
|
193
|
+
end
|
194
|
+
|
195
|
+
# Checks to see if self has already stashed a passed object for later.
|
196
|
+
#
|
197
|
+
# @param [Object] object the object you want to check
|
198
|
+
# @return true if self has stashed object, false if not
|
199
|
+
def has_stashed?(object)
|
200
|
+
stashed_items.exists?(:stashable_id => object.id, :stashable_type => object.class.to_s)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Destroys a Recommendable::StashedItem currently associating self with object
|
204
|
+
#
|
205
|
+
# @param [Object] object the object you want to remove from self's stash
|
206
|
+
# @return true if object is stashed, nil if nothing happened
|
207
|
+
def unstash(object)
|
208
|
+
true if stashed_items.where(:stashable_id => object.id, :stashable_type => object.class.to_s).first.try(:destroy)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Get a list of records that self has currently stashed for later
|
212
|
+
|
213
|
+
# @return [Array] an array of ActiveRecord objects that self has stashed
|
214
|
+
def stashed
|
215
|
+
stashed_items.map {|item| item.stashable}
|
216
|
+
end
|
217
|
+
|
218
|
+
alias_method :stashed_records, :stashed
|
219
|
+
|
220
|
+
# Get a list of Recommendable::StashedItems with a stashable_type of the
|
221
|
+
# passed class.
|
222
|
+
#
|
223
|
+
# @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.
|
224
|
+
# @note You should not need to use this method. (see {#stashed_for})
|
225
|
+
# @private
|
226
|
+
def stash_for(klass)
|
227
|
+
stashed_items.where(:stashable_type => klassify(klass).to_s)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Get a list of records belonging to a passed class that self currently
|
231
|
+
# has stashed away for later.
|
232
|
+
#
|
233
|
+
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
234
|
+
# @return [Array] an array of ActiveRecord objects that self has stashed belonging to klass
|
235
|
+
def stashed_for(klass)
|
236
|
+
klassify(klass).find stash_for(klass).map(&:stashable_id)
|
237
|
+
end
|
238
|
+
|
239
|
+
alias_method :stashed_records_for, :stashed_for
|
240
|
+
private :stash_for
|
241
|
+
end
|
242
|
+
|
243
|
+
module IgnoreMethods
|
244
|
+
# Creates a Recommendable::Ignore to associate self to a passed object. If
|
245
|
+
# self is currently found to have liked or dislikedobject, the
|
246
|
+
# corresponding Recommendable::Like or Recommendable::Dislike will be
|
247
|
+
# destroyed.
|
248
|
+
#
|
249
|
+
# @param [Object] object the object you want self to ignore.
|
250
|
+
# @return true if object has been ignored
|
251
|
+
# @raise [RecordNotRecommendableError] if you have not declared the passed object's model to `act_as_recommendable`
|
252
|
+
def ignore(object)
|
253
|
+
raise RecordNotRecommendableError unless object.recommendable?
|
254
|
+
return if has_ignored?(object)
|
255
|
+
unlike(object)
|
256
|
+
undislike(object)
|
257
|
+
unstash(object)
|
258
|
+
unpredict(object)
|
259
|
+
ignores.create!(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s)
|
260
|
+
true
|
261
|
+
end
|
262
|
+
|
263
|
+
# Checks to see if self has already ignored a passed object.
|
264
|
+
#
|
265
|
+
# @param [Object] object the object you want to check
|
266
|
+
# @return true if self has ignored object, false if not
|
267
|
+
def has_ignored?(object)
|
268
|
+
ignores.exists?(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Destroys a Recommendable::Ignore currently associating self with object
|
272
|
+
#
|
273
|
+
# @param [Object] object the object you want to remove from self's ignores
|
274
|
+
# @return true if object is removed from self's ignores, nil if nothing happened
|
275
|
+
def unignore(object)
|
276
|
+
true if ignores.where(:ignoreable_id => object.id, :ignoreable_type => object.class.to_s).first.try(:destroy)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Get a list of records that self is currently ignoring
|
280
|
+
|
281
|
+
# @return [Array] an array of ActiveRecord objects that self has ignored
|
282
|
+
def ignored
|
283
|
+
ignores.map {|ignore| ignore.ignoreable}
|
284
|
+
end
|
285
|
+
|
286
|
+
alias_method :ignored_records, :ignored
|
287
|
+
|
288
|
+
# Get a list of Recommendable::Ignores with a `#ignoreable_type` of the
|
289
|
+
# passed class.
|
290
|
+
#
|
291
|
+
# @param [Class, String, Symbol] klass the class for which you would like to return self's ignores. Can be the class constant, or a String/Symbol representation of the class name.
|
292
|
+
# @note You should not need to use this method. (see {#ignored_for})
|
293
|
+
# @private
|
294
|
+
def ignores_for(klass)
|
295
|
+
ignores.where(:ignoreable_type => klassify(klass).to_s)
|
296
|
+
end
|
297
|
+
|
298
|
+
# Get a list of records belonging to a passed class that self is
|
299
|
+
# currently ignoring.
|
300
|
+
#
|
301
|
+
# @param [Class, String, Symbol] klass the class of records. Can be the class constant, or a String/Symbol representation of the class name.
|
302
|
+
# @return [Array] an array of ActiveRecord objects that self has ignored belonging to klass
|
303
|
+
def ignored_for(klass)
|
304
|
+
klassify(klass).find ignores_for(klass).map(&:ignoreable_id)
|
305
|
+
end
|
306
|
+
|
307
|
+
alias_method :ignored_records_for, :ignored_for
|
308
|
+
private :ignores_for
|
309
|
+
end
|
310
|
+
|
311
|
+
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
|
+
# Checks to see if self has already liked or disliked a passed object.
|
317
|
+
#
|
318
|
+
# @param [Object] object the object you want to check
|
319
|
+
# @return true if self has liked or disliked object, false if not
|
320
|
+
def has_rated?(object)
|
321
|
+
likes?(object) || dislikes?(object)
|
322
|
+
end
|
323
|
+
|
324
|
+
# Checks to see if self has liked or disliked any objects yet.
|
325
|
+
#
|
326
|
+
# @return true if self has liked or disliked anything, false if not
|
327
|
+
def has_rated_anything?
|
328
|
+
likes.count > 0 || dislikes.count > 0
|
329
|
+
end
|
330
|
+
|
331
|
+
# Get a list of raters that have been found to be the most similar to
|
332
|
+
# self. They are sorted in a descending fashion with the most similar
|
333
|
+
# rater in the first index.
|
334
|
+
#
|
335
|
+
# @param [Hash] options the options for this query
|
336
|
+
# @option options [Fixnum] :count (10) The number of raters to return
|
337
|
+
# @return [Array] An array of instances of your user class
|
338
|
+
def similar_raters(options = {})
|
339
|
+
defaults = { :count => 10 }
|
340
|
+
options = defaults.merge(options)
|
341
|
+
|
342
|
+
rater_ids = Recommendable.redis.zrevrange(similarity_set, 0, options[:count] - 1).map(&:to_i)
|
343
|
+
raters = Recommendable.user_class.where("ID IN (?)", rater_ids)
|
344
|
+
|
345
|
+
# The query loses the ordering, so...
|
346
|
+
return raters.sort do |x, y|
|
347
|
+
rater_ids.index(x.id) <=> rater_ids.index(y.id)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# Get a list of recommendations for self. The whole point of this gem!
|
352
|
+
# Recommendations are returned in a descending order with the first index
|
353
|
+
# being the object that self has been found most likely to enjoy.
|
354
|
+
#
|
355
|
+
# @param [Hash] options the options for returning this list
|
356
|
+
# @option options [Fixnum] :count (10) the number of recommendations to get
|
357
|
+
# @return [Array] an array of ActiveRecord objects that are recommendable
|
358
|
+
def recommendations(options = {})
|
359
|
+
defaults = { :count => 10 }
|
360
|
+
options = defaults.merge options
|
361
|
+
return [] if likes.count + dislikes.count == 0
|
362
|
+
|
363
|
+
unioned_predictions = "#{self.class}:#{id}:predictions"
|
364
|
+
Recommendable.redis.zunionstore unioned_predictions, Recommendable.recommendable_classes.map {|klass| predictions_set_for(klass)}
|
365
|
+
return [] if Recommendable.redis.zcard(unioned_predictions) == 0
|
366
|
+
|
367
|
+
recommendations = Recommendable.redis.zrevrange(unioned_predictions, 0, options[:count]).map do |object|
|
368
|
+
klass, id = object.split(":")
|
369
|
+
klass.constantize.find(id)
|
370
|
+
end
|
371
|
+
|
372
|
+
Recommendable.redis.del unioned_predictions
|
373
|
+
return recommendations
|
374
|
+
end
|
375
|
+
|
376
|
+
# Get a list of recommendations for self on a single recommendable type.
|
377
|
+
# Recommendations are returned in a descending order with the first index
|
378
|
+
# being the object that self has been found most likely to enjoy.
|
379
|
+
#
|
380
|
+
# @param [Class, String, Symbol] klass the class to receive recommendations for. Can be the class constant, or a String/Symbol representation of the class name.
|
381
|
+
# @param [Hash] options the options for returning this list
|
382
|
+
# @option options [Fixnum] :count (10) the number of recommendations to get
|
383
|
+
# @return [Array] an array of ActiveRecord objects that are recommendable
|
384
|
+
def recommendations_for(klass, options = {})
|
385
|
+
defaults = { :count => 10 }
|
386
|
+
options = defaults.merge options
|
387
|
+
|
388
|
+
recommendations = []
|
389
|
+
return recommendations if likes_for(klass).count + dislikes_for(klass).count == 0 || Recommendable.redis.zcard(predictions_set_for(klass)) == 0
|
390
|
+
|
391
|
+
i = 0
|
392
|
+
until recommendations.size == options[:count]
|
393
|
+
prediction = Recommendable.redis.zrevrange(predictions_set_for(klass), i, i).first
|
394
|
+
return recommendations unless prediction # User might not have enough recommendations to return
|
395
|
+
|
396
|
+
object = klassify(klass).find(prediction.split(":")[1])
|
397
|
+
recommendations << object unless has_ignored?(object)
|
398
|
+
i += 1
|
399
|
+
end
|
400
|
+
|
401
|
+
return recommendations
|
402
|
+
end
|
403
|
+
|
404
|
+
# Return the value calculated by {#predict} on self for a passed object.
|
405
|
+
#
|
406
|
+
# @param [Object] object the object to fetch the probability for
|
407
|
+
# @return [Float] the likelihood of self liking the passed object
|
408
|
+
def probability_of_liking(object)
|
409
|
+
Recommendable.redis.zscore predictions_set_for(object.class), object.redis_key
|
410
|
+
end
|
411
|
+
|
412
|
+
# Return the negation of the value calculated by {#predict} on self
|
413
|
+
# for a passed object.
|
414
|
+
#
|
415
|
+
# @param [Object] object the object to fetch the probability for
|
416
|
+
# @return [Float] the likelihood of self disliking the passed object
|
417
|
+
# @see #probability of liking
|
418
|
+
def probability_of_disliking(object)
|
419
|
+
-probability_of_liking(object)
|
420
|
+
end
|
421
|
+
|
422
|
+
# Checks how similar a passed rater is with self. This method calculates
|
423
|
+
# a numeric similarity value that can fall between -1.0 and 1.0. A value of
|
424
|
+
# 1.0 indicates that rater has the exact same likes and dislikes as self
|
425
|
+
# while a value of -1.0 indicates that rater dislikes every object that self
|
426
|
+
# likes and likes every object that self dislikes. A value of 0.0 would
|
427
|
+
# indicate that the two users share no likes or dislikes.
|
428
|
+
#
|
429
|
+
# @param [Object] rater an ActiveRecord object declared to `act_as_recommendable_to`
|
430
|
+
# @return [Float] the numeric similarity between self and rater
|
431
|
+
# @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.
|
432
|
+
# @private
|
433
|
+
def similarity_with(rater)
|
434
|
+
rater.create_recommended_to_sets
|
435
|
+
agreements = common_likes_with(rater, :return_records => false).size
|
436
|
+
agreements += common_dislikes_with(rater, :return_records => false).size
|
437
|
+
disagreements = disagreements_with(rater).size
|
438
|
+
|
439
|
+
similarity = (agreements - disagreements).to_f / (likes.count + dislikes.count)
|
440
|
+
rater.destroy_recommended_to_sets
|
441
|
+
|
442
|
+
return similarity
|
443
|
+
end
|
444
|
+
# Makes a call to Redis and intersects the sets of likes belonging to self
|
445
|
+
# and rater.
|
446
|
+
#
|
447
|
+
# @param [Object] rater the person whose set of likes you wish to intersect with that of self
|
448
|
+
# @param [Hash] options the options for this intersection
|
449
|
+
# @option options [Class, String, Symbol] :class ('nil') Restrict the intersection to a single recommendable type. By default, all recomendable types are considered
|
450
|
+
# @option options [true, false] :return_records (true) Return the actual Model instances
|
451
|
+
# @return [Array] An array of IDs, or strings from Redis in the form of "#{likeable_type}:#{id}" if options[:class] is set
|
452
|
+
def common_likes_with(rater, options = {})
|
453
|
+
defaults = { :class => nil,
|
454
|
+
:return_records => true }
|
455
|
+
options = defaults.merge(options)
|
456
|
+
|
457
|
+
if options[:class]
|
458
|
+
in_common = Recommendable.redis.sinter likes_set_for(options[:class]), rater.likes_set_for(options[:class])
|
459
|
+
klassify(options[:class]).find in_common if options[:return_records]
|
460
|
+
else
|
461
|
+
Recommendable.recommendable_classes.flat_map do |klass|
|
462
|
+
in_common = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
|
463
|
+
|
464
|
+
if options[:return_records]
|
465
|
+
klassify(klass).find in_common if options[:return_records]
|
466
|
+
else
|
467
|
+
in_common.map {|id| "#{klassify(klass)}:#{id}"}
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
# Makes a call to Redis and intersects the sets of dislikes belonging to
|
474
|
+
# self and rater.
|
475
|
+
#
|
476
|
+
# @param [Object] rater the person whose set of dislikes you wish to intersect with that of self
|
477
|
+
# @param [Hash] options the options for this intersection
|
478
|
+
# @option options [Class, String, Symbol] :class ('nil') Restrict the intersection to a single recommendable type. By default, all recomendable types are considered
|
479
|
+
# @option options [true, false] :return_records (true) Return the actual Model instances
|
480
|
+
# @return [Array] An array of IDs, or strings from Redis in the form of #{dislikeable_type}:#{id}" if options[:class] is set
|
481
|
+
def common_dislikes_with(rater, options = {})
|
482
|
+
defaults = { :class => nil,
|
483
|
+
:return_records => true }
|
484
|
+
options = defaults.merge(options)
|
485
|
+
|
486
|
+
if options[:class]
|
487
|
+
in_common = Recommendable.redis.sinter dislikes_set_for(options[:class]), rater.dislikes_set_for(options[:class])
|
488
|
+
klassify(options[:class]).find in_common if options[:return_records]
|
489
|
+
else
|
490
|
+
Recommendable.recommendable_classes.flat_map do |klass|
|
491
|
+
in_common = Recommendable.redis.sinter(dislikes_set_for(klass), rater.dislikes_set_for(klass))
|
492
|
+
|
493
|
+
if options[:return_records]
|
494
|
+
klassify(klass).find in_common if options[:return_records]
|
495
|
+
else
|
496
|
+
in_common.map {|id| "#{klassify(klass)}:#{id}"}
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# Makes a call to Redis and intersects self's set of likes with rater's
|
503
|
+
# set of dislikes and vise versa. The idea here is that if self likes
|
504
|
+
# an object that rater dislikes, it is a disagreement and should count
|
505
|
+
# negatively towards their similarity.
|
506
|
+
#
|
507
|
+
# @param [Object] rater the person whose sets you wish to intersect with those of self
|
508
|
+
# @param [Hash] options the options for this intersection
|
509
|
+
# @option options [Class, String, Symbol] :class ('nil') Restrict the intersections to a single recommendable type. By default, all recomendable types are considered
|
510
|
+
# @option options [true, false] :return_records (true) Return the actual Model instances
|
511
|
+
# @return [Array] An array of IDs, or strings from Redis in the form of #{recommendable_type}:#{id}" if options[:class] is set
|
512
|
+
def disagreements_with(rater, options = {})
|
513
|
+
defaults = { :class => nil,
|
514
|
+
:return_records => true }
|
515
|
+
options = defaults.merge(options)
|
516
|
+
|
517
|
+
if options[:class]
|
518
|
+
disagreements = Recommendable.redis.sinter(likes_set_for(options[:class]), rater.likes_set_for(options[:class]))
|
519
|
+
disagreements += Recommendable.redis.sinter(dislikes_set_for(options[:class]), rater.dislikes_set_for(options[:class]))
|
520
|
+
klassify(options[:class]).find disagreements if options[:return_records]
|
521
|
+
else
|
522
|
+
Recommendable.recommendable_classes.flat_map do |klass|
|
523
|
+
disagreements = Recommendable.redis.sinter(likes_set_for(klass), rater.likes_set_for(klass))
|
524
|
+
disagreements += Recommendable.redis.sinter(dislikes_set_for(klass), rater.dislikes_set_for(klass))
|
525
|
+
|
526
|
+
if options[:return_records]
|
527
|
+
klassify(klass).find disagreements
|
528
|
+
else
|
529
|
+
disagreements.map {|id| "#{klassify(klass)}:#{id}"}
|
530
|
+
end
|
531
|
+
end
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
# Predict how likely it is that self will like a passed in object. This
|
536
|
+
# probability is not based on percentage. 0.0 indicates that self will
|
537
|
+
# neither like nor dislike the passed object. Values that approach Infinity
|
538
|
+
# indicate a rising probability of liking the passed object while values
|
539
|
+
# approaching -Infinity indicate a rising probability of disliking the
|
540
|
+
# passed object.
|
541
|
+
#
|
542
|
+
# @param [Object] object the object to check the likeliness of liking
|
543
|
+
# @return [Float] the probability that self will like object
|
544
|
+
# @private
|
545
|
+
def predict(object)
|
546
|
+
liked_by, disliked_by = object.send :create_recommendable_sets
|
547
|
+
rated_by = Recommendable.redis.scard(liked_by) + Recommendable.redis.scard(disliked_by)
|
548
|
+
similarity_sum = 0.0
|
549
|
+
prediction = 0.0
|
550
|
+
|
551
|
+
Recommendable.redis.smembers(liked_by).inject(similarity_sum) {|sum, r| sum += Recommendable.redis.zscore(similarity_set, r).to_f }
|
552
|
+
Recommendable.redis.smembers(disliked_by).inject(similarity_sum) {|sum, r| sum -= Recommendable.redis.zscore(similarity_set, r).to_f }
|
553
|
+
|
554
|
+
prediction = similarity_sum / rated_by.to_f
|
555
|
+
|
556
|
+
object.send :destroy_recommendable_sets
|
557
|
+
|
558
|
+
return prediction
|
559
|
+
end
|
560
|
+
|
561
|
+
# Used internally to update the similarity values between self and all
|
562
|
+
# other users. This is called in the Resque job to refresh recommendations.
|
563
|
+
#
|
564
|
+
# @private
|
565
|
+
def update_similarities
|
566
|
+
return unless has_rated_anything?
|
567
|
+
create_recommended_to_sets
|
568
|
+
|
569
|
+
Recommendable.user_class.find_each do |rater|
|
570
|
+
next if self == rater || !rater.can_rate?
|
571
|
+
Recommendable.redis.zadd similarity_set, similarity_with(rater), "#{rater.id}"
|
572
|
+
end
|
573
|
+
|
574
|
+
destroy_recommended_to_sets
|
575
|
+
end
|
576
|
+
|
577
|
+
# Used internally to update self's prediction values across all
|
578
|
+
# recommendable types. This is called in the Resque job to refresh
|
579
|
+
# recommendations.
|
580
|
+
#
|
581
|
+
# @private
|
582
|
+
def update_recommendations
|
583
|
+
Recommendable.recommendable_classes.each do |klass|
|
584
|
+
update_recommendations_for(klass)
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
# Used internally to update self's prediction values across a single
|
589
|
+
# recommendable type. Convenience method for {#update_recommendations}
|
590
|
+
#
|
591
|
+
# @param [Class] klass the recommendable type to update predictions for
|
592
|
+
# @private
|
593
|
+
def update_recommendations_for(klass)
|
594
|
+
klass.find_each do |object|
|
595
|
+
next if has_rated?(object) || !object.has_been_rated? || has_ignored?(object) || has_stashed?(object)
|
596
|
+
prediction = predict(object)
|
597
|
+
Recommendable.redis.zadd(predictions_set_for(object.class), prediction, object.redis_key) if prediction
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
# @private
|
602
|
+
def likes_set_for(klass)
|
603
|
+
"#{self.class}:#{id}:likes:#{klass}"
|
604
|
+
end
|
605
|
+
|
606
|
+
# @private
|
607
|
+
def dislikes_set_for(klass)
|
608
|
+
"#{self.class}:#{id}:dislikes:#{klass}"
|
609
|
+
end
|
610
|
+
|
611
|
+
# @private
|
612
|
+
def similarity_set
|
613
|
+
"#{self.class}:#{id}:similarities"
|
614
|
+
end
|
615
|
+
|
616
|
+
# @private
|
617
|
+
def predictions_set_for(klass)
|
618
|
+
"#{self.class}:#{id}:predictions:#{klass}"
|
619
|
+
end
|
620
|
+
|
621
|
+
# @private
|
622
|
+
def unpredict(object)
|
623
|
+
Recommendable.redis.zrem predictions_set_for(object.class), object.redis_key
|
624
|
+
end
|
625
|
+
|
626
|
+
# Used for setup purposes. Creates and populates sets in redis containing
|
627
|
+
# self's likes and dislikes.
|
628
|
+
# @private
|
629
|
+
def create_recommended_to_sets
|
630
|
+
Recommendable.recommendable_classes.each do |klass|
|
631
|
+
likes_for(klass).each {|like| Recommendable.redis.sadd likes_set_for(klass), like.likeable_id }
|
632
|
+
dislikes_for(klass).each {|dislike| Recommendable.redis.sadd dislikes_set_for(klass), dislike.dislikeable_id }
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
# Used for teardown purposes. Destroys the redis sets containing self's
|
637
|
+
# likes and dislikes, as they are only used during the process of
|
638
|
+
# updating recommendations and similarity values.
|
639
|
+
# @private
|
640
|
+
def destroy_recommended_to_sets
|
641
|
+
Recommendable.recommendable_classes.each do |klass|
|
642
|
+
Recommendable.redis.del likes_set_for(klass)
|
643
|
+
Recommendable.redis.del dislikes_set_for(klass)
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
protected :likes_set_for, :dislikes_set_for, :create_recommended_to_sets,
|
648
|
+
:destroy_recommended_to_sets
|
649
|
+
|
650
|
+
private :similarity_set, :unpredict, :predictions_set_for,
|
651
|
+
:update_recommendations_for, :update_recommendations,
|
652
|
+
:update_similarities, :similarity_with, :predict
|
653
|
+
end
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
def klassify(klass)
|
658
|
+
(klass.is_a?(String) || klass.is_a?(Symbol)) ? klass.to_s.camelize.constantize : klass
|
659
|
+
end
|