Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
rvm: 1.9.3
language: ruby
rvm:
- 1.9.3

39 changes: 13 additions & 26 deletions lib/recommendify/base.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
class Recommendify::Base

attr_reader :similarity_matrix, :input_matrices
attr_reader :similarity_matrix, :input_matrices, :max_neighbors

@@max_neighbors = nil
@@input_matrices = {}
def initialize(input_matrices = nil, opts = {})
@max_neighbors = opts[:max_neighbors] || Recommendify::DEFAULT_MAX_NEIGHBORS

def self.max_neighbors(n=nil)
return @@max_neighbors unless n
@@max_neighbors = n
end

def self.input_matrix(key, opts)
@@input_matrices[key] = opts
end

def self.input_matrices
@@input_matrices
end
@input_matrices = if input_matrices
Hash[input_matrices.map{ |key, opts|
opts.merge!(:key => key, :redis_prefix => redis_prefix)
[ key, Recommendify::InputMatrix.create(opts) ]
}]
else
{}
end

def initialize
@input_matrices = Hash[self.class.input_matrices.map{ |key, opts|
opts.merge!(:key => key, :redis_prefix => redis_prefix)
[ key, Recommendify::InputMatrix.create(opts) ]
}]
@similarity_matrix = Recommendify::SimilarityMatrix.new(
:max_neighbors => max_neighbors,
:key => :similarities,
Expand All @@ -34,10 +25,6 @@ def redis_prefix
"recommendify"
end

def max_neighbors
self.class.max_neighbors || Recommendify::DEFAULT_MAX_NEIGHBORS
end

def method_missing(method, *args)
if @input_matrices.has_key?(method)
@input_matrices[method]
Expand All @@ -57,7 +44,7 @@ def all_items
def for(item_id)
similarity_matrix[item_id].map do |item_id, similarity|
Recommendify::Neighbor.new(
:item_id => item_id,
:item_id => item_id,
:similarity => similarity
)
end.sort
Expand All @@ -69,7 +56,7 @@ def process!

def process_item!(item_id)
input_matrices.map do |k,m|
neighbors = m.similarities_for(item_id).map do |i,w|
neighbors = m.similarities_for(item_id).map do |i,w|
[i,w*m.weight]
end
similarity_matrix.update(item_id, neighbors)
Expand Down
2 changes: 0 additions & 2 deletions recommendify.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ Gem::Specification.new do |s|
s.description = %q{Recommendify is a distributed, incremental item-based recommendation engine for binary input ratings. It's based on ruby and redis and uses an approach called "Collaborative Filtering"}
s.licenses = ["MIT"]

s.extensions = ['ext/extconf.rb']

s.add_dependency "redis", ">= 2.2.2"

s.add_development_dependency "rspec", "~> 2.8.0"
Expand Down
68 changes: 27 additions & 41 deletions spec/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,40 @@

before(:each) do
flush_redis!
Recommendify::Base.send(:class_variable_set, :@@max_neighbors, nil)
Recommendify::Base.send(:class_variable_set, :@@input_matrices, {})
end

describe "configuration" do

it "should return default max_neighbors if not configured" do
Recommendify::DEFAULT_MAX_NEIGHBORS.should == 50
Recommendify::DEFAULT_MAX_NEIGHBORS.should == 50
sm = Recommendify::Base.new
sm.max_neighbors.should == 50
end

it "should remember max_neighbors if configured" do
Recommendify::Base.max_neighbors(23)
sm = Recommendify::Base.new
sm = Recommendify::Base.new(nil, max_neighbors: 23)
sm.max_neighbors.should == 23
end

it "should add an input_matrix by 'key'" do
Recommendify::Base.input_matrix(:myinput, :similarity_func => :jaccard)
Recommendify::Base.send(:class_variable_get, :@@input_matrices).keys.should == [:myinput]
sm = Recommendify::Base.new(myinput: { similarity_func: :jaccard })
sm.input_matrices.keys.should == [:myinput]
end

it "should retrieve an input_matrix on a new instance" do
Recommendify::Base.input_matrix(:myinput, :similarity_func => :jaccard)
sm = Recommendify::Base.new
sm = Recommendify::Base.new(myinput: { similarity_func: :jaccard })
lambda{ sm.myinput }.should_not raise_error
end

it "should retrieve an input_matrix on a new instance and correctly overload respond_to?" do
Recommendify::Base.input_matrix(:myinput, :similarity_func => :jaccard)
sm = Recommendify::Base.new
sm = Recommendify::Base.new(myinput: { similarity_func: :jaccard })
sm.respond_to?(:process!).should be_true
sm.respond_to?(:myinput).should be_true
sm.respond_to?(:fnord).should be_false
end

it "should retrieve an input_matrix on a new instance and intialize the correct class" do
Recommendify::Base.input_matrix(:myinput, :similarity_func => :jaccard)
sm = Recommendify::Base.new
sm = Recommendify::Base.new(myinput: { similarity_func: :jaccard })
sm.myinput.should be_a(Recommendify::JaccardInputMatrix)
end

Expand All @@ -52,41 +46,35 @@
describe "process_item!" do

it "should call similarities_for on each input_matrix" do
Recommendify::Base.input_matrix(:myfirstinput, :similarity_func => :jaccard)
Recommendify::Base.input_matrix(:mysecondinput, :similarity_func => :jaccard)
sm = Recommendify::Base.new
sm = Recommendify::Base.new(myfirstinput: { similarity_func: :jaccard }, mysecondinput: { similarity_func: :jaccard })
sm.myfirstinput.should_receive(:similarities_for).with("fnorditem").and_return([["fooitem",0.5]])
sm.mysecondinput.should_receive(:similarities_for).with("fnorditem").and_return([["fooitem",0.5]])
sm.similarity_matrix.stub!(:update)
sm.process_item!("fnorditem")
end

it "should call similarities_for on each input_matrix and add all outputs to the similarity matrix" do
Recommendify::Base.input_matrix(:myfirstinput, :similarity_func => :jaccard)
Recommendify::Base.input_matrix(:mysecondinput, :similarity_func => :jaccard)
sm = Recommendify::Base.new
sm.myfirstinput.should_receive(:similarities_for).and_return([["fooitem",0.5]])
sm = Recommendify::Base.new(myfirstinput: { similarity_func: :jaccard }, mysecondinput: { similarity_func: :jaccard })
sm.myfirstinput.should_receive(:similarities_for).and_return([["fooitem",0.5]])
sm.mysecondinput.should_receive(:similarities_for).and_return([["fooitem",0.75], ["baritem", 1.0]])
sm.similarity_matrix.should_receive(:update).with("fnorditem", [["fooitem",0.5]])
sm.similarity_matrix.should_receive(:update).with("fnorditem", [["fooitem",0.75], ["baritem", 1.0]])
sm.process_item!("fnorditem")
end

it "should call similarities_for on each input_matrix and add all outputs to the similarity matrix with weight" do
Recommendify::Base.input_matrix(:myfirstinput, :similarity_func => :jaccard, :weight => 4.0)
Recommendify::Base.input_matrix(:mysecondinput, :similarity_func => :jaccard)
sm = Recommendify::Base.new
sm.myfirstinput.should_receive(:similarities_for).and_return([["fooitem",0.5]])
sm = Recommendify::Base.new(myfirstinput: { similarity_func: :jaccard, weight: 4.0 }, mysecondinput: { similarity_func: :jaccard })
sm.myfirstinput.should_receive(:similarities_for).and_return([["fooitem",0.5]])
sm.mysecondinput.should_receive(:similarities_for).and_return([["fooitem",0.75], ["baritem", 1.0]])
sm.similarity_matrix.should_receive(:update).with("fnorditem", [["fooitem",2.0]])
sm.similarity_matrix.should_receive(:update).with("fnorditem", [["fooitem",0.75], ["baritem", 1.0]])
sm.process_item!("fnorditem")
end

it "should retrieve all items from all input matrices" do
Recommendify::Base.input_matrix(:anotherinput, :similarity_func => :test, :all_items => ["foo", "bar"])
Recommendify::Base.input_matrix(:yetanotherinput, :similarity_func => :test, :all_items => ["fnord", "shmoo"])
sm = Recommendify::Base.new
sm = Recommendify::Base.new(
anotherinput: { similarity_func: :test, all_items: ["foo", "bar"] },
yetanotherinput: { similarity_func: :test, all_items: ["fnord", "shmoo"] })
sm.all_items.length.should == 4
sm.all_items.should include("foo")
sm.all_items.should include("bar")
Expand All @@ -95,9 +83,9 @@
end

it "should retrieve all items from all input matrices (uniquely)" do
Recommendify::Base.input_matrix(:anotherinput, :similarity_func => :test, :all_items => ["foo", "bar"])
Recommendify::Base.input_matrix(:yetanotherinput, :similarity_func => :test, :all_items => ["fnord", "bar"])
sm = Recommendify::Base.new
sm = Recommendify::Base.new(
anotherinput: { similarity_func: :test, all_items: ["foo", "bar"] },
yetanotherinput: { similarity_func: :test, all_items: ["fnord"] })
sm.all_items.length.should == 3
sm.all_items.should include("foo")
sm.all_items.should include("bar")
Expand All @@ -109,17 +97,17 @@
describe "process!" do

it "should call process_item for all input_matrix.all_items's" do
Recommendify::Base.input_matrix(:anotherinput, :similarity_func => :test, :all_items => ["foo", "bar"])
Recommendify::Base.input_matrix(:yetanotherinput, :similarity_func => :test, :all_items => ["fnord", "shmoo"])
sm = Recommendify::Base.new
sm = Recommendify::Base.new(
anotherinput: { similarity_func: :test, all_items: ["foo", "bar"] },
yetanotherinput: { similarity_func: :test, all_items: ["fnord", "shmoo"] })
sm.should_receive(:process_item!).exactly(4).times
sm.process!
end

it "should call process_item for all input_matrix.all_items's (uniquely)" do
Recommendify::Base.input_matrix(:anotherinput, :similarity_func => :test, :all_items => ["foo", "bar"])
Recommendify::Base.input_matrix(:yetanotherinput, :similarity_func => :test, :all_items => ["fnord", "bar"])
sm = Recommendify::Base.new
sm = Recommendify::Base.new(
anotherinput: { similarity_func: :test, all_items: ["foo", "bar"] },
yetanotherinput: { similarity_func: :test, all_items: ["fnord"] })
sm.should_receive(:process_item!).exactly(3).times
sm.process!
end
Expand All @@ -133,7 +121,7 @@
sm.similarity_matrix.should_receive(:[]).with("fnorditem").and_return({:fooitem => 0.4, :baritem => 1.5})
sm.for("fnorditem").length.should == 2
end

it "should not throw exception for non existing items" do
sm = Recommendify::Base.new
sm.for("not_existing_item").length.should == 0
Expand Down Expand Up @@ -171,9 +159,7 @@
describe "delete_item!" do

it "should call delete_item on each input_matrix" do
Recommendify::Base.input_matrix(:myfirstinput, :similarity_func => :jaccard)
Recommendify::Base.input_matrix(:mysecondinput, :similarity_func => :jaccard)
sm = Recommendify::Base.new
sm = Recommendify::Base.new(myfirstinput: { similarity_func: :jaccard }, mysecondinput: { similarity_func: :jaccard })
sm.myfirstinput.should_receive(:delete_item).with("fnorditem")
sm.mysecondinput.should_receive(:delete_item).with("fnorditem")
sm.delete_item!("fnorditem")
Expand Down
16 changes: 4 additions & 12 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,14 @@ def redis_prefix

end


class TestRecommender < Recommendify::Base

input_matrix :jaccard_one,
:similarity_func => :jaccard

end

class Recommendify::TestInputMatrix

def initialize(opts)
@opts = opts
@opts = opts
end

def method_missing(method, *args)
@opts[method]
@opts[method]
end

end
end