commendo 1.2.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/bin/commendo-create-mysql-db +3 -0
  4. data/bin/commendo-create.sql +99 -0
  5. data/bin/commendo-load-tsv +11 -5
  6. data/bin/commendo-load-tsv-mysql.rb +43 -0
  7. data/bin/commendo-time-mysql.rb +31 -0
  8. data/commendo.gemspec +4 -2
  9. data/lib/commendo.rb +24 -0
  10. data/lib/commendo/configuration.rb +25 -0
  11. data/lib/commendo/content_set.rb +13 -182
  12. data/lib/commendo/mysql-backed/content_set.rb +152 -0
  13. data/lib/commendo/mysql-backed/tag_set.rb +81 -0
  14. data/lib/commendo/mysql-backed/weighted_group.rb +40 -0
  15. data/lib/commendo/redis-backed/content_set.rb +194 -0
  16. data/lib/commendo/{pair_comparison.lua → redis-backed/pair_comparison.lua} +0 -0
  17. data/lib/commendo/{similarity.lua → redis-backed/similarity.lua} +0 -0
  18. data/lib/commendo/redis-backed/tag_set.rb +54 -0
  19. data/lib/commendo/redis-backed/weighted_group.rb +54 -0
  20. data/lib/commendo/tag_set.rb +6 -42
  21. data/lib/commendo/version.rb +1 -1
  22. data/lib/commendo/weighted_group.rb +7 -41
  23. data/lib/mysql2/client.rb +17 -0
  24. data/model 2.mwb +0 -0
  25. data/sql_model.mwb +0 -0
  26. data/test/configuration_test.rb +71 -0
  27. data/test/mysql_content_set_test.rb +40 -0
  28. data/test/mysql_tag_set_test.rb +34 -0
  29. data/test/mysql_weighted_group_test.rb +54 -0
  30. data/test/redis_content_set_test.rb +57 -0
  31. data/test/redis_tag_set_test.rb +31 -0
  32. data/test/redis_weighted_group_test.rb +49 -0
  33. data/test/tests_for_content_sets.rb +379 -0
  34. data/test/tests_for_tag_sets.rb +130 -0
  35. data/test/tests_for_weighted_groups.rb +106 -0
  36. metadata +72 -12
  37. data/test/content_set_test.rb +0 -408
  38. data/test/tag_set_test.rb +0 -128
  39. data/test/weighted_group_test.rb +0 -191
