recommendable 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|