Skip to content

Commit

Permalink
Add the somewhat raw meat of the engine, stir in some configuration.
Browse files Browse the repository at this point in the history
  • Loading branch information
David Celis committed Jan 24, 2012
1 parent 82caa75 commit 7444f04
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 29 deletions.
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ source "http://rubygems.org"
# Add dependencies required to use your gem here.
gem "rails", ">= 3.2.0"
gem "redis", ">= 2.2.0"
gem "app"

# Add dependencies to develop your gem here.
# Include everything needed to run rake, tests, features, etc.
Expand Down
9 changes: 9 additions & 0 deletions app/models/recommendable/dislike.rb
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
9 changes: 9 additions & 0 deletions app/models/recommendable/like.rb
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
7 changes: 0 additions & 7 deletions config/app.rb

This file was deleted.

5 changes: 0 additions & 5 deletions config/app/development.rb

This file was deleted.

5 changes: 0 additions & 5 deletions config/app/production.rb

This file was deleted.

6 changes: 0 additions & 6 deletions config/app/test.rb

This file was deleted.

16 changes: 16 additions & 0 deletions db/migrate/20120124193723_create_likes.rb
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
17 changes: 17 additions & 0 deletions db/migrate/20120124193728_create_dislikes.rb
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
14 changes: 13 additions & 1 deletion lib/recommendable.rb
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
10 changes: 9 additions & 1 deletion lib/recommendable/engine.rb
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
152 changes: 152 additions & 0 deletions lib/recommendable/rater.rb
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
3 changes: 0 additions & 3 deletions lib/recommendable/version.rb

This file was deleted.

0 comments on commit 7444f04

Please sign in to comment.