@@ -0,0 +1,57 @@
1
+ require_relative 'tests_for_content_sets.rb'
2
+ gem 'minitest'
3
+ require 'minitest/autorun'
4
+ require 'minitest/pride'
5
+ require 'minitest/mock'
6
+ require 'mocha/setup'
7
+ require 'commendo'
8
+
9
+ module Commendo
10
+
11
+ class RedisContentSetTest < Minitest::Test
12
+
13
+ def setup
14
+ Commendo.config do |config|
15
+ config.backend = :redis
16
+ config.host = 'localhost'
17
+ config.port = 6379
18
+ config.database = 15
19
+ end
20
+ Redis.new(host: Commendo.config.host, port: Commendo.config.port, db: Commendo.config.database).flushdb
21
+ @key_base = 'CommendoTests'
22
+ @cs = ContentSet.new(key_base: @key_base)
23
+ end
24
+
25
+ def create_tag_set(kb)
26
+ Commendo::TagSet.new(key_base: kb)
27
+ end
28
+
29
+ def create_content_set(key_base, ts = nil)
30
+ Commendo::ContentSet.new(key_base: key_base, tag_set: ts)
31
+ end
32
+
33
+ def test_gives_similarity_key_for_resource
34
+ key_base = 'CommendoTestsFooBarBaz'
35
+ cs = create_content_set(key_base)
36
+ assert_equal 'CommendoTestsFooBarBaz:similar:resource-1', cs.similarity_key('resource-1')
37
+ end
38
+
39
+ def test_calculate_yields_after_each
40
+ (3..23).each do |group|
41
+ (3..23).each do |res|
42
+ @cs.add_by_group(group, res) if res % group == 0
43
+ end
44
+ end
45
+ expected_keys = ['CommendoTests:resources:3', 'CommendoTests:resources:4', 'CommendoTests:resources:5', 'CommendoTests:resources:6', 'CommendoTests:resources:7', 'CommendoTests:resources:8', 'CommendoTests:resources:9', 'CommendoTests:resources:10', 'CommendoTests:resources:11', 'CommendoTests:resources:12', 'CommendoTests:resources:13', 'CommendoTests:resources:14', 'CommendoTests:resources:15', 'CommendoTests:resources:16', 'CommendoTests:resources:17', 'CommendoTests:resources:18', 'CommendoTests:resources:19', 'CommendoTests:resources:20', 'CommendoTests:resources:21', 'CommendoTests:resources:22', 'CommendoTests:resources:23']
46
+ actual_keys = []
47
+ @cs.calculate_similarity { |key, index, total|
48
+ actual_keys << key
49
+ }
50
+ assert_equal expected_keys.sort, actual_keys.sort
51
+ end
52
+
53
+ include TestsForContentSets
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'tests_for_tag_sets'
2
+ gem 'minitest'
3
+ require 'minitest/autorun'
4
+ require 'minitest/pride'
5
+ require 'minitest/mock'
6
+ require 'mocha/setup'
7
+ require 'commendo'
8
+
9
+ module Commendo
10
+
11
+ class RedisTagSetTest < Minitest::Test
12
+
13
+ def setup
14
+ Commendo.config do |config|
15
+ config.backend = :redis
16
+ config.host = 'localhost'
17
+ config.port = 6379
18
+ config.database = 15
19
+ end
20
+ Redis.new(host: Commendo.config.host, port: Commendo.config.port, db: Commendo.config.database).flushdb
21
+ @ts = TagSet.new(key_base: 'TagSetTest')
22
+ end
23
+
24
+ def create_tag_set(kb)
25
+ Commendo::TagSet.new(key_base: kb)
26
+ end
27
+
28
+ include TestsForTagSets
29
+
30
+ end
31
+ end
@@ -0,0 +1,49 @@
1
+ require_relative 'tests_for_weighted_groups'
2
+ gem 'minitest'
3
+ require 'minitest/autorun'
4
+ require 'minitest/pride'
5
+ require 'minitest/mock'
6
+ require 'mocha/setup'
7
+ require 'commendo'
8
+
9
+ module Commendo
10
+
11
+ class RedisWeightedGroupTest < Minitest::Test
12
+
13
+ def setup
14
+ super
15
+ Commendo.config do |config|
16
+ config.backend = :redis
17
+ config.host = 'localhost'
18
+ config.port = 6379
19
+ config.database = 15
20
+ end
21
+ Redis.new(host: Commendo.config.host, port: Commendo.config.port, db: Commendo.config.database).flushdb
22
+ @tag_set = TagSet.new(key_base: 'CommendoTests:Tags')
23
+ @cs1 = ContentSet.new(key_base: 'CommendoTests:ContentSet1', tag_set: @tag_set)
24
+ @cs2 = ContentSet.new(key_base: 'CommendoTests:ContentSet2', tag_set: @tag_set)
25
+ @cs3 = ContentSet.new(key_base: 'CommendoTests:ContentSet3', tag_set: @tag_set)
26
+ (3..23).each do |group|
27
+ (3..23).each do |res|
28
+ @cs1.add_by_group(group, res) if res.modulo(group).zero? && res.modulo(2).zero?
29
+ @cs2.add_by_group(group, res) if res.modulo(group).zero? && res.modulo(3).zero?
30
+ @cs3.add_by_group(group, res) if res.modulo(group).zero? && res.modulo(6).zero?
31
+ @tag_set.add(res, 'mod3') if res.modulo(3).zero?
32
+ @tag_set.add(res, 'mod4') if res.modulo(4).zero?
33
+ @tag_set.add(res, 'mod5') if res.modulo(5).zero?
34
+ @tag_set.add(res, 'mod7') if res.modulo(7).zero?
35
+ end
36
+ end
37
+ [@cs1, @cs2, @cs3].each { |cs| cs.calculate_similarity }
38
+ @weighted_group = Commendo::WeightedGroup.new(key_base: 'CommendoTests:WeightedGroup',
39
+ content_sets: [{cs: @cs1, weight: 1.0},
40
+ {cs: @cs2, weight: 10.0},
41
+ {cs: @cs3, weight: 100.0}]
42
+ )
43
+ end
44
+
45
+ include TestsForWeightedGroups
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,379 @@
1
+ module TestsForContentSets
2
+
3
+ def contains_resource(resource, similarities)
4
+ !similarities.select { |sim| sim[:resource] == "#{resource}" }.empty?
5
+ end
6
+
7
+ def similar_to(cs, resource, similar)
8
+ contains_resource(similar, cs.similar_to(resource))
9
+ end
10
+
11
+ def test_recommends_for_many_applies_filters
12
+ ts = create_tag_set("#{@key_base}:tags")
13
+ @cs = create_content_set(@key_base, ts)
14
+ (3..23).each do |group|
15
+ (3..23).each do |res|
16
+ @cs.add(res, group) if res % group == 0
17
+ ts.add(res, 'mod3') if res.modulo(3).zero?
18
+ ts.add(res, 'mod4') if res.modulo(4).zero?
19
+ ts.add(res, 'mod5') if res.modulo(5).zero?
20
+ end
21
+ end
22
+ @cs.calculate_similarity
23
+ actual = @cs.filtered_similar_to([12, 6, 9], include: ['mod4'], exclude: ['mod3', 'mod5'])
24
+ refute contains_resource('6', actual)
25
+ refute contains_resource('18', actual)
26
+ assert contains_resource('4', actual)
27
+ refute contains_resource('3', actual)
28
+ refute contains_resource('9', actual)
29
+ assert contains_resource('8', actual)
30
+ refute contains_resource('21', actual)
31
+ assert contains_resource('16', actual)
32
+ refute contains_resource('15', actual)
33
+ refute contains_resource('20', actual)
34
+ end
35
+
36
+ def test_recommends_for_many
37
+ ts = create_tag_set("#{@key_base}:tags")
38
+ @cs = create_content_set(@key_base, ts)
39
+ (3..23).each do |group|
40
+ (3..23).each do |res|
41
+ @cs.add(res, group) if res % group == 0
42
+ ts.add(res, 'mod3') if res.modulo(3).zero?
43
+ ts.add(res, 'mod4') if res.modulo(4).zero?
44
+ ts.add(res, 'mod5') if res.modulo(5).zero?
45
+ end
46
+ end
47
+ @cs.calculate_similarity
48
+ expected = [
49
+ {resource: '18', similarity: 1.834},
50
+ {resource: '3', similarity: 1.734},
51
+ {resource: '6', similarity: 1.167},
52
+ {resource: '21', similarity: 1.086},
53
+ {resource: '15', similarity: 1.086},
54
+ {resource: '12', similarity: 1.0},
55
+ {resource: '9', similarity: 0.833},
56
+ {resource: '4', similarity: 0.4},
57
+ {resource: '8', similarity: 0.333},
58
+ {resource: '16', similarity: 0.286},
59
+ {resource: '20', similarity: 0.25}
60
+ ]
61
+ actual = @cs.similar_to([12, 6, 9])
62
+ assert_equal expected, actual
63
+ #, include: ['mod4'], exclude: ['mod3', 'mod5']
64
+ end
65
+
66
+ def test_filters_includes_and_exclude_by_tag_collection
67
+ ts = create_tag_set("#{@key_base}:tags")
68
+ @cs = create_content_set(@key_base, ts)
69
+ #Build some test data
70
+ (3..23).each do |group|
71
+ (3..23).each do |res|
72
+ @cs.add(res, group) if res % group == 0
73
+ ts.add(res, 'mod3') if res.modulo(3).zero?
74
+ ts.add(res, 'mod4') if res.modulo(4).zero?
75
+ ts.add(res, 'mod5') if res.modulo(5).zero?
76
+ end
77
+ end
78
+ @cs.calculate_similarity
79
+
80
+ actual = @cs.filtered_similar_to(12, include: ['mod4'], exclude: ['mod3', 'mod5'])
81
+ assert_equal 3, actual.length
82
+
83
+ refute contains_resource('6', actual)
84
+ refute contains_resource('18', actual)
85
+ assert contains_resource('4', actual)
86
+ refute contains_resource('3', actual)
87
+ refute contains_resource('9', actual)
88
+ assert contains_resource('8', actual)
89
+ refute contains_resource('21', actual)
90
+ assert contains_resource('16', actual)
91
+ refute contains_resource('15', actual)
92
+ refute contains_resource('20', actual)
93
+ end
94
+
95
+ def test_filters_exclude_by_tag_collection
96
+ ts = create_tag_set("#{@key_base}:tags")
97
+ @cs = create_content_set(@key_base, ts)
98
+ (3..23).each do |group|
99
+ (3..23).each do |res|
100
+ @cs.add(res, group) if res % group == 0
101
+ ts.add(res, 'mod3') if res.modulo(3).zero?
102
+ ts.add(res, 'mod4') if res.modulo(4).zero?
103
+ ts.add(res, 'mod5') if res.modulo(5).zero?
104
+ end
105
+ end
106
+ @cs.calculate_similarity
107
+
108
+ actual = @cs.filtered_similar_to(10, exclude: ['mod3'])
109
+ assert_equal 2, actual.length
110
+ assert contains_resource('5', actual)
111
+ assert contains_resource('20', actual)
112
+ refute contains_resource('15', actual)
113
+ end
114
+
115
+ def test_filters_include_by_tag_collection_and_limit
116
+ ts = create_tag_set("#{@key_base}:tags")
117
+ @cs = create_content_set(@key_base, ts)
118
+ (3..23).each do |group|
119
+ (3..23).each do |res|
120
+ @cs.add(res, group) if res % group == 0
121
+ ts.add(res, 'mod3') if res.modulo(3).zero?
122
+ ts.add(res, 'mod4') if res.modulo(4).zero?
123
+ ts.add(res, 'mod5') if res.modulo(5).zero?
124
+ end
125
+ end
126
+ @cs.calculate_similarity
127
+
128
+ actual = @cs.filtered_similar_to(10, include: ['mod5'], limit: 2)
129
+ assert_equal 2, actual.length
130
+ assert contains_resource('5', actual)
131
+ #assert contains_resource('15', actual)
132
+ assert contains_resource('20', actual)
133
+
134
+ end
135
+
136
+ def test_filters_include_by_tag_collection
137
+ ts = create_tag_set("#{@key_base}:tags")
138
+ @cs = create_content_set(@key_base, ts)
139
+ (3..23).each do |group|
140
+ (3..23).each do |res|
141
+ @cs.add(res, group) if res % group == 0
142
+ ts.add(res, 'mod3') if res.modulo(3).zero?
143
+ ts.add(res, 'mod4') if res.modulo(4).zero?
144
+ ts.add(res, 'mod5') if res.modulo(5).zero?
145
+ end
146
+ end
147
+ @cs.calculate_similarity
148
+
149
+ actual = @cs.filtered_similar_to(10, include: ['mod5'])
150
+ assert_equal 3, actual.length
151
+ assert contains_resource('5', actual)
152
+ assert contains_resource('15', actual)
153
+ assert contains_resource('20', actual)
154
+
155
+ end
156
+
157
+ def test_remove_and_calculate
158
+ (3..23).each do |group|
159
+ (3..23).each do |res|
160
+ @cs.add(res, group) if res % group == 0
161
+ end
162
+ end
163
+ @cs.calculate_similarity
164
+ assert similar_to(@cs, 18, 12)
165
+ @cs.remove_from_groups_and_calculate(18, 6, 3)
166
+ refute similar_to(@cs, 18, 12)
167
+ end
168
+
169
+ def test_accepts_incremental_updates
170
+ (3..23).each do |group|
171
+ (3..23).each do |res|
172
+ @cs.add(res, group) if res % group == 0
173
+ end
174
+ end
175
+ @cs.calculate_similarity
176
+ assert similar_to(@cs, 18, 12)
177
+ refute similar_to(@cs, 10, 12)
178
+
179
+ @cs.add_and_calculate(12, 'foo', true)
180
+ @cs.add_and_calculate(10, 'foo', true)
181
+ assert similar_to(@cs, 10, 12)
182
+ end
183
+
184
+ def test_remove_causes_similarity_to_change_when_recalculated
185
+ (3..23).each do |group|
186
+ (3..23).each do |res|
187
+ @cs.add(res, group) if res % group == 0
188
+ end
189
+ end
190
+ @cs.calculate_similarity
191
+ assert similar_to(@cs, 18, 12)
192
+ @cs.remove_from_groups(18, 6, 3)
193
+ @cs.calculate_similarity
194
+ refute similar_to(@cs, 18, 12)
195
+ end
196
+
197
+ def test_remove_from_groups
198
+ (3..23).each do |group|
199
+ (3..23).each do |res|
200
+ @cs.add(res, group) if res % group == 0
201
+ end
202
+ end
203
+ resource = 20
204
+ assert_equal ['4', '5', '10', '20'].sort!, @cs.groups(resource).sort!
205
+ @cs.remove_from_groups(resource, 10)
206
+ assert_equal ['4', '5', '20'].sort!, @cs.groups(resource).sort!
207
+ @cs.remove_from_groups(resource, 4)
208
+ assert_equal ['5', '20'].sort!, @cs.groups(resource).sort!
209
+ end
210
+
211
+ def test_deletes_resource_from_everywhere
212
+ (3..23).each do |group|
213
+ (3..23).each do |res|
214
+ @cs.add_by_group(group, res) if res % group == 0
215
+ end
216
+ end
217
+ @cs.calculate_similarity
218
+ assert similar_to(@cs, 18, 12)
219
+
220
+ @cs.delete(12)
221
+ assert_equal [], @cs.similar_to(12)
222
+ refute similar_to(@cs, 18, 12)
223
+
224
+ @cs.calculate_similarity
225
+ assert_equal [], @cs.similar_to(12)
226
+ refute similar_to(@cs, 18, 12)
227
+ end
228
+
229
+ def test_calculate_copes_with_missing_resource
230
+ @cs.calculate_similarity_for_resource('999999999999', 0.1)
231
+ end
232
+
233
+ def test_calculates_with_threshold
234
+ (3..23).each do |group|
235
+ (3..23).each do |res|
236
+ @cs.add_by_group(group, res) if res % group == 0
237
+ end
238
+ end
239
+ @cs.calculate_similarity(0.4)
240
+ expected = [
241
+ {resource: '9', similarity: 0.667},
242
+ {resource: '6', similarity: 0.667},
243
+ {resource: '12', similarity: 0.5}
244
+ ]
245
+ assert_equal expected, @cs.similar_to(18)
246
+ end
247
+
248
+ def test_calculates_similarity_scores
249
+ (3..23).each do |group|
250
+ (3..23).each do |res|
251
+ @cs.add_by_group(group, res) if res % group == 0
252
+ end
253
+ end
254
+ @cs.calculate_similarity
255
+ expected = [
256
+ {resource: '9', similarity: 0.667},
257
+ {resource: '6', similarity: 0.667},
258
+ {resource: '12', similarity: 0.5},
259
+ {resource: '3', similarity: 0.4},
260
+ {resource: '21', similarity: 0.286},
261
+ {resource: '15', similarity: 0.286}
262
+ ]
263
+ assert_equal expected, @cs.similar_to(18)
264
+ end
265
+
266
+ def test_recommendations_are_isolated_by_key_base
267
+ cs1 = create_content_set('ContentSetOne')
268
+ cs2 = create_content_set('ContentSetTwo')
269
+ cs1.add('1', 'a group')
270
+ cs2.add('2', 'a group')
271
+ cs1.add('3', 'a group')
272
+ cs2.add('4', 'a group')
273
+ cs1.add('5', 'a group')
274
+ cs2.add('6', 'a group')
275
+ cs1.calculate_similarity
276
+ cs2.calculate_similarity
277
+ assert_equal [{resource: '5', similarity: 1.0}, {resource: '3', similarity: 1.0}], cs1.similar_to('1')
278
+ assert_equal [{resource: '6', similarity: 1.0}, {resource: '4', similarity: 1.0}], cs2.similar_to('2')
279
+ end
280
+
281
+ def test_recommends_when_added_by_group_with_scores
282
+ @cs.add_by_group('group-1', ['resource-1', 2], ['resource-2', 3], ['resource-3', 7])
283
+ @cs.add_by_group('group-2', ['resource-1', 2], ['resource-3', 3], ['resource-4', 5])
284
+ @cs.calculate_similarity
285
+ expected = [
286
+ {resource: 'resource-3', similarity: 1.0},
287
+ {resource: 'resource-4', similarity: 0.778},
288
+ {resource: 'resource-2', similarity: 0.714}
289
+ ]
290
+ assert_equal expected, @cs.similar_to('resource-1')
291
+ end
292
+
293
+ def test_recommends_when_added_by_group
294
+ @cs.add_by_group('group-1', 'resource-1', 'resource-2', 'resource-3')
295
+ @cs.add_by_group('group-2', 'resource-1', 'resource-3', 'resource-4')
296
+ @cs.calculate_similarity
297
+ expected = [
298
+ {resource: 'resource-3', similarity: 1.0},
299
+ {resource: 'resource-4', similarity: 0.667},
300
+ {resource: 'resource-2', similarity: 0.667}
301
+ ]
302
+ assert_equal expected, @cs.similar_to('resource-1')
303
+ end
304
+
305
+ def test_recommends_when_extra_scores_added
306
+ test_recommends_when_added_with_scores #sets up the content set
307
+ @cs.add('resource-3', ['group-1', 1], ['group-3', 2])
308
+ @cs.add('resource-4', ['group-2', 1])
309
+ @cs.add_by_group('group-1', ['newource-9', 100], 'resource-2', 'resource-3')
310
+ @cs.add_by_group('group-2', 'resource-1', 'resource-3', 'resource-4')
311
+ @cs.calculate_similarity
312
+ expected = [
313
+ {resource: 'newource-9', similarity: 1.0},
314
+ {resource: 'resource-1', similarity: 0.769},
315
+ {resource: 'resource-3', similarity: 0.706}
316
+ ]
317
+ actual = @cs.similar_to('resource-2')
318
+ assert_equal expected, actual
319
+ end
320
+
321
+ def test_recommends_with_large_number_of_groups
322
+ (0..3000).each do |i|
323
+ @cs.add('resource-1', ["group-#{i}", i/100.0], ["group-#{i+1}", i/20.0])
324
+ @cs.add('resource-9', ["group-#{i}", i/100.0], ["group-#{i+1}", i/20.0])
325
+ end
326
+ @cs.calculate_similarity
327
+ expected = [
328
+ {resource: 'resource-9', similarity: 1.0}
329
+ ]
330
+ assert_equal expected, @cs.similar_to('resource-1')
331
+ end
332
+
333
+ def test_recommends_when_added_with_scores
334
+ @cs.add('resource-1', ['group-1', 2], ['group-2', 2])
335
+ @cs.add('resource-2', ['group-1', 7])
336
+ @cs.add('resource-3', ['group-1', 2], ['group-2', 2])
337
+ @cs.add('resource-4', ['group-2', 3])
338
+ @cs.calculate_similarity
339
+ expected = [
340
+ {resource: 'resource-3', similarity: 1.0},
341
+ {resource: 'resource-2', similarity: 0.818},
342
+ {resource: 'resource-4', similarity: 0.714}
343
+ ]
344
+ actual = @cs.similar_to('resource-1')
345
+ assert_equal expected, actual
346
+ end
347
+
348
+ def test_recommends_limited_by_number
349
+ @cs.add('resource-1', 'group-1', 'group-2')
350
+ @cs.add('resource-2', 'group-1')
351
+ @cs.add('resource-3', 'group-1', 'group-2')
352
+ @cs.add('resource-4', 'group-2')
353
+ @cs.calculate_similarity
354
+ expected = [
355
+ {resource: 'resource-3', similarity: 1.0},
356
+ {resource: 'resource-4', similarity: 0.667},
357
+ {resource: 'resource-2', similarity: 0.667}
358
+ ]
359
+ assert_equal expected[0..0], @cs.similar_to('resource-1', 1)
360
+ assert_equal expected[0..1], @cs.similar_to('resource-1', 2)
361
+ assert_equal expected, @cs.similar_to('resource-1', 3)
362
+ assert_equal expected, @cs.similar_to('resource-1', 99)
363
+ end
364
+
365
+ def test_recommends_when_added
366
+ @cs.add('resource-1', 'group-1', 'group-2')
367
+ @cs.add('resource-2', 'group-1')
368
+ @cs.add('resource-3', 'group-1', 'group-2')
369
+ @cs.add('resource-4', 'group-2')
370
+ @cs.calculate_similarity
371
+ expected = [
372
+ {resource: 'resource-3', similarity: 1.0},
373
+ {resource: 'resource-4', similarity: 0.667},
374
+ {resource: 'resource-2', similarity: 0.667}
375
+ ]
376
+ assert_equal expected, @cs.similar_to('resource-1')
377
+ end
378
+
379
+ end