forked from davidcelis/recommendable
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the somewhat raw meat of the engine, stir in some configuration.
- Loading branch information
David Celis
committed
Jan 24, 2012
1 parent
82caa75
commit 7444f04
Showing
13 changed files
with
225 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
module Recommendable | ||
class Dislike < ActiveRecord::Base | ||
belongs_to :user, :class_name => Recommendable.user_class.to_s | ||
belongs_to :dislikeable, :polymorphic => :true | ||
|
||
validates_uniqueness_of :dislikeable_id, :scope => [:user_id, :dislikeable_type], | ||
:message => "already exists for this item" | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
module Recommendable | ||
class Like < ActiveRecord::Base | ||
belongs_to :user, :class_name => Recommendable.user_class.to_s | ||
belongs_to :likeable, :polymorphic => :true | ||
|
||
validates_uniqueness_of :likeable_id, :scope => [:user_id, :likeable_type], | ||
:message => "already exists for this item" | ||
end | ||
end |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
class CreateLikes < ActiveRecord::Migration | ||
def up | ||
create_table :likes, :force => true do |t| | ||
t.references :likeable, :polymorphic => true | ||
t.timestamps | ||
end | ||
|
||
add_index :likes, :likeable_id | ||
add_index :likes, :likeable_type | ||
add_index :likes, :user_id, :likeable_id, :likeable_type, :unique => true | ||
end | ||
|
||
def down | ||
drop_table :likes | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
class CreateDislikes < ActiveRecord::Migration | ||
def up | ||
create_table :dislikes, :force => true do |t| | ||
t.references :user | ||
t.references :dislikeable, :polymorphic => true | ||
t.timestamps | ||
end | ||
|
||
add_index :dislikes, :dislikeable_id | ||
add_index :dislikes, :dislikeable_type | ||
add_index :dislikes, :user_id, :dislikeable_id, :dislikeable_type, :unique => true | ||
end | ||
|
||
def down | ||
drop_table :dislikes | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,16 @@ | ||
require "recommendable/engine" | ||
require "recommendable/rater" | ||
|
||
module Recommendable | ||
end | ||
mattr_accessor :user_class, :redis, :redis_host, :redis_port | ||
|
||
class << self | ||
def user_class | ||
@@user_class.constantize | ||
end | ||
|
||
def redis | ||
@@redis ||= Redis.new(@@redis_host, @@redis_port) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,13 @@ | ||
module Recommendable | ||
class Engine < ::Rails::Engine | ||
isolate_namespace Recommendable | ||
engine_name "recommendable" | ||
|
||
class << self | ||
attr_accessor :root | ||
|
||
def root | ||
@root ||= Pathname.new(File.expand_path('../../', __FILE__)) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
module Recommendable | ||
module Rater | ||
def included(base) | ||
base.extend ClassMethods | ||
end | ||
|
||
module ClassMethods | ||
def acts_as_liker | ||
class_eval do | ||
send :has_many, :likes, :as => :likeable | ||
send :include, LikeMethods | ||
send :include, RecommendationMethods | ||
end | ||
end | ||
|
||
def acts_as_disliker | ||
class_eval do | ||
send :has_many, :dislikes, :as => :dislikeable | ||
send :include, DislikeMethods | ||
send :include, RecommendationMethods | ||
end | ||
end | ||
end | ||
|
||
module LikeMethods | ||
def like(item) | ||
self.create_like(item) | ||
end | ||
|
||
def likes?(item) | ||
self.likes.where(:likeable_id => item.id, :likeable_type => item.class.to_s).first | ||
end | ||
|
||
def unlike(item) | ||
return unless like = self.likes?(item) | ||
like.destroy | ||
end | ||
|
||
def likes_for(klass) | ||
self.likes.where(:likeable_type => klass.to_s) | ||
end | ||
end | ||
|
||
module DislikeMethods | ||
def dislike(item) | ||
self.create_dislike(item) | ||
end | ||
|
||
def dislikes?(item) | ||
self.dislikes.where(:dislikeable_id => item.id, :dislikeable_type => item.class.to_s).first | ||
end | ||
|
||
def undislike(item) | ||
return unless dislike = self.dislikes?(item) | ||
dislike.destroy | ||
end | ||
|
||
def dislikes_for(klass) | ||
self.dislikes.where(:dislikeable_type => klass.to_s) | ||
end | ||
end | ||
|
||
module RecommendationMethods | ||
def similarity_with(rater) | ||
similarity = 0.0 | ||
|
||
return similarity if like_count + dislike_count == 0 | ||
|
||
agreements = common_likes_with(rater).size + common_dislikes(rater).size | ||
disagreements = disagreements_with(rater).size | ||
similarity = (agreements - disagreements).to_f / (like_count + dislike_count) | ||
|
||
return similarity | ||
end | ||
|
||
def common_likes_with(rater) | ||
Recommendable.redis.sinter "rater:#{id}:likes", "rater:#{rater.id}:likes" | ||
end | ||
|
||
def common_dislikes_with(rater) | ||
Recommendable.redis.sinter "rater:#{id}:dislikes", "rater:#{rater.id}:dislikes" | ||
end | ||
|
||
def disagreements_with(rater) | ||
Recommendable.redis.sinter("rater:#{id}:likes", "rater:#{rater.id}:dislikes") + | ||
Recommendable.redis.sinter("rater:#{id}:dislikes", "rater:#{rater.id}:likes") | ||
end | ||
|
||
def similar_raters(options) | ||
defaults = { :count => 10 } | ||
options.merge! defaults | ||
|
||
ids = Recommendable.redis.zrevrange "user_#{id}:similarities", 0, options[:count] - 1 | ||
class.find ids, order: "field(id, #{ids.join(',')})" | ||
end | ||
|
||
|
||
def update_similarities | ||
self.class.find_each do |rater| | ||
next if self == rater | ||
|
||
similarity = similarity_with(rater) | ||
Recommendable.redis.zadd "rater:#{id}:similarities", similarity, rater.id | ||
Recommendable.redis.zadd "rater:#{rater.id}:similarities", similarity, id | ||
end | ||
end | ||
|
||
def update_predictions_for(klass) | ||
klass.find_each do |item| | ||
unless has_liked?(item) || has_disliked?(item) | ||
prediction = predict(item) | ||
Recommendable.redis.zadd "rater:#{id}:predictions", prediction, item.id if prediction | ||
end | ||
end | ||
end | ||
|
||
def recommend_for(klass) | ||
predictions = [] | ||
return predictions if like_count + dislike_count == 0 | ||
return predictions if Recommendable.redis.zcard("rater:#{id}:predictions") == 0 | ||
i = options[:offset] | ||
|
||
until predictions.size == count | ||
item = klass.find Recommendable.redis.zrevrange("rater:#{id}:predictions", i, i).first | ||
predictions << item unless has_rated?(item) || has_hidden?(beer) | ||
i += 1 | ||
end | ||
|
||
return predictions | ||
end | ||
|
||
def predict(item) | ||
sum = 0.0 | ||
prediction = 0.0 | ||
|
||
Recommendable.redis.smembers("rateable:#{item.id}:liked_by").inject(sum) {|r, sum| sum += Recommendable.redis.zscore("rater:#{id}:similarities", r)} | ||
Recommendable.redis.smembers("rateable:#{item.id}:disliked_by").inject(sum) {|r, sum| sum -= Recommendable.redis.zscore("rater:#{id}:similarities", r)} | ||
|
||
rated_by = Recommendable.redis.scard("rateable:#{item.id}:liked_by") + Recommendable.redis.scard("rateable:#{item.id}:disliked_by") | ||
prediction = similarity_sum / rated_by.to_f unless rated_by == 0 | ||
end | ||
|
||
def probability_of_liking(item) | ||
Recommendable.redis.zscore "rater:#{id}:predictions", item.id | ||
end | ||
|
||
def probability_of_disliking(item) | ||
-probability_of_liking(item) | ||
end | ||
end | ||
end | ||
end |
This file was deleted.
Oops, something went wrong.