commendo 1.2.4 → 2.0.0

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.
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