recommendify_whosv 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/README.md +154 -0
- data/Rakefile +18 -0
- data/doc/example.png +0 -0
- data/doc/example.rb +87 -0
- data/doc/example_data.csv +120048 -0
- data/ext/cc_item.h +8 -0
- data/ext/cosine.c +3 -0
- data/ext/extconf.rb +18 -0
- data/ext/iikey.c +18 -0
- data/ext/jaccard.c +19 -0
- data/ext/output.c +22 -0
- data/ext/recommendify.c +214 -0
- data/ext/sort.c +23 -0
- data/ext/version.h +17 -0
- data/lib/recommendify/base.rb +88 -0
- data/lib/recommendify/cc_matrix.rb +51 -0
- data/lib/recommendify/cosine_input_matrix.rb +7 -0
- data/lib/recommendify/input_matrix.rb +52 -0
- data/lib/recommendify/jaccard_input_matrix.rb +62 -0
- data/lib/recommendify/neighbor.rb +19 -0
- data/lib/recommendify/recommendify.rb +25 -0
- data/lib/recommendify/similarity_matrix.rb +63 -0
- data/lib/recommendify/sparse_matrix.rb +53 -0
- data/lib/recommendify.rb +9 -0
- data/recommendify.gemspec +25 -0
- data/spec/base_spec.rb +188 -0
- data/spec/cc_matrix_shared.rb +89 -0
- data/spec/cosine_input_matrix_spec.rb +18 -0
- data/spec/input_matrix_shared.rb +27 -0
- data/spec/input_matrix_spec.rb +29 -0
- data/spec/jaccard_input_matrix_spec.rb +95 -0
- data/spec/recommendify_spec.rb +28 -0
- data/spec/similarity_matrix_spec.rb +93 -0
- data/spec/sparse_matrix_spec.rb +78 -0
- data/spec/spec_helper.rb +42 -0
- metadata +122 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
class Recommendify::SimilarityMatrix
|
2
|
+
|
3
|
+
attr_reader :write_queue
|
4
|
+
|
5
|
+
def initialize(opts={})
|
6
|
+
@opts = opts
|
7
|
+
@write_queue = Hash.new{ |h,k| h[k] = {} }
|
8
|
+
end
|
9
|
+
|
10
|
+
def redis_key(append=nil)
|
11
|
+
[@opts.fetch(:redis_prefix), @opts.fetch(:key), append].flatten.compact.join(":")
|
12
|
+
end
|
13
|
+
|
14
|
+
def max_neighbors
|
15
|
+
@opts[:max_neighbors] || Recommendify::DEFAULT_MAX_NEIGHBORS
|
16
|
+
end
|
17
|
+
|
18
|
+
def update(item_id, neighbors)
|
19
|
+
neighbors.each do |neighbor_id, score|
|
20
|
+
if @write_queue[item_id].has_key?(neighbor_id)
|
21
|
+
@write_queue[item_id][neighbor_id] += score
|
22
|
+
else
|
23
|
+
@write_queue[item_id][neighbor_id] = score
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](item_id)
|
29
|
+
if @write_queue.has_key?(item_id)
|
30
|
+
@write_queue[item_id]
|
31
|
+
else
|
32
|
+
retrieve_item(item_id)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def commit_item!(item_id)
|
37
|
+
serialized = serialize_item(item_id)
|
38
|
+
Recommendify.redis.hset(redis_key, item_id, serialized)
|
39
|
+
@write_queue.delete(item_id)
|
40
|
+
end
|
41
|
+
|
42
|
+
# optimize: the items are already stored in a sorted fashion. we shouldn't
|
43
|
+
# throw away this info by storing them in a hash (and re-sorting later). maybe
|
44
|
+
# use activesupport's orderedhash?
|
45
|
+
def retrieve_item(item_id)
|
46
|
+
data = Recommendify.redis.hget(redis_key, item_id)
|
47
|
+
return {} if data.nil?
|
48
|
+
Hash[data.split("|").map{ |i| (k,s=i.split(":")) && [k,s.to_f] }]
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# optimize: implement a better sort. never add more than 50 items the the array
|
54
|
+
def serialize_item(item_id, max_precision=5)
|
55
|
+
items = @write_queue[item_id].to_a
|
56
|
+
items.sort!{ |a,b| b[1] <=> a[1] }
|
57
|
+
#items = items[0..max_neighbors-1]
|
58
|
+
#items = items.map{ |i,s| s>0 ? "#{i}:#{s.to_s[0..max_precision]}" : nil }
|
59
|
+
items = items.map{ |i,s| "#{i}:#{s.to_s[0..max_precision]}" }
|
60
|
+
items.compact * "|"
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Recommendify::SparseMatrix
|
2
|
+
|
3
|
+
def initialize(opts={})
|
4
|
+
@opts = opts
|
5
|
+
end
|
6
|
+
|
7
|
+
def redis_key
|
8
|
+
[@opts.fetch(:redis_prefix), @opts.fetch(:key)].join(":")
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](x,y)
|
12
|
+
k_get(key(x,y))
|
13
|
+
end
|
14
|
+
|
15
|
+
def []=(x,y,v)
|
16
|
+
v == 0 ? k_del(key(x,y)) : k_set(key(x,y), v)
|
17
|
+
end
|
18
|
+
|
19
|
+
def incr(x,y)
|
20
|
+
k_incr(key(x,y))
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def key(x,y)
|
26
|
+
[x,y].sort.join(":")
|
27
|
+
end
|
28
|
+
|
29
|
+
def k_set(key, val)
|
30
|
+
Recommendify.redis.hset(redis_key, key, val)
|
31
|
+
end
|
32
|
+
|
33
|
+
def k_del(key)
|
34
|
+
Recommendify.redis.hdel(redis_key, key)
|
35
|
+
end
|
36
|
+
|
37
|
+
def k_get(key)
|
38
|
+
Recommendify.redis.hget(redis_key, key).to_f
|
39
|
+
end
|
40
|
+
|
41
|
+
def k_incr(key)
|
42
|
+
Recommendify.redis.hincrby(redis_key, key, 1)
|
43
|
+
end
|
44
|
+
|
45
|
+
# OPTIMIZE: use scripting/lua in redis 2.6
|
46
|
+
def k_delall(*keys)
|
47
|
+
Recommendify.redis.hkeys(redis_key).each do |iikey|
|
48
|
+
next unless (iikey.split(":") & keys).size > 0
|
49
|
+
Recommendify.redis.hdel(redis_key, iikey)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/lib/recommendify.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require "recommendify/recommendify"
|
2
|
+
require "recommendify/sparse_matrix"
|
3
|
+
require "recommendify/cc_matrix"
|
4
|
+
require "recommendify/similarity_matrix"
|
5
|
+
require "recommendify/input_matrix"
|
6
|
+
require "recommendify/jaccard_input_matrix"
|
7
|
+
require "recommendify/cosine_input_matrix"
|
8
|
+
require "recommendify/base"
|
9
|
+
require "recommendify/neighbor"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "recommendify_whosv"
|
6
|
+
s.version = "0.5.8"
|
7
|
+
s.date = Date.today.to_s
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Paul Asmuth"]
|
10
|
+
s.email = ["paul@paulasmuth.com"]
|
11
|
+
s.homepage = "http://github.com/paulasmuth/recommendify"
|
12
|
+
s.summary = %q{ruby/redis based recommendation engine (collaborative filtering)}
|
13
|
+
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"}
|
14
|
+
s.licenses = ["MIT"]
|
15
|
+
|
16
|
+
s.extensions = ['ext/extconf.rb']
|
17
|
+
|
18
|
+
s.add_dependency "redis", ">= 2.2.2"
|
19
|
+
|
20
|
+
s.add_development_dependency "rspec", "~> 2.8.0"
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split("\n") - [".gitignore", ".rspec", ".travis.yml"]
|
23
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
end
|
data/spec/base_spec.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
require ::File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Recommendify::Base do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
flush_redis!
|
7
|
+
Recommendify::Base.send(:class_variable_set, :@@max_neighbors, nil)
|
8
|
+
Recommendify::Base.send(:class_variable_set, :@@input_matrices, {})
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "configuration" do
|
12
|
+
|
13
|
+
it "should return default max_neighbors if not configured" do
|
14
|
+
Recommendify::DEFAULT_MAX_NEIGHBORS.should == 50
|
15
|
+
sm = Recommendify::Base.new
|
16
|
+
sm.max_neighbors.should == 50
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should remember max_neighbors if configured" do
|
20
|
+
Recommendify::Base.max_neighbors(23)
|
21
|
+
sm = Recommendify::Base.new
|
22
|
+
sm.max_neighbors.should == 23
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should add an input_matrix by 'key'" do
|
26
|
+
Recommendify::Base.input_matrix(:myinput, :similarity_func => :jaccard)
|
27
|
+
Recommendify::Base.send(:class_variable_get, :@@input_matrices).keys.should == [:myinput]
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should retrieve an input_matrix on a new instance" do
|
31
|
+
Recommendify::Base.input_matrix(:myinput, :similarity_func => :jaccard)
|
32
|
+
sm = Recommendify::Base.new
|
33
|
+
lambda{ sm.myinput }.should_not raise_error
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should retrieve an input_matrix on a new instance and correctly overload respond_to?" do
|
37
|
+
Recommendify::Base.input_matrix(:myinput, :similarity_func => :jaccard)
|
38
|
+
sm = Recommendify::Base.new
|
39
|
+
sm.respond_to?(:process!).should be_true
|
40
|
+
sm.respond_to?(:myinput).should be_true
|
41
|
+
sm.respond_to?(:fnord).should be_false
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should retrieve an input_matrix on a new instance and intialize the correct class" do
|
45
|
+
Recommendify::Base.input_matrix(:myinput, :similarity_func => :jaccard)
|
46
|
+
sm = Recommendify::Base.new
|
47
|
+
sm.myinput.should be_a(Recommendify::JaccardInputMatrix)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "process_item!" do
|
53
|
+
|
54
|
+
it "should call similarities_for on each input_matrix" do
|
55
|
+
Recommendify::Base.input_matrix(:myfirstinput, :similarity_func => :jaccard)
|
56
|
+
Recommendify::Base.input_matrix(:mysecondinput, :similarity_func => :jaccard)
|
57
|
+
sm = Recommendify::Base.new
|
58
|
+
sm.myfirstinput.should_receive(:similarities_for).with("fnorditem").and_return([["fooitem",0.5]])
|
59
|
+
sm.mysecondinput.should_receive(:similarities_for).with("fnorditem").and_return([["fooitem",0.5]])
|
60
|
+
sm.similarity_matrix.stub!(:update)
|
61
|
+
sm.process_item!("fnorditem")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should call similarities_for on each input_matrix and add all outputs to the similarity matrix" do
|
65
|
+
Recommendify::Base.input_matrix(:myfirstinput, :similarity_func => :jaccard)
|
66
|
+
Recommendify::Base.input_matrix(:mysecondinput, :similarity_func => :jaccard)
|
67
|
+
sm = Recommendify::Base.new
|
68
|
+
sm.myfirstinput.should_receive(:similarities_for).and_return([["fooitem",0.5]])
|
69
|
+
sm.mysecondinput.should_receive(:similarities_for).and_return([["fooitem",0.75], ["baritem", 1.0]])
|
70
|
+
sm.similarity_matrix.should_receive(:update).with("fnorditem", [["fooitem",0.5]])
|
71
|
+
sm.similarity_matrix.should_receive(:update).with("fnorditem", [["fooitem",0.75], ["baritem", 1.0]])
|
72
|
+
sm.process_item!("fnorditem")
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should call similarities_for on each input_matrix and add all outputs to the similarity matrix with weight" do
|
76
|
+
Recommendify::Base.input_matrix(:myfirstinput, :similarity_func => :jaccard, :weight => 4.0)
|
77
|
+
Recommendify::Base.input_matrix(:mysecondinput, :similarity_func => :jaccard)
|
78
|
+
sm = Recommendify::Base.new
|
79
|
+
sm.myfirstinput.should_receive(:similarities_for).and_return([["fooitem",0.5]])
|
80
|
+
sm.mysecondinput.should_receive(:similarities_for).and_return([["fooitem",0.75], ["baritem", 1.0]])
|
81
|
+
sm.similarity_matrix.should_receive(:update).with("fnorditem", [["fooitem",2.0]])
|
82
|
+
sm.similarity_matrix.should_receive(:update).with("fnorditem", [["fooitem",0.75], ["baritem", 1.0]])
|
83
|
+
sm.process_item!("fnorditem")
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should retrieve all items from all input matrices" do
|
87
|
+
Recommendify::Base.input_matrix(:anotherinput, :similarity_func => :test, :all_items => ["foo", "bar"])
|
88
|
+
Recommendify::Base.input_matrix(:yetanotherinput, :similarity_func => :test, :all_items => ["fnord", "shmoo"])
|
89
|
+
sm = Recommendify::Base.new
|
90
|
+
sm.all_items.length.should == 4
|
91
|
+
sm.all_items.should include("foo")
|
92
|
+
sm.all_items.should include("bar")
|
93
|
+
sm.all_items.should include("fnord")
|
94
|
+
sm.all_items.should include("shmoo")
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should retrieve all items from all input matrices (uniquely)" do
|
98
|
+
Recommendify::Base.input_matrix(:anotherinput, :similarity_func => :test, :all_items => ["foo", "bar"])
|
99
|
+
Recommendify::Base.input_matrix(:yetanotherinput, :similarity_func => :test, :all_items => ["fnord", "bar"])
|
100
|
+
sm = Recommendify::Base.new
|
101
|
+
sm.all_items.length.should == 3
|
102
|
+
sm.all_items.should include("foo")
|
103
|
+
sm.all_items.should include("bar")
|
104
|
+
sm.all_items.should include("fnord")
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "process!" do
|
110
|
+
|
111
|
+
it "should call process_item for all input_matrix.all_items's" do
|
112
|
+
Recommendify::Base.input_matrix(:anotherinput, :similarity_func => :test, :all_items => ["foo", "bar"])
|
113
|
+
Recommendify::Base.input_matrix(:yetanotherinput, :similarity_func => :test, :all_items => ["fnord", "shmoo"])
|
114
|
+
sm = Recommendify::Base.new
|
115
|
+
sm.should_receive(:process_item!).exactly(4).times
|
116
|
+
sm.process!
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should call process_item for all input_matrix.all_items's (uniquely)" do
|
120
|
+
Recommendify::Base.input_matrix(:anotherinput, :similarity_func => :test, :all_items => ["foo", "bar"])
|
121
|
+
Recommendify::Base.input_matrix(:yetanotherinput, :similarity_func => :test, :all_items => ["fnord", "bar"])
|
122
|
+
sm = Recommendify::Base.new
|
123
|
+
sm.should_receive(:process_item!).exactly(3).times
|
124
|
+
sm.process!
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "for(item_id)" do
|
130
|
+
|
131
|
+
it "should retrieve the n-most similar neighbors" do
|
132
|
+
sm = Recommendify::Base.new
|
133
|
+
sm.similarity_matrix.should_receive(:[]).with("fnorditem").and_return({:fooitem => 0.4, :baritem => 1.5})
|
134
|
+
sm.for("fnorditem").length.should == 2
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should not throw exception for non existing items" do
|
138
|
+
sm = Recommendify::Base.new
|
139
|
+
sm.for("not_existing_item").length.should == 0
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should retrieve the n-most similar neighbors as Recommendify::Neighbor objects" do
|
143
|
+
sm = Recommendify::Base.new
|
144
|
+
sm.similarity_matrix.should_receive(:[]).exactly(2).times.with("fnorditem").and_return({:fooitem => 0.4, :baritem => 1.5})
|
145
|
+
sm.for("fnorditem").first.should be_a(Recommendify::Neighbor)
|
146
|
+
sm.for("fnorditem").last.should be_a(Recommendify::Neighbor)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should retrieve the n-most similar neighbors in the correct order" do
|
150
|
+
sm = Recommendify::Base.new
|
151
|
+
sm.similarity_matrix.should_receive(:[]).exactly(4).times.with("fnorditem").and_return({:fooitem => 0.4, :baritem => 1.5})
|
152
|
+
sm.for("fnorditem").first.similarity.should == 1.5
|
153
|
+
sm.for("fnorditem").first.item_id.should == "baritem"
|
154
|
+
sm.for("fnorditem").last.similarity.should == 0.4
|
155
|
+
sm.for("fnorditem").last.item_id.should == "fooitem"
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should return an empty array if the item if no neighbors were found" do
|
159
|
+
sm = Recommendify::Base.new
|
160
|
+
sm.similarity_matrix.should_receive(:[]).with("fnorditem").and_return({})
|
161
|
+
sm.for("fnorditem").should == []
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should not call split on nil when retrieving a non-existent item (return an empty array)" do
|
165
|
+
sm = Recommendify::Base.new
|
166
|
+
sm.for("NONEXISTENT").should == []
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
describe "delete_item!" do
|
172
|
+
|
173
|
+
it "should call delete_item on each input_matrix" do
|
174
|
+
Recommendify::Base.input_matrix(:myfirstinput, :similarity_func => :jaccard)
|
175
|
+
Recommendify::Base.input_matrix(:mysecondinput, :similarity_func => :jaccard)
|
176
|
+
sm = Recommendify::Base.new
|
177
|
+
sm.myfirstinput.should_receive(:delete_item).with("fnorditem")
|
178
|
+
sm.mysecondinput.should_receive(:delete_item).with("fnorditem")
|
179
|
+
sm.delete_item!("fnorditem")
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should delete the item from the similarity matrix"
|
183
|
+
|
184
|
+
it "should delete all occurences of the item in other similarity sets from the similarity matrix"
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
share_examples_for Recommendify::CCMatrix do
|
2
|
+
|
3
|
+
it "should build a sparsematrix with the correct key" do
|
4
|
+
@matrix.ccmatrix.redis_key.should == "recommendify-test:mymatrix:ccmatrix"
|
5
|
+
end
|
6
|
+
|
7
|
+
it "should increment all item counts on set addition" do
|
8
|
+
Recommendify.redis.hset("recommendify-test:mymatrix:items", "bar", 2)
|
9
|
+
@matrix.add_set("user123", ["foo", "bar"])
|
10
|
+
Recommendify.redis.hget("recommendify-test:mymatrix:items", "bar").to_i.should == 3
|
11
|
+
Recommendify.redis.hget("recommendify-test:mymatrix:items", "foo").to_i.should == 1
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should increment all item<->item pairs on set addition" do
|
15
|
+
@matrix.ccmatrix["bar", "foo"] = 2
|
16
|
+
res = @matrix.add_set("user123", ["foo", "bar", "fnord"])
|
17
|
+
res.length.should == 3
|
18
|
+
@matrix.ccmatrix["bar", "foo"].should == 3
|
19
|
+
@matrix.ccmatrix["foo", "fnord"].should == 1
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should increment all item<->item paris on single item addition" do
|
23
|
+
@matrix.ccmatrix["bar", "fnord"] = 2
|
24
|
+
@matrix.add_single("user123", "fnord", ["foo", "bar"])
|
25
|
+
@matrix.ccmatrix["bar", "fnord"].should == 3
|
26
|
+
@matrix.ccmatrix["foo", "fnord"].should == 1
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should increment the item count on single item addition" do
|
30
|
+
@matrix.send(:item_count_incr, "fnordfnord")
|
31
|
+
@matrix.send(:item_count_incr, "fnordfnord")
|
32
|
+
@matrix.send(:item_count_incr, "foofnord")
|
33
|
+
@matrix.add_single("user123", "fnordfnord", ["foofnord", "barfnord"])
|
34
|
+
@matrix.send(:item_count, "foofnord").should == 1
|
35
|
+
@matrix.send(:item_count, "barfnord").should == 0
|
36
|
+
@matrix.send(:item_count, "fnordfnord").should == 3
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should calculate all item<->item pairs (3)" do
|
40
|
+
res = @matrix.send(:all_pairs, ["foo", "bar", "fnord"])
|
41
|
+
res.length.should == 3
|
42
|
+
res.should include("bar:foo")
|
43
|
+
res.should include("fnord:foo")
|
44
|
+
res.should include("bar:fnord")
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should calculate all item<->item pairs (6)" do
|
48
|
+
res = @matrix.send(:all_pairs, ["foo", "bar", "fnord", "blubb"])
|
49
|
+
res.length.should == 6
|
50
|
+
res.should include("bar:foo")
|
51
|
+
res.should include("fnord:foo")
|
52
|
+
res.should include("bar:fnord")
|
53
|
+
res.should include("blubb:foo")
|
54
|
+
res.should include("blubb:fnord")
|
55
|
+
res.should include("bar:blubb")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should return all_items" do
|
59
|
+
@matrix.add_set("user42", ["fnord", "blubb"])
|
60
|
+
@matrix.add_set("user23", ["hans", "wurst"])
|
61
|
+
@matrix.all_items.length.should == 4
|
62
|
+
@matrix.all_items.should include("wurst")
|
63
|
+
@matrix.all_items.should include("fnord")
|
64
|
+
@matrix.all_items.should include("hans")
|
65
|
+
@matrix.all_items.should include("wurst")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should delete all item<->item pairs on item deletion" do
|
69
|
+
@matrix.ccmatrix["foo", "fnord"] = 2
|
70
|
+
@matrix.add_set("user123", ["foo", "bar", "fnord"])
|
71
|
+
@matrix.add_set("user456", ["fnord", "blubb"])
|
72
|
+
@matrix.ccmatrix["bar", "foo"].should == 1
|
73
|
+
@matrix.ccmatrix["foo", "fnord"].should == 3
|
74
|
+
@matrix.ccmatrix["blubb", "fnord"].should == 1
|
75
|
+
@matrix.delete_item("fnord")
|
76
|
+
@matrix.ccmatrix["bar", "foo"].should == 1
|
77
|
+
@matrix.ccmatrix["foo", "fnord"].should == 0
|
78
|
+
@matrix.ccmatrix["blubb", "fnord"].should == 0
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should delete the item count on deletion" do
|
82
|
+
@matrix.add_set("user123", ["foo", "bar", "fnord"])
|
83
|
+
@matrix.add_set("user456", ["fnord", "blubb"])
|
84
|
+
@matrix.send(:item_count, "fnord").should == 2
|
85
|
+
@matrix.delete_item("fnord")
|
86
|
+
@matrix.send(:item_count, "fnord").should == 0
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require ::File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Recommendify::CosineInputMatrix do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
@matrix = Recommendify::CosineInputMatrix.new(:redis_prefix => "recommendify-test", :key => "mymatrix")
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
flush_redis!
|
11
|
+
end
|
12
|
+
|
13
|
+
it_should_behave_like Recommendify::InputMatrix
|
14
|
+
it_should_behave_like Recommendify::CCMatrix
|
15
|
+
|
16
|
+
it "should calculate the correct cosine similarity (here be dragons)"
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
share_examples_for Recommendify::InputMatrix do
|
2
|
+
|
3
|
+
it "should build the correct keys" do
|
4
|
+
@matrix.redis_key.should == "recommendify-test:mymatrix"
|
5
|
+
end
|
6
|
+
|
7
|
+
it "should respond to add_set" do
|
8
|
+
@matrix.respond_to?(:add_set).should == true
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should respond to add_single" do
|
12
|
+
@matrix.respond_to?(:add_single).should == true
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should respond to similarity" do
|
16
|
+
@matrix.respond_to?(:similarity).should == true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should respond to similarities_for" do
|
20
|
+
@matrix.respond_to?(:similarities_for).should == true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should respond to all_items" do
|
24
|
+
@matrix.respond_to?(:all_items).should == true
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require ::File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Recommendify::InputMatrix do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
@matrix = Recommendify::InputMatrix.new(:redis_prefix => "recommendify-test", :key => "mymatrix")
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
flush_redis!
|
11
|
+
end
|
12
|
+
|
13
|
+
it_should_behave_like Recommendify::InputMatrix
|
14
|
+
|
15
|
+
describe "object creation" do
|
16
|
+
|
17
|
+
it "should create an object with the correct class" do
|
18
|
+
obj = Recommendify::InputMatrix.create(:key => "fubar", :similarity_func => :jaccard)
|
19
|
+
obj.should be_a(Recommendify::JaccardInputMatrix)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should create an object with the correct class and pass opts" do
|
23
|
+
obj = Recommendify::InputMatrix.create(:key => "fubar", :similarity_func => :jaccard)
|
24
|
+
obj.instance_variable_get(:@opts)[:key].should == "fubar"
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require ::File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Recommendify::JaccardInputMatrix do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
@matrix = Recommendify::JaccardInputMatrix.new(:redis_prefix => "recommendify-test", :key => "mymatrix")
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
flush_redis!
|
11
|
+
end
|
12
|
+
|
13
|
+
it_should_behave_like Recommendify::InputMatrix
|
14
|
+
it_should_behave_like Recommendify::CCMatrix
|
15
|
+
|
16
|
+
it "should calculate the correct jaccard index" do
|
17
|
+
@matrix.send(:calculate_jaccard,
|
18
|
+
["foo", "bar", "fnord", "blubb"],
|
19
|
+
["bar", "fnord", "shmoo", "snafu"]
|
20
|
+
).should == 2.0/6.0
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should calculate the correct similarity between to items" do
|
24
|
+
add_two_item_test_data!(@matrix)
|
25
|
+
# sim(fnord,blubb) = (users(fnord) & users(blub)) / (users(fnord) + users(blubb))
|
26
|
+
# => {user42 user48} / {user42 user46 user48 user50} + {user42 user44 user48}
|
27
|
+
# => {user42 user48} / {user42 user44 user46 user48 user50}
|
28
|
+
# => 2 / 5 => 0.4
|
29
|
+
@matrix.similarity("fnord", "blubb").should == 0.4
|
30
|
+
@matrix.similarity("blubb", "fnord").should == 0.4
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should calculate all similarities for an item (1/3)" do
|
34
|
+
add_three_item_test_data!(@matrix)
|
35
|
+
res = @matrix.similarities_for("fnord")
|
36
|
+
res.length.should == 2
|
37
|
+
res.should include ["shmoo", 0.75]
|
38
|
+
res.should include ["blubb", 0.4]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should calculate all similarities for an item (2/3)" do
|
42
|
+
add_three_item_test_data!(@matrix)
|
43
|
+
res = @matrix.similarities_for("shmoo")
|
44
|
+
res.length.should == 2
|
45
|
+
res.should include ["blubb", 0.2]
|
46
|
+
res.should include ["fnord", 0.75]
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
it "should calculate all similarities for an item (3/3)" do
|
51
|
+
add_three_item_test_data!(@matrix)
|
52
|
+
res = @matrix.similarities_for("blubb")
|
53
|
+
res.length.should == 2
|
54
|
+
res.should include ["shmoo", 0.2]
|
55
|
+
res.should include ["fnord", 0.4]
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should call run_native when the native option was passed" do
|
59
|
+
Recommendify::JaccardInputMatrix.class_eval do
|
60
|
+
def check_native; true; end
|
61
|
+
end
|
62
|
+
matrix = Recommendify::JaccardInputMatrix.new(
|
63
|
+
:redis_prefix => "recommendify-test",
|
64
|
+
:native => true,
|
65
|
+
:key => "mymatrix"
|
66
|
+
)
|
67
|
+
matrix.should_receive(:run_native).with("fnord").and_return(true)
|
68
|
+
matrix.similarities_for("fnord")
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should return the correct redis url" do
|
72
|
+
@matrix.send(:redis_url).should == "127.0.0.1:6379"
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def add_two_item_test_data!(matrix)
|
78
|
+
matrix.add_set("user42", ["fnord", "blubb"])
|
79
|
+
matrix.add_set("user44", ["blubb"])
|
80
|
+
matrix.add_set("user46", ["fnord"])
|
81
|
+
matrix.add_set("user48", ["fnord", "blubb"])
|
82
|
+
matrix.add_set("user50", ["fnord"])
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_three_item_test_data!(matrix)
|
86
|
+
matrix.add_set("user42", ["fnord", "blubb", "shmoo"])
|
87
|
+
matrix.add_set("user44", ["blubb"])
|
88
|
+
matrix.add_set("user46", ["fnord", "shmoo"])
|
89
|
+
matrix.add_set("user48", ["fnord", "blubb"])
|
90
|
+
matrix.add_set("user50", ["fnord", "shmoo"])
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require ::File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Recommendify do
|
4
|
+
|
5
|
+
it "should store a redis connection" do
|
6
|
+
Recommendify.redis = "asd"
|
7
|
+
Recommendify.redis.should == "asd"
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should raise an exception if unconfigured redis connection is accessed" do
|
11
|
+
Recommendify.redis = nil
|
12
|
+
lambda{ ecommendify.redis }.should raise_error
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should capitalize a string" do
|
16
|
+
Recommendify.capitalize("fuubar").should == "Fuubar"
|
17
|
+
Recommendify.capitalize("fuUBar").should == "Fuubar"
|
18
|
+
Recommendify.capitalize("FUUBAR").should == "Fuubar"
|
19
|
+
Recommendify.capitalize("Fuubar").should == "Fuubar"
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should constantize a string" do
|
23
|
+
obj = Recommendify.constantize("JaccardInputMatrix")
|
24
|
+
# should_be doesn't work here...
|
25
|
+
obj.inspect.should == "Recommendify::JaccardInputMatrix"
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|