diff --git a/.travis.yml b/.travis.yml index 22444f2..00fa315 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,4 @@ -rvm: 1.9.3 +language: ruby +rvm: + - 1.9.3 + diff --git a/lib/recommendify/base.rb b/lib/recommendify/base.rb index 620a3fd..0f13989 100644 --- a/lib/recommendify/base.rb +++ b/lib/recommendify/base.rb @@ -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, @@ -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] @@ -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 @@ -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) diff --git a/recommendify.gemspec b/recommendify.gemspec index e321d4d..3a98ca4 100644 --- a/recommendify.gemspec +++ b/recommendify.gemspec @@ -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" diff --git a/spec/base_spec.rb b/spec/base_spec.rb index 8d071ee..925cce7 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -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 @@ -52,9 +46,7 @@ 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) @@ -62,10 +54,8 @@ 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]]) @@ -73,10 +63,8 @@ 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]]) @@ -84,9 +72,9 @@ 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") @@ -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") @@ -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 @@ -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 @@ -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") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a1df05c..40abf5d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 \ No newline at end of file +end