predictor 2.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Changelog.md +12 -0
- data/README.md +47 -0
- data/Rakefile +6 -1
- data/lib/predictor/base.rb +55 -7
- data/lib/predictor/input_matrix.rb +6 -2
- data/lib/predictor/predictor.rb +18 -1
- data/lib/predictor/version.rb +1 -1
- data/spec/base_spec.rb +233 -60
- data/spec/input_matrix_spec.rb +88 -36
- data/spec/predictor_spec.rb +2 -2
- data/spec/spec_helper.rb +6 -6
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f06d8361ac24ffaedb43dc650bba9af6ad62374a
|
4
|
+
data.tar.gz: c2815b5b8a507026bae58773ac32a1d7188debcb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2988190b65071a5d155974db67bc9815614720f57d9e5131ac429c0fcae5d7210527a07548aca73066a63fee542ee6edb7fb9209304041374df04304e72650ff
|
7
|
+
data.tar.gz: aa8215990ac119de3ca275c9ab666cbe246ffa53f28f5c428361734d7a3a79186bffc007d83056d3b1511beead9ab9ec0aa1f941a97fc8d41378b48b61775b53
|
data/Changelog.md
CHANGED
@@ -2,6 +2,18 @@
|
|
2
2
|
Predictor Changelog
|
3
3
|
=========
|
4
4
|
|
5
|
+
2.2.0 (Unreleased)
|
6
|
+
---------------------
|
7
|
+
* The namespace used for keys in Redis is now configurable on a global or per-class basis. See the readme for more information. If you were overriding the redis_prefix instance method before, it is recommended that you use the new redis_prefix class method instead.
|
8
|
+
* Data stored in Redis is now namespaced by the class name of the recommender it is stored by. This change ensures that different recommenders with input matrices of the same name don't overwrite each others' data. After upgrading you'll need to either reindex your data in Redis or configure Predictor to use the naming system you were using before. If you were using the defaults before and you're not worried about matrix name collisions, you can mimic the old behavior with:
|
9
|
+
```ruby
|
10
|
+
class MyRecommender
|
11
|
+
include Predictor::Base
|
12
|
+
redis_prefix [nil]
|
13
|
+
end
|
14
|
+
```
|
15
|
+
* The #predictions_for method on recommenders now accepts a :boost option to give more weight to items with particular attributes. See the readme for more information.
|
16
|
+
|
5
17
|
2.1.0 (2014-06-19)
|
6
18
|
---------------------
|
7
19
|
* The similarity limit now defaults to 128, instead of being unlimited. This is intended to save space in Redis. See the Readme for more information. It is strongly recommended that you run `ensure_similarity_limit_is_obeyed!` to shrink existing similarity sets.
|
data/README.md
CHANGED
@@ -172,6 +172,53 @@ You can also use `limit_similarities_to(nil)` to remove the limit entirely. This
|
|
172
172
|
|
173
173
|
If at some point you decide to lower your similarity limits, you'll want to be sure to shrink the size of the sorted sets already in Redis. You can do this with `CourseRecommender.new.ensure_similarity_limit_is_obeyed!`.
|
174
174
|
|
175
|
+
Boost
|
176
|
+
---------------------
|
177
|
+
What if you want to recommend courses to users based not only on what courses they've taken, but on other attributes of courses that they may be interested in? You can do that by passing the :boost argument to predictions_for:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class CourseRecommender
|
181
|
+
include Predictor::Base
|
182
|
+
|
183
|
+
# Courses are compared to one another by the users taking them and their tags.
|
184
|
+
input_matrix :users, weight: 3.0
|
185
|
+
input_matrix :tags, weight: 2.0
|
186
|
+
input_matrix :topics, weight: 2.0
|
187
|
+
end
|
188
|
+
|
189
|
+
recommender = CourseRecommender.new
|
190
|
+
|
191
|
+
# We want to find recommendations for Billy, who's told us that he's
|
192
|
+
# especially interested in free, interactive courses on Photoshop. So, we give
|
193
|
+
# a boost to courses that are tagged as free and interactive and have
|
194
|
+
# Photoshop as a topic:
|
195
|
+
recommender.predictions_for("Billy", matrix_label: :users, boost: {tags: ['free', 'interactive'], topics: ["Photoshop"]})
|
196
|
+
|
197
|
+
# We can also modify how much these tags and topics matter by specifying a
|
198
|
+
# weight. The default is 1.0, but if that's too much we can just tweak it:
|
199
|
+
recommender.predictions_for("Billy", matrix_label: :users, boost: {tags: {values: ['free', 'interactive'], weight: 0.4}, topics: {values: ["Photoshop"], weight: 0.3}})
|
200
|
+
```
|
201
|
+
|
202
|
+
Key Prefixes
|
203
|
+
---------------------
|
204
|
+
As of 2.2.0, there is much more control available over the format of the keys Predictor will use in Redis. By default, the CourseRecommender given as an example above will use keys like "predictor:CourseRecommender:users:items:user1". You can configure the global namespace like so:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
Predictor.redis_prefix 'my_namespace' # => "my_namespace:CourseRecommender:users:items:user1"
|
208
|
+
# Or, for a multitenanted setup:
|
209
|
+
Predictor.redis_prefix { "user-#{User.current.id}" } # => "user-7:CourseRecommender:users:items:user1"
|
210
|
+
```
|
211
|
+
|
212
|
+
You can also configure the namespace used by each class you create:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class CourseRecommender
|
216
|
+
include Predictor::Base
|
217
|
+
redis_prefix "courses" # => "predictor:courses:users:items:user1"
|
218
|
+
redis_prefix { "courses_for_user-#{User.current.id}" } # => "predictor:courses_for_user-7:users:items:user1"
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
175
222
|
Upgrading from 1.0 to 2.0
|
176
223
|
---------------------
|
177
224
|
As mentioned, 2.0.0 is quite a bit different than 1.0.0, so simply upgrading with no changes likely won't work. My apologies for this. I promise this won't happen in future releases, as I'm much more confident in this Predictor release than the last. Anywho, upgrading really shouldn't be that much of a pain if you follow these steps:
|
data/Rakefile
CHANGED
data/lib/predictor/base.rb
CHANGED
@@ -30,17 +30,33 @@ module Predictor::Base
|
|
30
30
|
def input_matrices
|
31
31
|
@matrices
|
32
32
|
end
|
33
|
+
|
34
|
+
def redis_prefix(prefix = nil, &block)
|
35
|
+
@redis_prefix = block_given? ? block : prefix
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_redis_prefix
|
39
|
+
if @redis_prefix
|
40
|
+
if @redis_prefix.respond_to?(:call)
|
41
|
+
@redis_prefix.call
|
42
|
+
else
|
43
|
+
@redis_prefix
|
44
|
+
end
|
45
|
+
else
|
46
|
+
to_s
|
47
|
+
end
|
48
|
+
end
|
33
49
|
end
|
34
50
|
|
35
51
|
def input_matrices
|
36
52
|
@input_matrices ||= Hash[self.class.input_matrices.map{ |key, opts|
|
37
|
-
opts.merge!(:key => key, :
|
53
|
+
opts.merge!(:key => key, :base => self)
|
38
54
|
[ key, Predictor::InputMatrix.new(opts) ]
|
39
55
|
}]
|
40
56
|
end
|
41
57
|
|
42
58
|
def redis_prefix
|
43
|
-
|
59
|
+
[Predictor.get_redis_prefix, self.class.get_redis_prefix]
|
44
60
|
end
|
45
61
|
|
46
62
|
def similarity_limit
|
@@ -88,7 +104,7 @@ module Predictor::Base
|
|
88
104
|
keys.empty? ? [] : (Predictor.redis.sunion(keys) - [item.to_s])
|
89
105
|
end
|
90
106
|
|
91
|
-
def predictions_for(set=nil, item_set: nil, matrix_label: nil, with_scores: false, offset: 0, limit: -1, exclusion_set: [])
|
107
|
+
def predictions_for(set=nil, item_set: nil, matrix_label: nil, with_scores: false, offset: 0, limit: -1, exclusion_set: [], boost: {})
|
92
108
|
fail "item_set or matrix_label and set is required" unless item_set || (matrix_label && set)
|
93
109
|
|
94
110
|
if matrix_label
|
@@ -96,16 +112,48 @@ module Predictor::Base
|
|
96
112
|
item_set = Predictor.redis.smembers(matrix.redis_key(:items, set))
|
97
113
|
end
|
98
114
|
|
99
|
-
item_keys =
|
115
|
+
item_keys = []
|
116
|
+
weights = []
|
117
|
+
|
118
|
+
item_set.each do |item|
|
119
|
+
item_keys << redis_key(:similarities, item)
|
120
|
+
weights << 1.0
|
121
|
+
end
|
122
|
+
|
123
|
+
boost.each do |matrix_label, values|
|
124
|
+
m = input_matrices[matrix_label]
|
125
|
+
|
126
|
+
# Passing plain sets to zunionstore is undocumented, but tested and supported:
|
127
|
+
# https://github.com/antirez/redis/blob/2.8.11/tests/unit/type/zset.tcl#L481-L489
|
128
|
+
|
129
|
+
case values
|
130
|
+
when Hash
|
131
|
+
values[:values].each do |value|
|
132
|
+
item_keys << m.redis_key(:items, value)
|
133
|
+
weights << values[:weight]
|
134
|
+
end
|
135
|
+
when Array
|
136
|
+
values.each do |value|
|
137
|
+
item_keys << m.redis_key(:items, value)
|
138
|
+
weights << 1.0
|
139
|
+
end
|
140
|
+
else
|
141
|
+
raise "Bad value for boost: #{boost.inspect}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
100
145
|
return [] if item_keys.empty?
|
146
|
+
|
101
147
|
predictions = nil
|
148
|
+
|
102
149
|
Predictor.redis.multi do |multi|
|
103
|
-
multi.zunionstore 'temp', item_keys
|
104
|
-
multi.zrem 'temp', item_set
|
150
|
+
multi.zunionstore 'temp', item_keys, weights: weights
|
151
|
+
multi.zrem 'temp', item_set if item_set.any?
|
105
152
|
multi.zrem 'temp', exclusion_set if exclusion_set.length > 0
|
106
153
|
predictions = multi.zrevrange 'temp', offset, limit == -1 ? limit : offset + (limit - 1), with_scores: with_scores
|
107
154
|
multi.del 'temp'
|
108
155
|
end
|
156
|
+
|
109
157
|
predictions.value
|
110
158
|
end
|
111
159
|
|
@@ -169,7 +217,7 @@ module Predictor::Base
|
|
169
217
|
end
|
170
218
|
|
171
219
|
def clean!
|
172
|
-
keys = Predictor.redis.keys(
|
220
|
+
keys = Predictor.redis.keys(redis_key('*'))
|
173
221
|
unless keys.empty?
|
174
222
|
Predictor.redis.del(keys)
|
175
223
|
end
|
@@ -4,12 +4,16 @@ module Predictor
|
|
4
4
|
@opts = opts
|
5
5
|
end
|
6
6
|
|
7
|
+
def base
|
8
|
+
@opts[:base]
|
9
|
+
end
|
10
|
+
|
7
11
|
def parent_redis_key(*append)
|
8
|
-
|
12
|
+
base.redis_key(*append)
|
9
13
|
end
|
10
14
|
|
11
15
|
def redis_key(*append)
|
12
|
-
|
16
|
+
base.redis_key(@opts.fetch(:key), *append)
|
13
17
|
end
|
14
18
|
|
15
19
|
def weight
|
data/lib/predictor/predictor.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Predictor
|
2
2
|
@@redis = nil
|
3
|
+
@@redis_prefix = nil
|
3
4
|
|
4
5
|
def self.redis=(redis)
|
5
6
|
@@redis = redis
|
@@ -10,6 +11,22 @@ module Predictor
|
|
10
11
|
raise "redis not configured! - Predictor.redis = Redis.new"
|
11
12
|
end
|
12
13
|
|
14
|
+
def self.redis_prefix(prefix = nil, &block)
|
15
|
+
@@redis_prefix = block_given? ? block : prefix
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.get_redis_prefix
|
19
|
+
if @@redis_prefix
|
20
|
+
if @@redis_prefix.respond_to?(:call)
|
21
|
+
@@redis_prefix.call
|
22
|
+
else
|
23
|
+
@@redis_prefix
|
24
|
+
end
|
25
|
+
else
|
26
|
+
'predictor'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
13
30
|
def self.capitalize(str_or_sym)
|
14
31
|
str = str_or_sym.to_s.each_char.to_a
|
15
32
|
str.first.upcase + str[1..-1].join("").downcase
|
@@ -18,4 +35,4 @@ module Predictor
|
|
18
35
|
def self.constantize(klass)
|
19
36
|
Object.module_eval("Predictor::#{klass}", __FILE__, __LINE__)
|
20
37
|
end
|
21
|
-
end
|
38
|
+
end
|
data/lib/predictor/version.rb
CHANGED
data/spec/base_spec.rb
CHANGED
@@ -1,54 +1,135 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Predictor::Base do
|
4
|
-
class BaseRecommender
|
5
|
-
include Predictor::Base
|
6
|
-
end
|
7
|
-
|
8
4
|
before(:each) do
|
9
5
|
flush_redis!
|
10
6
|
BaseRecommender.input_matrices = {}
|
11
7
|
BaseRecommender.reset_similarity_limit!
|
8
|
+
BaseRecommender.redis_prefix(nil)
|
9
|
+
UserRecommender.input_matrices = {}
|
10
|
+
UserRecommender.reset_similarity_limit!
|
12
11
|
end
|
13
12
|
|
14
13
|
describe "configuration" do
|
15
14
|
it "should add an input_matrix by 'key'" do
|
16
15
|
BaseRecommender.input_matrix(:myinput)
|
17
|
-
BaseRecommender.input_matrices.keys.
|
16
|
+
expect(BaseRecommender.input_matrices.keys).to eq([:myinput])
|
18
17
|
end
|
19
18
|
|
20
19
|
it "should default the similarity_limit to 128" do
|
21
|
-
BaseRecommender.similarity_limit.
|
20
|
+
expect(BaseRecommender.similarity_limit).to eq(128)
|
22
21
|
end
|
23
22
|
|
24
23
|
it "should allow the similarity limit to be configured" do
|
25
24
|
BaseRecommender.limit_similarities_to(500)
|
26
|
-
BaseRecommender.similarity_limit.
|
25
|
+
expect(BaseRecommender.similarity_limit).to eq(500)
|
27
26
|
end
|
28
27
|
|
29
28
|
it "should allow the similarity limit to be removed" do
|
30
29
|
BaseRecommender.limit_similarities_to(nil)
|
31
|
-
BaseRecommender.similarity_limit.
|
30
|
+
expect(BaseRecommender.similarity_limit).to eq(nil)
|
32
31
|
end
|
33
32
|
|
34
33
|
it "should retrieve an input_matrix on a new instance" do
|
35
34
|
BaseRecommender.input_matrix(:myinput)
|
36
35
|
sm = BaseRecommender.new
|
37
|
-
|
36
|
+
expect{ sm.myinput }.not_to raise_error
|
38
37
|
end
|
39
38
|
|
40
39
|
it "should retrieve an input_matrix on a new instance and correctly overload respond_to?" do
|
41
40
|
BaseRecommender.input_matrix(:myinput)
|
42
41
|
sm = BaseRecommender.new
|
43
|
-
sm.respond_to?(:process!).
|
44
|
-
sm.respond_to?(:myinput).
|
45
|
-
sm.respond_to?(:fnord).
|
42
|
+
expect(sm.respond_to?(:process!)).to be_true
|
43
|
+
expect(sm.respond_to?(:myinput)).to be_true
|
44
|
+
expect(sm.respond_to?(:fnord)).to be_false
|
46
45
|
end
|
47
46
|
|
48
47
|
it "should retrieve an input_matrix on a new instance and intialize the correct class" do
|
49
48
|
BaseRecommender.input_matrix(:myinput)
|
50
49
|
sm = BaseRecommender.new
|
51
|
-
sm.myinput.
|
50
|
+
expect(sm.myinput).to be_a(Predictor::InputMatrix)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "redis_key" do
|
55
|
+
it "should vary based on the class name" do
|
56
|
+
expect(BaseRecommender.new.redis_key).to eq('predictor-test:BaseRecommender')
|
57
|
+
expect(UserRecommender.new.redis_key).to eq('predictor-test:UserRecommender')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "redis_key" do
|
62
|
+
it "should vary based on the class name" do
|
63
|
+
expect(BaseRecommender.new.redis_key).to eq('predictor-test:BaseRecommender')
|
64
|
+
expect(UserRecommender.new.redis_key).to eq('predictor-test:UserRecommender')
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should be able to mimic the old naming defaults" do
|
68
|
+
BaseRecommender.redis_prefix([nil])
|
69
|
+
expect(BaseRecommender.new.redis_key(:key)).to eq('predictor-test:key')
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should respect the Predictor prefix configuration setting" do
|
73
|
+
br = BaseRecommender.new
|
74
|
+
|
75
|
+
expect(br.redis_key).to eq("predictor-test:BaseRecommender")
|
76
|
+
expect(br.redis_key(:another)).to eq("predictor-test:BaseRecommender:another")
|
77
|
+
expect(br.redis_key(:another, :key)).to eq("predictor-test:BaseRecommender:another:key")
|
78
|
+
expect(br.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:BaseRecommender:another:set:of:keys")
|
79
|
+
|
80
|
+
i = 0
|
81
|
+
Predictor.redis_prefix { i += 1 }
|
82
|
+
expect(br.redis_key).to eq("1:BaseRecommender")
|
83
|
+
expect(br.redis_key(:another)).to eq("2:BaseRecommender:another")
|
84
|
+
expect(br.redis_key(:another, :key)).to eq("3:BaseRecommender:another:key")
|
85
|
+
expect(br.redis_key(:another, [:set, :of, :keys])).to eq("4:BaseRecommender:another:set:of:keys")
|
86
|
+
|
87
|
+
Predictor.redis_prefix nil
|
88
|
+
expect(br.redis_key).to eq("predictor:BaseRecommender")
|
89
|
+
expect(br.redis_key(:another)).to eq("predictor:BaseRecommender:another")
|
90
|
+
expect(br.redis_key(:another, :key)).to eq("predictor:BaseRecommender:another:key")
|
91
|
+
expect(br.redis_key(:another, [:set, :of, :keys])).to eq("predictor:BaseRecommender:another:set:of:keys")
|
92
|
+
|
93
|
+
Predictor.redis_prefix [nil]
|
94
|
+
expect(br.redis_key).to eq("BaseRecommender")
|
95
|
+
expect(br.redis_key(:another)).to eq("BaseRecommender:another")
|
96
|
+
expect(br.redis_key(:another, :key)).to eq("BaseRecommender:another:key")
|
97
|
+
expect(br.redis_key(:another, [:set, :of, :keys])).to eq("BaseRecommender:another:set:of:keys")
|
98
|
+
|
99
|
+
Predictor.redis_prefix { [1, 2, 3] }
|
100
|
+
expect(br.redis_key).to eq("1:2:3:BaseRecommender")
|
101
|
+
expect(br.redis_key(:another)).to eq("1:2:3:BaseRecommender:another")
|
102
|
+
expect(br.redis_key(:another, :key)).to eq("1:2:3:BaseRecommender:another:key")
|
103
|
+
expect(br.redis_key(:another, [:set, :of, :keys])).to eq("1:2:3:BaseRecommender:another:set:of:keys")
|
104
|
+
|
105
|
+
Predictor.redis_prefix 'predictor-test'
|
106
|
+
expect(br.redis_key).to eq("predictor-test:BaseRecommender")
|
107
|
+
expect(br.redis_key(:another)).to eq("predictor-test:BaseRecommender:another")
|
108
|
+
expect(br.redis_key(:another, :key)).to eq("predictor-test:BaseRecommender:another:key")
|
109
|
+
expect(br.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:BaseRecommender:another:set:of:keys")
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should respect the class prefix configuration setting" do
|
113
|
+
br = BaseRecommender.new
|
114
|
+
|
115
|
+
BaseRecommender.redis_prefix('base')
|
116
|
+
expect(br.redis_key).to eq("predictor-test:base")
|
117
|
+
expect(br.redis_key(:another)).to eq("predictor-test:base:another")
|
118
|
+
expect(br.redis_key(:another, :key)).to eq("predictor-test:base:another:key")
|
119
|
+
expect(br.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:base:another:set:of:keys")
|
120
|
+
|
121
|
+
i = 0
|
122
|
+
BaseRecommender.redis_prefix { i += 1 }
|
123
|
+
expect(br.redis_key).to eq("predictor-test:1")
|
124
|
+
expect(br.redis_key(:another)).to eq("predictor-test:2:another")
|
125
|
+
expect(br.redis_key(:another, :key)).to eq("predictor-test:3:another:key")
|
126
|
+
expect(br.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:4:another:set:of:keys")
|
127
|
+
|
128
|
+
BaseRecommender.redis_prefix(nil)
|
129
|
+
expect(br.redis_key).to eq("predictor-test:BaseRecommender")
|
130
|
+
expect(br.redis_key(:another)).to eq("predictor-test:BaseRecommender:another")
|
131
|
+
expect(br.redis_key(:another, :key)).to eq("predictor-test:BaseRecommender:another:key")
|
132
|
+
expect(br.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:BaseRecommender:another:set:of:keys")
|
52
133
|
end
|
53
134
|
end
|
54
135
|
|
@@ -59,8 +140,23 @@ describe Predictor::Base do
|
|
59
140
|
sm = BaseRecommender.new
|
60
141
|
sm.add_to_matrix(:anotherinput, 'a', "foo", "bar")
|
61
142
|
sm.add_to_matrix(:yetanotherinput, 'b', "fnord", "shmoo", "bar")
|
62
|
-
sm.all_items.
|
63
|
-
sm.all_items.length.
|
143
|
+
expect(sm.all_items).to include('foo', 'bar', 'fnord', 'shmoo')
|
144
|
+
expect(sm.all_items.length).to eq(4)
|
145
|
+
end
|
146
|
+
|
147
|
+
it "doesn't return items from other recommenders" do
|
148
|
+
BaseRecommender.input_matrix(:anotherinput)
|
149
|
+
BaseRecommender.input_matrix(:yetanotherinput)
|
150
|
+
UserRecommender.input_matrix(:anotherinput)
|
151
|
+
UserRecommender.input_matrix(:yetanotherinput)
|
152
|
+
sm = BaseRecommender.new
|
153
|
+
sm.add_to_matrix(:anotherinput, 'a', "foo", "bar")
|
154
|
+
sm.add_to_matrix(:yetanotherinput, 'b', "fnord", "shmoo", "bar")
|
155
|
+
expect(sm.all_items).to include('foo', 'bar', 'fnord', 'shmoo')
|
156
|
+
expect(sm.all_items.length).to eq(4)
|
157
|
+
|
158
|
+
ur = UserRecommender.new
|
159
|
+
expect(ur.all_items).to eq([])
|
64
160
|
end
|
65
161
|
end
|
66
162
|
|
@@ -68,7 +164,7 @@ describe Predictor::Base do
|
|
68
164
|
it "calls add_to_set on the given matrix" do
|
69
165
|
BaseRecommender.input_matrix(:anotherinput)
|
70
166
|
sm = BaseRecommender.new
|
71
|
-
sm.anotherinput.
|
167
|
+
expect(sm.anotherinput).to receive(:add_to_set).with('a', 'foo', 'bar')
|
72
168
|
sm.add_to_matrix(:anotherinput, 'a', 'foo', 'bar')
|
73
169
|
end
|
74
170
|
|
@@ -76,7 +172,7 @@ describe Predictor::Base do
|
|
76
172
|
BaseRecommender.input_matrix(:anotherinput)
|
77
173
|
sm = BaseRecommender.new
|
78
174
|
sm.add_to_matrix(:anotherinput, 'a', 'foo', 'bar')
|
79
|
-
sm.all_items.
|
175
|
+
expect(sm.all_items).to include('foo', 'bar')
|
80
176
|
end
|
81
177
|
end
|
82
178
|
|
@@ -84,8 +180,8 @@ describe Predictor::Base do
|
|
84
180
|
it "calls add_to_matrix and process_items! for the given items" do
|
85
181
|
BaseRecommender.input_matrix(:anotherinput)
|
86
182
|
sm = BaseRecommender.new
|
87
|
-
sm.
|
88
|
-
sm.
|
183
|
+
expect(sm).to receive(:add_to_matrix).with(:anotherinput, 'a', 'foo')
|
184
|
+
expect(sm).to receive(:process_items!).with('foo')
|
89
185
|
sm.add_to_matrix!(:anotherinput, 'a', 'foo')
|
90
186
|
end
|
91
187
|
end
|
@@ -100,8 +196,8 @@ describe Predictor::Base do
|
|
100
196
|
sm.yetanotherinput.add_to_set('b', "fnord", "shmoo", "bar")
|
101
197
|
sm.finalinput.add_to_set('c', "nada")
|
102
198
|
sm.process!
|
103
|
-
sm.related_items("bar").
|
104
|
-
sm.related_items("bar").length.
|
199
|
+
expect(sm.related_items("bar")).to include("foo", "fnord", "shmoo")
|
200
|
+
expect(sm.related_items("bar").length).to eq(3)
|
105
201
|
end
|
106
202
|
end
|
107
203
|
|
@@ -119,20 +215,96 @@ describe Predictor::Base do
|
|
119
215
|
sm.tags.add_to_set('tag3', "shmoo", "nada")
|
120
216
|
sm.process!
|
121
217
|
predictions = sm.predictions_for('me', matrix_label: :users)
|
122
|
-
predictions.
|
218
|
+
expect(predictions).to eq(["shmoo", "other", "nada"])
|
123
219
|
predictions = sm.predictions_for(item_set: ["foo", "bar", "fnord"])
|
124
|
-
predictions.
|
220
|
+
expect(predictions).to eq(["shmoo", "other", "nada"])
|
125
221
|
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1, limit: 1)
|
126
|
-
predictions.
|
222
|
+
expect(predictions).to eq(["other"])
|
127
223
|
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1)
|
128
|
-
predictions.
|
224
|
+
expect(predictions).to eq(["other", "nada"])
|
225
|
+
end
|
226
|
+
|
227
|
+
it "accepts a :boost option" do
|
228
|
+
BaseRecommender.input_matrix(:users, weight: 4.0)
|
229
|
+
BaseRecommender.input_matrix(:tags, weight: 1.0)
|
230
|
+
sm = BaseRecommender.new
|
231
|
+
sm.users.add_to_set('me', "foo", "bar", "fnord")
|
232
|
+
sm.users.add_to_set('not_me', "foo", "shmoo")
|
233
|
+
sm.users.add_to_set('another', "fnord", "other")
|
234
|
+
sm.users.add_to_set('another', "nada")
|
235
|
+
sm.tags.add_to_set('tag1', "foo", "fnord", "shmoo")
|
236
|
+
sm.tags.add_to_set('tag2', "bar", "shmoo")
|
237
|
+
sm.tags.add_to_set('tag3', "shmoo", "nada")
|
238
|
+
sm.process!
|
239
|
+
|
240
|
+
# Syntax #1: Tags passed as array, weights assumed to be 1.0
|
241
|
+
predictions = sm.predictions_for('me', matrix_label: :users, boost: {tags: ['tag3']})
|
242
|
+
expect(predictions).to eq(["shmoo", "nada", "other"])
|
243
|
+
predictions = sm.predictions_for(item_set: ["foo", "bar", "fnord"], boost: {tags: ['tag3']})
|
244
|
+
expect(predictions).to eq(["shmoo", "nada", "other"])
|
245
|
+
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1, limit: 1, boost: {tags: ['tag3']})
|
246
|
+
expect(predictions).to eq(["nada"])
|
247
|
+
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1, boost: {tags: ['tag3']})
|
248
|
+
expect(predictions).to eq(["nada", "other"])
|
249
|
+
|
250
|
+
# Syntax #2: Weights explicitly set.
|
251
|
+
predictions = sm.predictions_for('me', matrix_label: :users, boost: {tags: {values: ['tag3'], weight: 1.0}})
|
252
|
+
expect(predictions).to eq(["shmoo", "nada", "other"])
|
253
|
+
predictions = sm.predictions_for(item_set: ["foo", "bar", "fnord"], boost: {tags: {values: ['tag3'], weight: 1.0}})
|
254
|
+
expect(predictions).to eq(["shmoo", "nada", "other"])
|
255
|
+
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1, limit: 1, boost: {tags: {values: ['tag3'], weight: 1.0}})
|
256
|
+
expect(predictions).to eq(["nada"])
|
257
|
+
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1, boost: {tags: {values: ['tag3'], weight: 1.0}})
|
258
|
+
expect(predictions).to eq(["nada", "other"])
|
259
|
+
|
260
|
+
# Make sure weights are actually being passed to Redis.
|
261
|
+
shmoo, nada, other = sm.predictions_for('me', matrix_label: :users, boost: {tags: {values: ['tag3'], weight: 10000.0}}, with_scores: true)
|
262
|
+
expect(shmoo[0]).to eq('shmoo')
|
263
|
+
expect(shmoo[1]).to be > 10000
|
264
|
+
expect(nada[0]).to eq('nada')
|
265
|
+
expect(nada[1]).to be > 10000
|
266
|
+
expect(other[0]).to eq('other')
|
267
|
+
expect(other[1]).to be < 10
|
268
|
+
end
|
269
|
+
|
270
|
+
it "accepts a :boost option, even with an empty item set" do
|
271
|
+
BaseRecommender.input_matrix(:users, weight: 4.0)
|
272
|
+
BaseRecommender.input_matrix(:tags, weight: 1.0)
|
273
|
+
sm = BaseRecommender.new
|
274
|
+
sm.users.add_to_set('not_me', "foo", "shmoo")
|
275
|
+
sm.users.add_to_set('another', "fnord", "other")
|
276
|
+
sm.users.add_to_set('another', "nada")
|
277
|
+
sm.tags.add_to_set('tag1', "foo", "fnord", "shmoo")
|
278
|
+
sm.tags.add_to_set('tag2', "bar", "shmoo")
|
279
|
+
sm.tags.add_to_set('tag3', "shmoo", "nada")
|
280
|
+
sm.process!
|
281
|
+
|
282
|
+
# Syntax #1: Tags passed as array, weights assumed to be 1.0
|
283
|
+
predictions = sm.predictions_for('me', matrix_label: :users, boost: {tags: ['tag3']})
|
284
|
+
expect(predictions).to eq(["shmoo", "nada"])
|
285
|
+
predictions = sm.predictions_for(item_set: [], boost: {tags: ['tag3']})
|
286
|
+
expect(predictions).to eq(["shmoo", "nada"])
|
287
|
+
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1, limit: 1, boost: {tags: ['tag3']})
|
288
|
+
expect(predictions).to eq(["nada"])
|
289
|
+
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1, boost: {tags: ['tag3']})
|
290
|
+
expect(predictions).to eq(["nada"])
|
291
|
+
|
292
|
+
# Syntax #2: Weights explicitly set.
|
293
|
+
predictions = sm.predictions_for('me', matrix_label: :users, boost: {tags: {values: ['tag3'], weight: 1.0}})
|
294
|
+
expect(predictions).to eq(["shmoo", "nada"])
|
295
|
+
predictions = sm.predictions_for(item_set: [], boost: {tags: {values: ['tag3'], weight: 1.0}})
|
296
|
+
expect(predictions).to eq(["shmoo", "nada"])
|
297
|
+
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1, limit: 1, boost: {tags: {values: ['tag3'], weight: 1.0}})
|
298
|
+
expect(predictions).to eq(["nada"])
|
299
|
+
predictions = sm.predictions_for('me', matrix_label: :users, offset: 1, boost: {tags: {values: ['tag3'], weight: 1.0}})
|
300
|
+
expect(predictions).to eq(["nada"])
|
129
301
|
end
|
130
302
|
end
|
131
303
|
|
132
304
|
describe "similarities_for" do
|
133
305
|
it "should not throw exception for non existing items" do
|
134
306
|
sm = BaseRecommender.new
|
135
|
-
sm.similarities_for("not_existing_item").length.
|
307
|
+
expect(sm.similarities_for("not_existing_item").length).to eq(0)
|
136
308
|
end
|
137
309
|
|
138
310
|
it "correctly weighs and sums input matrices" do
|
@@ -150,10 +322,10 @@ describe Predictor::Base do
|
|
150
322
|
sm.tags.add_to_set('tag2', "c1", "c4")
|
151
323
|
|
152
324
|
sm.process!
|
153
|
-
sm.similarities_for("c1", with_scores: true).
|
154
|
-
sm.similarities_for("c2", with_scores: true).
|
155
|
-
sm.similarities_for("c3", with_scores: true).
|
156
|
-
sm.similarities_for("c4", with_scores: true, exclusion_set: ["c3"]).
|
325
|
+
expect(sm.similarities_for("c1", with_scores: true)).to eq([["c4", 6.5], ["c2", 2.0]])
|
326
|
+
expect(sm.similarities_for("c2", with_scores: true)).to eq([["c3", 4.0], ["c1", 2.0], ["c4", 1.5]])
|
327
|
+
expect(sm.similarities_for("c3", with_scores: true)).to eq([["c2", 4.0], ["c4", 0.5]])
|
328
|
+
expect(sm.similarities_for("c4", with_scores: true, exclusion_set: ["c3"])).to eq([["c1", 6.5], ["c2", 1.5]])
|
157
329
|
end
|
158
330
|
end
|
159
331
|
|
@@ -165,9 +337,9 @@ describe Predictor::Base do
|
|
165
337
|
sm.set1.add_to_set "item1", "foo", "bar"
|
166
338
|
sm.set1.add_to_set "item2", "nada", "bar"
|
167
339
|
sm.set2.add_to_set "item3", "bar", "other"
|
168
|
-
sm.sets_for("bar").length.
|
169
|
-
sm.sets_for("bar").
|
170
|
-
sm.sets_for("other").
|
340
|
+
expect(sm.sets_for("bar").length).to eq(3)
|
341
|
+
expect(sm.sets_for("bar")).to include("item1", "item2", "item3")
|
342
|
+
expect(sm.sets_for("other")).to eq(["item3"])
|
171
343
|
end
|
172
344
|
end
|
173
345
|
|
@@ -182,10 +354,10 @@ describe Predictor::Base do
|
|
182
354
|
sm.mysecondinput.add_to_set 'set2', 'item2', 'item3'
|
183
355
|
sm.mythirdinput.add_to_set 'set3', 'item2', 'item3'
|
184
356
|
sm.mythirdinput.add_to_set 'set4', 'item1', 'item2', 'item3'
|
185
|
-
sm.similarities_for('item2').
|
357
|
+
expect(sm.similarities_for('item2')).to be_empty
|
186
358
|
sm.process_items!('item2')
|
187
359
|
similarities = sm.similarities_for('item2', with_scores: true)
|
188
|
-
similarities.
|
360
|
+
expect(similarities).to include(["item3", 4.0], ["item1", 2.5])
|
189
361
|
end
|
190
362
|
end
|
191
363
|
|
@@ -200,11 +372,11 @@ describe Predictor::Base do
|
|
200
372
|
sm.mysecondinput.add_to_set 'set2', 'item2', 'item3'
|
201
373
|
sm.mythirdinput.add_to_set 'set3', 'item2', 'item3'
|
202
374
|
sm.mythirdinput.add_to_set 'set4', 'item1', 'item2', 'item3'
|
203
|
-
sm.similarities_for('item2').
|
375
|
+
expect(sm.similarities_for('item2')).to be_empty
|
204
376
|
sm.process_items!('item2')
|
205
377
|
similarities = sm.similarities_for('item2', with_scores: true)
|
206
|
-
similarities.
|
207
|
-
similarities.length.
|
378
|
+
expect(similarities).to include(["item3", 4.0])
|
379
|
+
expect(similarities.length).to eq(1)
|
208
380
|
end
|
209
381
|
end
|
210
382
|
end
|
@@ -216,8 +388,8 @@ describe Predictor::Base do
|
|
216
388
|
sm = BaseRecommender.new
|
217
389
|
sm.anotherinput.add_to_set('a', "foo", "bar")
|
218
390
|
sm.yetanotherinput.add_to_set('b', "fnord", "shmoo")
|
219
|
-
sm.all_items.
|
220
|
-
sm.
|
391
|
+
expect(sm.all_items).to include("foo", "bar", "fnord", "shmoo")
|
392
|
+
expect(sm).to receive(:process_items!).with(*sm.all_items)
|
221
393
|
sm.process!
|
222
394
|
end
|
223
395
|
end
|
@@ -230,8 +402,8 @@ describe Predictor::Base do
|
|
230
402
|
sm.anotherinput.add_to_set('a', "foo", "bar")
|
231
403
|
sm.yetanotherinput.add_to_set('b', "bar", "shmoo")
|
232
404
|
sm.process!
|
233
|
-
sm.similarities_for('bar').
|
234
|
-
sm.anotherinput.
|
405
|
+
expect(sm.similarities_for('bar')).to include('foo', 'shmoo')
|
406
|
+
expect(sm.anotherinput).to receive(:delete_item).with('foo')
|
235
407
|
sm.delete_from_matrix!(:anotherinput, 'foo')
|
236
408
|
end
|
237
409
|
|
@@ -242,9 +414,9 @@ describe Predictor::Base do
|
|
242
414
|
sm.anotherinput.add_to_set('a', "foo", "bar")
|
243
415
|
sm.yetanotherinput.add_to_set('b', "bar", "shmoo")
|
244
416
|
sm.process!
|
245
|
-
sm.similarities_for('bar').
|
417
|
+
expect(sm.similarities_for('bar')).to include('foo', 'shmoo')
|
246
418
|
sm.delete_from_matrix!(:anotherinput, 'foo')
|
247
|
-
sm.similarities_for('bar').
|
419
|
+
expect(sm.similarities_for('bar')).to eq(['shmoo'])
|
248
420
|
end
|
249
421
|
end
|
250
422
|
|
@@ -253,8 +425,8 @@ describe Predictor::Base do
|
|
253
425
|
BaseRecommender.input_matrix(:myfirstinput)
|
254
426
|
BaseRecommender.input_matrix(:mysecondinput)
|
255
427
|
sm = BaseRecommender.new
|
256
|
-
sm.myfirstinput.
|
257
|
-
sm.mysecondinput.
|
428
|
+
expect(sm.myfirstinput).to receive(:delete_item).with("fnorditem")
|
429
|
+
expect(sm.mysecondinput).to receive(:delete_item).with("fnorditem")
|
258
430
|
sm.delete_item!("fnorditem")
|
259
431
|
end
|
260
432
|
|
@@ -263,9 +435,9 @@ describe Predictor::Base do
|
|
263
435
|
sm = BaseRecommender.new
|
264
436
|
sm.anotherinput.add_to_set('a', "foo", "bar")
|
265
437
|
sm.process!
|
266
|
-
sm.all_items.
|
438
|
+
expect(sm.all_items).to include('foo')
|
267
439
|
sm.delete_item!('foo')
|
268
|
-
sm.all_items.
|
440
|
+
expect(sm.all_items).not_to include('foo')
|
269
441
|
end
|
270
442
|
|
271
443
|
it "should remove the item's similarities and also remove the item from related_items' similarities" do
|
@@ -275,11 +447,11 @@ describe Predictor::Base do
|
|
275
447
|
sm.anotherinput.add_to_set('a', "foo", "bar")
|
276
448
|
sm.yetanotherinput.add_to_set('b', "bar", "shmoo")
|
277
449
|
sm.process!
|
278
|
-
sm.similarities_for('bar').
|
279
|
-
sm.similarities_for('shmoo').
|
450
|
+
expect(sm.similarities_for('bar')).to include('foo', 'shmoo')
|
451
|
+
expect(sm.similarities_for('shmoo')).to include('bar')
|
280
452
|
sm.delete_item!('shmoo')
|
281
|
-
sm.similarities_for('bar').
|
282
|
-
sm.similarities_for('shmoo').
|
453
|
+
expect(sm.similarities_for('bar')).not_to include('shmoo')
|
454
|
+
expect(sm.similarities_for('shmoo')).to be_empty
|
283
455
|
end
|
284
456
|
end
|
285
457
|
|
@@ -291,9 +463,10 @@ describe Predictor::Base do
|
|
291
463
|
sm.set1.add_to_set "item1", "foo", "bar"
|
292
464
|
sm.set1.add_to_set "item2", "nada", "bar"
|
293
465
|
sm.set2.add_to_set "item3", "bar", "other"
|
294
|
-
|
466
|
+
|
467
|
+
expect(Predictor.redis.keys(sm.redis_key('*'))).not_to be_empty
|
295
468
|
sm.clean!
|
296
|
-
Predictor.redis.keys(
|
469
|
+
expect(Predictor.redis.keys(sm.redis_key('*'))).to be_empty
|
297
470
|
end
|
298
471
|
end
|
299
472
|
|
@@ -304,20 +477,20 @@ describe Predictor::Base do
|
|
304
477
|
BaseRecommender.input_matrix(:myfirstinput)
|
305
478
|
sm = BaseRecommender.new
|
306
479
|
sm.myfirstinput.add_to_set *(['set1'] + 130.times.map{|i| "item#{i}"})
|
307
|
-
sm.similarities_for('item2').
|
480
|
+
expect(sm.similarities_for('item2')).to be_empty
|
308
481
|
sm.process_items!('item2')
|
309
|
-
sm.similarities_for('item2').length.
|
482
|
+
expect(sm.similarities_for('item2').length).to eq(129)
|
310
483
|
|
311
484
|
redis = Predictor.redis
|
312
485
|
key = sm.redis_key(:similarities, 'item2')
|
313
|
-
redis.zcard(key).
|
314
|
-
redis.object(:encoding, key).
|
486
|
+
expect(redis.zcard(key)).to eq(129)
|
487
|
+
expect(redis.object(:encoding, key)).to eq('skiplist') # Inefficient
|
315
488
|
|
316
489
|
BaseRecommender.reset_similarity_limit!
|
317
490
|
sm.ensure_similarity_limit_is_obeyed!
|
318
491
|
|
319
|
-
redis.zcard(key).
|
320
|
-
redis.object(:encoding, key).
|
492
|
+
expect(redis.zcard(key)).to eq(128)
|
493
|
+
expect(redis.object(:encoding, key)).to eq('ziplist') # Efficient
|
321
494
|
end
|
322
495
|
end
|
323
496
|
end
|
data/spec/input_matrix_spec.rb
CHANGED
@@ -6,7 +6,8 @@ describe Predictor::InputMatrix do
|
|
6
6
|
before(:each) { @options = {} }
|
7
7
|
|
8
8
|
before(:all) do
|
9
|
-
@
|
9
|
+
@base = BaseRecommender.new
|
10
|
+
@default_options = { base: @base, key: "mymatrix" }
|
10
11
|
@matrix = Predictor::InputMatrix.new(@default_options)
|
11
12
|
end
|
12
13
|
|
@@ -14,45 +15,96 @@ describe Predictor::InputMatrix do
|
|
14
15
|
flush_redis!
|
15
16
|
end
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
describe "redis_key" do
|
19
|
+
it "should respect the global namespace configuration" do
|
20
|
+
expect(@matrix.redis_key).to eq("predictor-test:BaseRecommender:mymatrix")
|
21
|
+
expect(@matrix.redis_key(:another)).to eq("predictor-test:BaseRecommender:mymatrix:another")
|
22
|
+
expect(@matrix.redis_key(:another, :key)).to eq("predictor-test:BaseRecommender:mymatrix:another:key")
|
23
|
+
expect(@matrix.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:BaseRecommender:mymatrix:another:set:of:keys")
|
24
|
+
|
25
|
+
i = 0
|
26
|
+
Predictor.redis_prefix { i += 1 }
|
27
|
+
expect(@matrix.redis_key).to eq("1:BaseRecommender:mymatrix")
|
28
|
+
expect(@matrix.redis_key(:another)).to eq("2:BaseRecommender:mymatrix:another")
|
29
|
+
expect(@matrix.redis_key(:another, :key)).to eq("3:BaseRecommender:mymatrix:another:key")
|
30
|
+
expect(@matrix.redis_key(:another, [:set, :of, :keys])).to eq("4:BaseRecommender:mymatrix:another:set:of:keys")
|
31
|
+
|
32
|
+
Predictor.redis_prefix(nil)
|
33
|
+
expect(@matrix.redis_key).to eq("predictor:BaseRecommender:mymatrix")
|
34
|
+
expect(@matrix.redis_key(:another)).to eq("predictor:BaseRecommender:mymatrix:another")
|
35
|
+
expect(@matrix.redis_key(:another, :key)).to eq("predictor:BaseRecommender:mymatrix:another:key")
|
36
|
+
expect(@matrix.redis_key(:another, [:set, :of, :keys])).to eq("predictor:BaseRecommender:mymatrix:another:set:of:keys")
|
37
|
+
|
38
|
+
Predictor.redis_prefix('predictor-test')
|
39
|
+
expect(@matrix.redis_key).to eq("predictor-test:BaseRecommender:mymatrix")
|
40
|
+
expect(@matrix.redis_key(:another)).to eq("predictor-test:BaseRecommender:mymatrix:another")
|
41
|
+
expect(@matrix.redis_key(:another, :key)).to eq("predictor-test:BaseRecommender:mymatrix:another:key")
|
42
|
+
expect(@matrix.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:BaseRecommender:mymatrix:another:set:of:keys")
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should respect the class-level configuration" do
|
46
|
+
i = 0
|
47
|
+
BaseRecommender.redis_prefix { i += 1 }
|
48
|
+
expect(@matrix.redis_key).to eq("predictor-test:1:mymatrix")
|
49
|
+
expect(@matrix.redis_key(:another)).to eq("predictor-test:2:mymatrix:another")
|
50
|
+
expect(@matrix.redis_key(:another, :key)).to eq("predictor-test:3:mymatrix:another:key")
|
51
|
+
expect(@matrix.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:4:mymatrix:another:set:of:keys")
|
52
|
+
|
53
|
+
BaseRecommender.redis_prefix([nil])
|
54
|
+
expect(@matrix.redis_key).to eq("predictor-test:mymatrix")
|
55
|
+
expect(@matrix.redis_key(:another)).to eq("predictor-test:mymatrix:another")
|
56
|
+
expect(@matrix.redis_key(:another, :key)).to eq("predictor-test:mymatrix:another:key")
|
57
|
+
expect(@matrix.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:mymatrix:another:set:of:keys")
|
58
|
+
|
59
|
+
BaseRecommender.redis_prefix(['a', 'b'])
|
60
|
+
expect(@matrix.redis_key).to eq("predictor-test:a:b:mymatrix")
|
61
|
+
expect(@matrix.redis_key(:another)).to eq("predictor-test:a:b:mymatrix:another")
|
62
|
+
expect(@matrix.redis_key(:another, :key)).to eq("predictor-test:a:b:mymatrix:another:key")
|
63
|
+
expect(@matrix.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:a:b:mymatrix:another:set:of:keys")
|
64
|
+
|
65
|
+
BaseRecommender.redis_prefix(nil)
|
66
|
+
expect(@matrix.redis_key).to eq("predictor-test:BaseRecommender:mymatrix")
|
67
|
+
expect(@matrix.redis_key(:another)).to eq("predictor-test:BaseRecommender:mymatrix:another")
|
68
|
+
expect(@matrix.redis_key(:another, :key)).to eq("predictor-test:BaseRecommender:mymatrix:another:key")
|
69
|
+
expect(@matrix.redis_key(:another, [:set, :of, :keys])).to eq("predictor-test:BaseRecommender:mymatrix:another:set:of:keys")
|
70
|
+
end
|
19
71
|
end
|
20
72
|
|
21
73
|
describe "weight" do
|
22
74
|
it "returns the weight configured or a default of 1" do
|
23
|
-
@matrix.weight.
|
75
|
+
expect(@matrix.weight).to eq(1.0) # default weight
|
24
76
|
matrix = Predictor::InputMatrix.new(redis_prefix: "predictor-test", key: "mymatrix", weight: 5.0)
|
25
|
-
matrix.weight.
|
77
|
+
expect(matrix.weight).to eq(5.0)
|
26
78
|
end
|
27
79
|
end
|
28
80
|
|
29
81
|
describe "add_to_set" do
|
30
82
|
it "adds each member of the set to the key's 'sets' set" do
|
31
|
-
@matrix.items_for("item1").
|
83
|
+
expect(@matrix.items_for("item1")).not_to include("foo", "bar", "fnord", "blubb")
|
32
84
|
@matrix.add_to_set "item1", "foo", "bar", "fnord", "blubb"
|
33
|
-
@matrix.items_for("item1").
|
85
|
+
expect(@matrix.items_for("item1")).to include("foo", "bar", "fnord", "blubb")
|
34
86
|
end
|
35
87
|
|
36
88
|
it "adds the key to each set member's 'items' set" do
|
37
|
-
@matrix.sets_for("foo").
|
38
|
-
@matrix.sets_for("bar").
|
39
|
-
@matrix.sets_for("fnord").
|
40
|
-
@matrix.sets_for("blubb").
|
89
|
+
expect(@matrix.sets_for("foo")).not_to include("item1")
|
90
|
+
expect(@matrix.sets_for("bar")).not_to include("item1")
|
91
|
+
expect(@matrix.sets_for("fnord")).not_to include("item1")
|
92
|
+
expect(@matrix.sets_for("blubb")).not_to include("item1")
|
41
93
|
@matrix.add_to_set "item1", "foo", "bar", "fnord", "blubb"
|
42
|
-
@matrix.sets_for("foo").
|
43
|
-
@matrix.sets_for("bar").
|
44
|
-
@matrix.sets_for("fnord").
|
45
|
-
@matrix.sets_for("blubb").
|
94
|
+
expect(@matrix.sets_for("foo")).to include("item1")
|
95
|
+
expect(@matrix.sets_for("bar")).to include("item1")
|
96
|
+
expect(@matrix.sets_for("fnord")).to include("item1")
|
97
|
+
expect(@matrix.sets_for("blubb")).to include("item1")
|
46
98
|
end
|
47
99
|
end
|
48
100
|
|
49
101
|
describe "items_for" do
|
50
102
|
it "returns the items in the given set ID" do
|
51
103
|
@matrix.add_to_set "item1", ["foo", "bar", "fnord", "blubb"]
|
52
|
-
@matrix.items_for("item1").
|
104
|
+
expect(@matrix.items_for("item1")).to include("foo", "bar", "fnord", "blubb")
|
53
105
|
@matrix.add_to_set "item2", ["foo", "bar", "snafu", "nada"]
|
54
|
-
@matrix.items_for("item2").
|
55
|
-
@matrix.items_for("item1").
|
106
|
+
expect(@matrix.items_for("item2")).to include("foo", "bar", "snafu", "nada")
|
107
|
+
expect(@matrix.items_for("item1")).not_to include("snafu", "nada")
|
56
108
|
end
|
57
109
|
end
|
58
110
|
|
@@ -60,8 +112,8 @@ describe Predictor::InputMatrix do
|
|
60
112
|
it "returns the set IDs the given item is in" do
|
61
113
|
@matrix.add_to_set "item1", ["foo", "bar", "fnord", "blubb"]
|
62
114
|
@matrix.add_to_set "item2", ["foo", "bar", "snafu", "nada"]
|
63
|
-
@matrix.sets_for("foo").
|
64
|
-
@matrix.sets_for("snafu").
|
115
|
+
expect(@matrix.sets_for("foo")).to include("item1", "item2")
|
116
|
+
expect(@matrix.sets_for("snafu")).to eq(["item2"])
|
65
117
|
end
|
66
118
|
end
|
67
119
|
|
@@ -70,11 +122,11 @@ describe Predictor::InputMatrix do
|
|
70
122
|
@matrix.add_to_set "item1", ["foo", "bar", "fnord", "blubb"]
|
71
123
|
@matrix.add_to_set "item2", ["foo", "bar", "snafu", "nada"]
|
72
124
|
@matrix.add_to_set "item3", ["nada", "other"]
|
73
|
-
@matrix.related_items("bar").
|
74
|
-
@matrix.related_items("bar").length.
|
75
|
-
@matrix.related_items("other").
|
76
|
-
@matrix.related_items("snafu").
|
77
|
-
@matrix.related_items("snafu").length.
|
125
|
+
expect(@matrix.related_items("bar")).to include("foo", "fnord", "blubb", "snafu", "nada")
|
126
|
+
expect(@matrix.related_items("bar").length).to eq(5)
|
127
|
+
expect(@matrix.related_items("other")).to eq(["nada"])
|
128
|
+
expect(@matrix.related_items("snafu")).to include("foo", "bar", "nada")
|
129
|
+
expect(@matrix.related_items("snafu").length).to eq(3)
|
78
130
|
end
|
79
131
|
end
|
80
132
|
|
@@ -86,13 +138,13 @@ describe Predictor::InputMatrix do
|
|
86
138
|
end
|
87
139
|
|
88
140
|
it "should delete the item from sets it is in" do
|
89
|
-
@matrix.items_for("item1").
|
90
|
-
@matrix.items_for("item2").
|
91
|
-
@matrix.sets_for("bar").
|
141
|
+
expect(@matrix.items_for("item1")).to include("bar")
|
142
|
+
expect(@matrix.items_for("item2")).to include("bar")
|
143
|
+
expect(@matrix.sets_for("bar")).to include("item1", "item2")
|
92
144
|
@matrix.delete_item("bar")
|
93
|
-
@matrix.items_for("item1").
|
94
|
-
@matrix.items_for("item2").
|
95
|
-
@matrix.sets_for("bar").
|
145
|
+
expect(@matrix.items_for("item1")).not_to include("bar")
|
146
|
+
expect(@matrix.items_for("item2")).not_to include("bar")
|
147
|
+
expect(@matrix.sets_for("bar")).to be_empty
|
96
148
|
end
|
97
149
|
end
|
98
150
|
|
@@ -105,7 +157,7 @@ describe Predictor::InputMatrix do
|
|
105
157
|
matrix.add_to_set "item2", "bar", "fnord", "shmoo", "snafu"
|
106
158
|
matrix.add_to_set "item3", "bar", "nada", "snafu"
|
107
159
|
|
108
|
-
matrix.score("bar", "snafu").
|
160
|
+
expect(matrix.score("bar", "snafu")).to eq(2.0/3.0)
|
109
161
|
end
|
110
162
|
|
111
163
|
it "scores as jaccard index when given option" do
|
@@ -114,13 +166,13 @@ describe Predictor::InputMatrix do
|
|
114
166
|
matrix.add_to_set "item2", "bar", "fnord", "shmoo", "snafu"
|
115
167
|
matrix.add_to_set "item3", "bar", "nada", "snafu"
|
116
168
|
|
117
|
-
matrix.score("bar", "snafu").
|
169
|
+
expect(matrix.score("bar", "snafu")).to eq(2.0/3.0)
|
118
170
|
end
|
119
171
|
|
120
172
|
it "should handle missing sets" do
|
121
173
|
matrix.add_to_set "item1", "foo", "bar", "fnord", "blubb"
|
122
174
|
|
123
|
-
matrix.score("is", "missing").
|
175
|
+
expect(matrix.score("is", "missing")).to eq(0.0)
|
124
176
|
end
|
125
177
|
end
|
126
178
|
|
@@ -132,13 +184,13 @@ describe Predictor::InputMatrix do
|
|
132
184
|
matrix.add_to_set "item2", "fnord", "shmoo", "snafu"
|
133
185
|
matrix.add_to_set "item3", "bar", "nada", "snafu"
|
134
186
|
|
135
|
-
matrix.score("bar", "snafu").
|
187
|
+
expect(matrix.score("bar", "snafu")).to eq(2.0/4.0)
|
136
188
|
end
|
137
189
|
|
138
190
|
it "should handle missing sets" do
|
139
191
|
matrix.add_to_set "item1", "foo", "bar", "fnord", "blubb"
|
140
192
|
|
141
|
-
matrix.score("is", "missing").
|
193
|
+
expect(matrix.score("is", "missing")).to eq(0.0)
|
142
194
|
end
|
143
195
|
end
|
144
196
|
end
|
data/spec/predictor_spec.rb
CHANGED
@@ -4,12 +4,12 @@ describe Predictor do
|
|
4
4
|
|
5
5
|
it "should store a redis connection" do
|
6
6
|
Predictor.redis = "asd"
|
7
|
-
Predictor.redis.
|
7
|
+
expect(Predictor.redis).to eq("asd")
|
8
8
|
end
|
9
9
|
|
10
10
|
it "should raise an exception if unconfigured redis connection is accessed" do
|
11
11
|
Predictor.redis = nil
|
12
|
-
|
12
|
+
expect{ Predictor.redis }.to raise_error(/not configured/i)
|
13
13
|
end
|
14
14
|
|
15
15
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -8,12 +8,14 @@ def flush_redis!
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
|
11
|
+
Predictor.redis_prefix "predictor-test"
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
class BaseRecommender
|
14
|
+
include Predictor::Base
|
15
|
+
end
|
16
16
|
|
17
|
+
class UserRecommender
|
18
|
+
include Predictor::Base
|
17
19
|
end
|
18
20
|
|
19
21
|
class TestRecommender
|
@@ -23,7 +25,6 @@ class TestRecommender
|
|
23
25
|
end
|
24
26
|
|
25
27
|
class Predictor::TestInputMatrix
|
26
|
-
|
27
28
|
def initialize(opts)
|
28
29
|
@opts = opts
|
29
30
|
end
|
@@ -31,5 +32,4 @@ class Predictor::TestInputMatrix
|
|
31
32
|
def method_missing(method, *args)
|
32
33
|
@opts[method]
|
33
34
|
end
|
34
|
-
|
35
35
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: predictor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pathgather
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
11
|
+
date: 2014-06-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|