likes 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.travis.yml +13 -0
- data/Gemfile +5 -1
- data/README.md +2 -0
- data/lib/likes.rb +2 -0
- data/lib/likes/engines.rb +9 -0
- data/lib/likes/engines/best_intersection_size.rb +151 -0
- data/lib/likes/engines/best_relative_intersection_size.rb +106 -0
- data/lib/likes/engines/fast_jaccard_similarity.rb +159 -0
- data/lib/likes/engines/protocol.rb +34 -0
- data/lib/likes/set.rb +13 -106
- data/lib/likes/support.rb +68 -0
- data/lib/likes/version.rb +1 -1
- data/spec/fixtures.rb +107 -0
- data/spec/likes/engines/best_intersection_size_spec.rb +44 -0
- data/spec/likes/engines/best_relative_intersection_size_spec.rb +44 -0
- data/spec/likes/engines/fast_jaccard_similarity_spec.rb +44 -0
- data/spec/likes/like_spec.rb +14 -14
- data/spec/likes/set_spec.rb +15 -72
- data/spec/spec_helper.rb +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec87d59328fd9af20cf8ac632d146dbfa8d62479
|
4
|
+
data.tar.gz: 26d0b83be096dc1150ac797545abdbf6170de08e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd78634d24a31e845a6f08ac785d2a8698be63e82f6fc6822a2c8011fcb61f526b37582b53e7b3cad7136f49b585e9d41375b150ea0d8bf943abb29214b92f3a
|
7
|
+
data.tar.gz: aaf4af28de77e664cce38e5cd06973c2b6dfafa9a1066e48c5874e605a3184ac0b2f438a1061d81dd5d8e0069476d6b44b24272345ffd4b6b51571b9eea50747
|
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
data/lib/likes.rb
CHANGED
@@ -0,0 +1,151 @@
|
|
1
|
+
module Likes
|
2
|
+
module Engines
|
3
|
+
# Job: Understands which items could be recommended
|
4
|
+
#
|
5
|
+
# Calculates intersection sizes to all other person likings and
|
6
|
+
# chooses maximum
|
7
|
+
#
|
8
|
+
# Worst approximation of execution time:
|
9
|
+
#
|
10
|
+
# Given K = how much likes target person has, in reasonable
|
11
|
+
# situations it is not very big number. But in theory can be as
|
12
|
+
# high as P
|
13
|
+
#
|
14
|
+
# Given N = how much distinct people we have
|
15
|
+
#
|
16
|
+
# Given P = how much distinct items we have
|
17
|
+
#
|
18
|
+
# Complexity: O(NK) * O(hash operations ~ log N + log P)
|
19
|
+
#
|
20
|
+
# @see BestRelativeIntersectionSize
|
21
|
+
class BestIntersectionSize
|
22
|
+
|
23
|
+
# Creates new instance of BestIntersectionSize engine
|
24
|
+
#
|
25
|
+
# @param [Person#==] person The person to provide
|
26
|
+
# recommendations for
|
27
|
+
# @param [Hash<Person, Array<Item>>] likes_of Input data in form
|
28
|
+
# of map person => [item]
|
29
|
+
# @param [Hash<Item, Array<Person>>] liked Input data in form of
|
30
|
+
# map item => [person]
|
31
|
+
#
|
32
|
+
# @param [Factory<Intersections>#new(Person)]
|
33
|
+
# intersections_factory Knows how to find best intersection
|
34
|
+
# candidates
|
35
|
+
def initialize(person, likes_of, liked, intersections_factory=Intersections)
|
36
|
+
@intersections = intersections_factory.build(person)
|
37
|
+
@likes_of = likes_of
|
38
|
+
@liked = liked
|
39
|
+
@its_likes = likes_of.fetch(person)
|
40
|
+
add_similar_tastes
|
41
|
+
end
|
42
|
+
|
43
|
+
# Solves the problem and returns recommendation list
|
44
|
+
#
|
45
|
+
# @return [Array<Item>] Returns list of recommended items
|
46
|
+
def solve
|
47
|
+
solution_candidate(intersections.next_people_with_similar_tastes)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :intersections, :likes_of, :liked, :its_likes
|
53
|
+
|
54
|
+
def solution_candidate(candidates)
|
55
|
+
return [] if candidates.empty?
|
56
|
+
non_empty_solution(_solution_candidate(candidates))
|
57
|
+
end
|
58
|
+
|
59
|
+
def _solution_candidate(candidates)
|
60
|
+
candidates.map { |other_person, _|
|
61
|
+
likes_of.fetch(other_person) - its_likes
|
62
|
+
}.flatten.uniq
|
63
|
+
end
|
64
|
+
|
65
|
+
def non_empty_solution(solution)
|
66
|
+
return solve if solution.empty?
|
67
|
+
solution
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_similar_tastes
|
71
|
+
its_likes.each do |item|
|
72
|
+
intersections.add_similar_tastes(liked.fetch(item))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# @private
|
77
|
+
# Job: Null object for size transformation logic
|
78
|
+
class NullSizeTransform
|
79
|
+
def call(_, size)
|
80
|
+
size
|
81
|
+
end
|
82
|
+
|
83
|
+
alias_method :[], :call
|
84
|
+
end
|
85
|
+
|
86
|
+
# @private
|
87
|
+
# Job: Understands similar tastes
|
88
|
+
class Intersections
|
89
|
+
NO_LIMIT = Object.new.freeze
|
90
|
+
|
91
|
+
def self.build(person)
|
92
|
+
new(person, NullSizeTransform.new)
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize(person, size_transform)
|
96
|
+
@sizes = {}
|
97
|
+
@person = person
|
98
|
+
@best_size = NO_LIMIT
|
99
|
+
@size_transform = size_transform
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_similar_tastes(people)
|
103
|
+
people.each do |other_person|
|
104
|
+
next if person == other_person
|
105
|
+
sizes[other_person] = sizes.fetch(other_person, 0) + 1
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def next_people_with_similar_tastes
|
110
|
+
candidates_with(next_best_size)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
attr_reader :sizes, :person, :best_size, :size_transform
|
116
|
+
|
117
|
+
def candidates_with(intersection_size)
|
118
|
+
return [] if no_limit?(intersection_size)
|
119
|
+
transformed_sizes.select { |_, size|
|
120
|
+
Support::FloatWithError.new(intersection_size) == size
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def next_best_size
|
125
|
+
@best_size = get_best_size(best_size)
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_best_size(limit)
|
129
|
+
transformed_sizes.
|
130
|
+
select { |_, size| in_limit?(size, limit) }.
|
131
|
+
map { |_, size| size }.max || NO_LIMIT
|
132
|
+
end
|
133
|
+
|
134
|
+
def transformed_sizes
|
135
|
+
@_transformed_sizes ||= Hash[sizes.map { |person, size|
|
136
|
+
[person, size_transform[person, size]]
|
137
|
+
}]
|
138
|
+
end
|
139
|
+
|
140
|
+
def in_limit?(size, limit)
|
141
|
+
return true if no_limit?(limit)
|
142
|
+
size < limit
|
143
|
+
end
|
144
|
+
|
145
|
+
def no_limit?(limit)
|
146
|
+
NO_LIMIT == limit
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require "likes/engines/best_intersection_size"
|
2
|
+
|
3
|
+
module Likes
|
4
|
+
module Engines
|
5
|
+
# Job: Understands which items could be recommended
|
6
|
+
#
|
7
|
+
# Calculates relative intersection sizes to all other person
|
8
|
+
# likings and chooses maximum. This is Jaccard Similarity of Sets
|
9
|
+
# algorithm implementation
|
10
|
+
#
|
11
|
+
# Relative intersection size = intersection size / union size
|
12
|
+
#
|
13
|
+
# Worst approximation of execution time:
|
14
|
+
#
|
15
|
+
# Given K = how much likes target person has, in reasonable
|
16
|
+
# situations it is not very big number. But in theory can be as
|
17
|
+
# high as P
|
18
|
+
#
|
19
|
+
# Given P = how much distinct items we have
|
20
|
+
#
|
21
|
+
# Given N = how much distinct people we have
|
22
|
+
#
|
23
|
+
# Complexity: O(NK) * O(hash operations ~ log N + log P)
|
24
|
+
#
|
25
|
+
# @see BestIntersectionSize
|
26
|
+
class BestRelativeIntersectionSize
|
27
|
+
|
28
|
+
# Creates new instance of BestRelativeIntersectionSize engine
|
29
|
+
#
|
30
|
+
# @param [Person#==] person The person to provide
|
31
|
+
# recommendations for
|
32
|
+
# @param [Hash<Person, Array<Item>>] likes_of Input data in form
|
33
|
+
# of map person => [item]
|
34
|
+
# @param [Hash<Item, Array<Person>>] liked Input data in form of
|
35
|
+
# map item => [person]
|
36
|
+
def initialize(person, likes_of, liked)
|
37
|
+
@delegatee = BestIntersectionSize.new(
|
38
|
+
person,
|
39
|
+
likes_of,
|
40
|
+
liked,
|
41
|
+
RelativeIntersectionsFactory.new(likes_of)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Solves the problem and returns recommendation list
|
46
|
+
#
|
47
|
+
# @return [Array<Item>] Returns list of recommended items
|
48
|
+
def solve
|
49
|
+
delegatee.solve
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :delegatee
|
55
|
+
|
56
|
+
# @private
|
57
|
+
# Job: Understands how needs of Intersections with relative
|
58
|
+
# logic
|
59
|
+
class RelativeIntersectionsFactory
|
60
|
+
def initialize(likes_of)
|
61
|
+
@sets_sizes = Hash[likes_of.map { |person, items| [person, items.size] }]
|
62
|
+
end
|
63
|
+
|
64
|
+
def build(person)
|
65
|
+
BestIntersectionSize::Intersections.new(person, size_transform(person))
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
attr_reader :sets_sizes
|
71
|
+
|
72
|
+
def size_transform(person)
|
73
|
+
RelativeSizeTransform.new(person, sets_sizes)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @private
|
78
|
+
# Job: Understands conversion between absolute and relative
|
79
|
+
# intersection sizes
|
80
|
+
class RelativeSizeTransform
|
81
|
+
def initialize(person, sets_sizes)
|
82
|
+
@person = person
|
83
|
+
@sets_sizes = sets_sizes
|
84
|
+
end
|
85
|
+
|
86
|
+
def call(other_person, intersection_size)
|
87
|
+
1.0 * intersection_size / union_size(other_person)
|
88
|
+
end
|
89
|
+
|
90
|
+
alias_method :[], :call
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
attr_reader :person, :sets_sizes
|
95
|
+
|
96
|
+
def union_size(other_person)
|
97
|
+
sets_sizes[other_person] + own_size
|
98
|
+
end
|
99
|
+
|
100
|
+
def own_size
|
101
|
+
@_own_size ||= sets_sizes[person]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Likes
|
2
|
+
module Engines
|
3
|
+
# Job: Understands which items could be recommended
|
4
|
+
#
|
5
|
+
# Calculates Minhash Signatures and uses them to give approximate
|
6
|
+
# relative intersection size. Returns recommendations from best
|
7
|
+
# approximate relative intersection sizes
|
8
|
+
#
|
9
|
+
# Relative intersection size = intersection size / union size
|
10
|
+
#
|
11
|
+
# Worst approximation of execution time:
|
12
|
+
#
|
13
|
+
# Given P = how much distinct items we have
|
14
|
+
#
|
15
|
+
# Given N = how much distinct people we have
|
16
|
+
#
|
17
|
+
# Given D = depth of this implementation - maximum count of hash functions (~100)
|
18
|
+
#
|
19
|
+
# Complexity: O(ND + PD) * O(hash operations ~ log N + log P) ~ O(N * log N)
|
20
|
+
# @ignore has a lot of memoization :reek:TooManyInstanceVariables
|
21
|
+
class FastJaccardSimilarity
|
22
|
+
|
23
|
+
MAX_HASH_FUNCTIONS_COUNT = 200
|
24
|
+
ALLOW_FLUCTUATION = 1.075
|
25
|
+
INFINITY = 1.0 / 0
|
26
|
+
|
27
|
+
# Creates new instance of FastJaccardSimilarity engine
|
28
|
+
#
|
29
|
+
# @param [Person#==] person The person to provide
|
30
|
+
# recommendations for
|
31
|
+
# @param [Hash<Person, Array<Item>>] likes_of Input data in form
|
32
|
+
# of map person => [item]
|
33
|
+
# @param [Hash<Item, Array<Person>>] liked Input data in form of
|
34
|
+
# map item => [person]
|
35
|
+
def initialize(person, likes_of, liked)
|
36
|
+
@person = person
|
37
|
+
@likes_of = likes_of
|
38
|
+
@liked = liked
|
39
|
+
@items = liked.keys
|
40
|
+
@people = likes_of.keys
|
41
|
+
end
|
42
|
+
|
43
|
+
# Solves the problem and returns recommendation list
|
44
|
+
#
|
45
|
+
# @return [Array<Item>] Returns list of recommended items
|
46
|
+
def solve
|
47
|
+
init_signature
|
48
|
+
compute_hashes
|
49
|
+
compute_signature
|
50
|
+
recommendations
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
attr_reader :person, :likes_of, :liked, :items, :people,
|
56
|
+
:signature, :hash_functions, :hashes
|
57
|
+
|
58
|
+
def init_signature
|
59
|
+
@signature = (0...MAX_HASH_FUNCTIONS_COUNT).map {
|
60
|
+
Array.new(people.count, INFINITY)
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def own_column
|
65
|
+
@_own_column ||= people.index(person)
|
66
|
+
end
|
67
|
+
|
68
|
+
def similarities
|
69
|
+
@_similarities ||= people.
|
70
|
+
each_with_index.
|
71
|
+
inject({person => -1}) do |similarities, (other_person, column)|
|
72
|
+
if person != other_person
|
73
|
+
similarities[other_person] = relative_similarity(similarity_for(column), other_person)
|
74
|
+
end
|
75
|
+
similarities
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def similarity_for(column)
|
80
|
+
signature.each_with_index.select { |signature_row, index|
|
81
|
+
signature_row[column] == signature_row[own_column]
|
82
|
+
}.count
|
83
|
+
end
|
84
|
+
|
85
|
+
def relative_similarity(similarity, other_person)
|
86
|
+
1.0 * similarity / union_size(other_person)
|
87
|
+
end
|
88
|
+
|
89
|
+
def union_size(other_person)
|
90
|
+
own_likes_count + likes_of.fetch(other_person).count
|
91
|
+
end
|
92
|
+
|
93
|
+
def own_likes
|
94
|
+
@_own_likes ||= likes_of.fetch(person)
|
95
|
+
end
|
96
|
+
|
97
|
+
def own_likes_count
|
98
|
+
@_own_likes_count ||= own_likes.count
|
99
|
+
end
|
100
|
+
|
101
|
+
def best_similarity
|
102
|
+
similarities.values.max
|
103
|
+
end
|
104
|
+
|
105
|
+
def candidates
|
106
|
+
Hash[similarities.select { |_, similarity|
|
107
|
+
best_similarity <= similarity * ALLOW_FLUCTUATION
|
108
|
+
}].keys
|
109
|
+
end
|
110
|
+
|
111
|
+
def recommendations
|
112
|
+
candidates.map { |other_person, _|
|
113
|
+
likes_of.fetch(other_person) - own_likes
|
114
|
+
}.flatten.uniq
|
115
|
+
end
|
116
|
+
|
117
|
+
# Full complexity: D * O(likeset size * log N) ~ O(N log N) with big constant
|
118
|
+
def compute_signature
|
119
|
+
each_like do |_, _, row, column|
|
120
|
+
signature_step(row, column)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Complexity: O(likeset size) * O(log N) ~ O(N log N)
|
125
|
+
def each_like(&blk)
|
126
|
+
items.each_with_index do |item, row|
|
127
|
+
each_column_of_likes(item, row, &blk)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def each_column_of_likes(item, row, &blk)
|
132
|
+
# only columns with 1 in matrix:
|
133
|
+
liked.fetch(item).each_with_index do |person, column|
|
134
|
+
blk[item, person, row, column]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Complexity: D * O(1)
|
139
|
+
def signature_step(row, column)
|
140
|
+
signature.each_with_index do |signature_row, index|
|
141
|
+
signature_row[column] = [signature_row[column], hashes[index][row]].min
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def compute_hashes
|
146
|
+
@hashes = hash_functions.map { |function|
|
147
|
+
function.map(0...items.count)
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def hash_functions
|
152
|
+
@_hash_functions ||= Support::HashFunction.sample(
|
153
|
+
MAX_HASH_FUNCTIONS_COUNT,
|
154
|
+
items.count
|
155
|
+
)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Likes
|
2
|
+
module Engines
|
3
|
+
# @abstract
|
4
|
+
# Defines protocol which any Engine should support
|
5
|
+
class Protocol
|
6
|
+
|
7
|
+
# @ignore :reek:UnusedParameters
|
8
|
+
# Creates new instance of engine
|
9
|
+
#
|
10
|
+
# @param [Person#==] person The person to provide
|
11
|
+
# recommendations for
|
12
|
+
# @param [Hash<Person, Array<Item>>] likes_of Input data in form
|
13
|
+
# of map person => [item]
|
14
|
+
# @param [Hash<Item, Array<Person>>] liked Input data in form of
|
15
|
+
# map item => [person]
|
16
|
+
def initialize(person, likes_of, liked)
|
17
|
+
abstract
|
18
|
+
end
|
19
|
+
|
20
|
+
# Solves the problem and returns recommendation list
|
21
|
+
#
|
22
|
+
# @return [Array<Item>] Returns list of recommended items
|
23
|
+
def solve
|
24
|
+
abstract
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def abstract
|
30
|
+
raise NotImplementedError.new("Likes::Engines::Protocol is abstract")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/likes/set.rb
CHANGED
@@ -1,11 +1,20 @@
|
|
1
1
|
module Likes
|
2
2
|
# Job: Understands patterns in people likings
|
3
3
|
class Set
|
4
|
+
# Default engine - simplest one
|
5
|
+
#
|
6
|
+
# @see Engines::BestIntersectionSize
|
7
|
+
DEFAULT_ENGINE = Engines::BestIntersectionSize
|
8
|
+
|
4
9
|
# Creates new instance of Set
|
5
10
|
#
|
6
11
|
# @param [Array<Like>] likes List of likes
|
7
|
-
|
12
|
+
# @param [Engines::Protocol] engine Recommendation engine to use
|
13
|
+
#
|
14
|
+
# @see Engines
|
15
|
+
def initialize(likes, engine=DEFAULT_ENGINE)
|
8
16
|
@likes = likes
|
17
|
+
@engine = engine
|
9
18
|
build_likes_of
|
10
19
|
build_liked
|
11
20
|
end
|
@@ -13,29 +22,20 @@ module Likes
|
|
13
22
|
# Provides list of recommendations for person based on this
|
14
23
|
# likeset
|
15
24
|
#
|
16
|
-
# Should handle amount of distinct
|
25
|
+
# Should handle amount of distinct people <= 10**6 and amount of
|
17
26
|
# distinct items <= 10**6, but likeset length is <= 10**7, ie it
|
18
27
|
# is advised to use only recent likes (couple of weeks or month)
|
19
28
|
#
|
20
|
-
# Worst approximation of execution time:
|
21
|
-
#
|
22
|
-
# Given K = how much likes target person has, in reasonable
|
23
|
-
# situations it is not very big number
|
24
|
-
#
|
25
|
-
# Given N = how much distinct persons we have
|
26
|
-
#
|
27
|
-
# Complexity: O(NK)
|
28
|
-
#
|
29
29
|
# @param [Person#==] person The person to provide recommendations
|
30
30
|
# for
|
31
31
|
# @return [Array<Item>] List of recommendations for the person
|
32
32
|
def recommendations_for(person)
|
33
|
-
|
33
|
+
engine.new(person, likes_of, liked).solve
|
34
34
|
end
|
35
35
|
|
36
36
|
private
|
37
37
|
|
38
|
-
attr_accessor :likes, :likes_of, :liked
|
38
|
+
attr_accessor :likes, :engine, :likes_of, :liked
|
39
39
|
|
40
40
|
def build_likes_of
|
41
41
|
@likes_of = {}
|
@@ -50,98 +50,5 @@ module Likes
|
|
50
50
|
like.add_person_to_map(liked)
|
51
51
|
end
|
52
52
|
end
|
53
|
-
|
54
|
-
# @private
|
55
|
-
# Job: Understands which items could be recommended
|
56
|
-
class Recommendations
|
57
|
-
def initialize(person, likes_of, liked)
|
58
|
-
@intersections = Intersections.new(person)
|
59
|
-
@likes_of = likes_of
|
60
|
-
@liked = liked
|
61
|
-
@its_likes = likes_of.fetch(person)
|
62
|
-
add_similar_tastes
|
63
|
-
end
|
64
|
-
|
65
|
-
def solve
|
66
|
-
solution_candidate(intersections.next_people_with_similar_tastes)
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
attr_reader :intersections, :likes_of, :liked, :its_likes
|
72
|
-
|
73
|
-
def solution_candidate(candidates)
|
74
|
-
return [] if candidates.empty?
|
75
|
-
non_empty_solution(_solution_candidate(candidates))
|
76
|
-
end
|
77
|
-
|
78
|
-
def _solution_candidate(candidates)
|
79
|
-
candidates.map { |other_person, _|
|
80
|
-
likes_of.fetch(other_person) - its_likes
|
81
|
-
}.flatten.uniq
|
82
|
-
end
|
83
|
-
|
84
|
-
def non_empty_solution(solution)
|
85
|
-
return solve if solution.empty?
|
86
|
-
solution
|
87
|
-
end
|
88
|
-
|
89
|
-
def add_similar_tastes
|
90
|
-
its_likes.each do |item|
|
91
|
-
intersections.add_similar_tastes(liked.fetch(item))
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# @private
|
97
|
-
# Job: Understands similar tastes
|
98
|
-
class Intersections
|
99
|
-
NO_LIMIT = Object.new.freeze
|
100
|
-
|
101
|
-
def initialize(person)
|
102
|
-
@sizes = {}
|
103
|
-
@person = person
|
104
|
-
@best_size = NO_LIMIT
|
105
|
-
end
|
106
|
-
|
107
|
-
def add_similar_tastes(people)
|
108
|
-
people.each do |other_person|
|
109
|
-
next if person == other_person
|
110
|
-
sizes[other_person] = sizes.fetch(other_person, 0) + 1
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def next_people_with_similar_tastes
|
115
|
-
candidates_with(next_best_size)
|
116
|
-
end
|
117
|
-
|
118
|
-
private
|
119
|
-
|
120
|
-
attr_reader :sizes, :person
|
121
|
-
|
122
|
-
def candidates_with(intersection_size)
|
123
|
-
return [] if no_limit?(intersection_size)
|
124
|
-
sizes.select { |_, size| intersection_size == size }
|
125
|
-
end
|
126
|
-
|
127
|
-
def next_best_size
|
128
|
-
@best_size = get_best_size(@best_size)
|
129
|
-
end
|
130
|
-
|
131
|
-
def get_best_size(limit)
|
132
|
-
sizes
|
133
|
-
.select { |_, size| in_limit?(size, limit) }
|
134
|
-
.map { |_, size| size }.max || NO_LIMIT
|
135
|
-
end
|
136
|
-
|
137
|
-
def in_limit?(size, limit)
|
138
|
-
return true if no_limit?(limit)
|
139
|
-
size < limit
|
140
|
-
end
|
141
|
-
|
142
|
-
def no_limit?(limit)
|
143
|
-
NO_LIMIT == limit
|
144
|
-
end
|
145
|
-
end
|
146
53
|
end
|
147
54
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
PrimeFactory = if RUBY_VERSION.to_f > 1.8
|
2
|
+
require "prime"
|
3
|
+
Prime
|
4
|
+
else
|
5
|
+
require "mathn"
|
6
|
+
Prime.new
|
7
|
+
end
|
8
|
+
|
9
|
+
module Likes
|
10
|
+
module Support
|
11
|
+
# @private
|
12
|
+
# Job: Understands float comparision in computer world
|
13
|
+
class FloatWithError
|
14
|
+
ALLOWED_ERROR = 1e-9
|
15
|
+
|
16
|
+
def self.lift(value)
|
17
|
+
return value if FloatWithError === value
|
18
|
+
FloatWithError.new(value)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(value)
|
22
|
+
@value = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
(self.value - FloatWithError.lift(other).value).abs < ALLOWED_ERROR
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
attr_accessor :value
|
32
|
+
end
|
33
|
+
|
34
|
+
# @private
|
35
|
+
# Job: Understands random hash function generation
|
36
|
+
class HashFunction
|
37
|
+
PRIMES = PrimeFactory.first(3200)[-250..-1]
|
38
|
+
|
39
|
+
def self.sample(count, modulo)
|
40
|
+
(0...count).map { new(*(sample_primes(2) + [modulo])) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(factor, constant, modulo)
|
44
|
+
@factor, @constant, @modulo = factor, constant, modulo
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(value)
|
48
|
+
(factor * value + constant) % modulo
|
49
|
+
end
|
50
|
+
|
51
|
+
def map(values)
|
52
|
+
values.map { |value| call(value) }
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
attr_accessor :factor, :constant, :modulo
|
58
|
+
|
59
|
+
def self.sample_primes(count)
|
60
|
+
if RUBY_VERSION.to_f > 1.8
|
61
|
+
PRIMES.sample(count)
|
62
|
+
else
|
63
|
+
(0...count).map { PRIMES.choice }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/likes/version.rb
CHANGED
data/spec/fixtures.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# @ignore :reek:UtilityFunction
|
2
|
+
module Fixtures
|
3
|
+
class << self
|
4
|
+
def small_likeset
|
5
|
+
[
|
6
|
+
Likes::Like.new(:person => 1, :item => 5),
|
7
|
+
|
8
|
+
Likes::Like.new(:person => 3, :item => 5),
|
9
|
+
Likes::Like.new(:person => 3, :item => 4),
|
10
|
+
]
|
11
|
+
end
|
12
|
+
|
13
|
+
def small_likeset_likes_of
|
14
|
+
{
|
15
|
+
1 => [5],
|
16
|
+
3 => [5, 4],
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def small_likeset_liked
|
21
|
+
{
|
22
|
+
5 => [1, 3],
|
23
|
+
4 => [3],
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def regular_likeset
|
28
|
+
[
|
29
|
+
Likes::Like.new(:person => 1, :item => 1),
|
30
|
+
Likes::Like.new(:person => 1, :item => 5),
|
31
|
+
Likes::Like.new(:person => 1, :item => 4),
|
32
|
+
Likes::Like.new(:person => 1, :item => 7),
|
33
|
+
Likes::Like.new(:person => 1, :item => 19),
|
34
|
+
|
35
|
+
Likes::Like.new(:person => 2, :item => 1),
|
36
|
+
Likes::Like.new(:person => 2, :item => 3),
|
37
|
+
Likes::Like.new(:person => 2, :item => 5),
|
38
|
+
Likes::Like.new(:person => 2, :item => 4),
|
39
|
+
|
40
|
+
Likes::Like.new(:person => 3, :item => 3),
|
41
|
+
Likes::Like.new(:person => 3, :item => 2),
|
42
|
+
Likes::Like.new(:person => 3, :item => 5),
|
43
|
+
Likes::Like.new(:person => 3, :item => 4),
|
44
|
+
Likes::Like.new(:person => 3, :item => 9),
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
def best_intersection_candidate_has_no_other_likings
|
49
|
+
[
|
50
|
+
Likes::Like.new(:person => 1, :item => 1),
|
51
|
+
Likes::Like.new(:person => 1, :item => 5),
|
52
|
+
Likes::Like.new(:person => 1, :item => 4),
|
53
|
+
Likes::Like.new(:person => 1, :item => 7),
|
54
|
+
|
55
|
+
Likes::Like.new(:person => 2, :item => 1),
|
56
|
+
Likes::Like.new(:person => 2, :item => 3),
|
57
|
+
Likes::Like.new(:person => 2, :item => 5),
|
58
|
+
Likes::Like.new(:person => 2, :item => 4),
|
59
|
+
|
60
|
+
Likes::Like.new(:person => 4, :item => 1),
|
61
|
+
Likes::Like.new(:person => 4, :item => 5),
|
62
|
+
Likes::Like.new(:person => 4, :item => 4),
|
63
|
+
Likes::Like.new(:person => 4, :item => 7),
|
64
|
+
|
65
|
+
Likes::Like.new(:person => 3, :item => 3),
|
66
|
+
Likes::Like.new(:person => 3, :item => 2),
|
67
|
+
Likes::Like.new(:person => 3, :item => 5),
|
68
|
+
Likes::Like.new(:person => 3, :item => 4),
|
69
|
+
Likes::Like.new(:person => 3, :item => 9),
|
70
|
+
]
|
71
|
+
end
|
72
|
+
|
73
|
+
def there_is_nothing_to_recommend
|
74
|
+
[
|
75
|
+
Likes::Like.new(:person => 1, :item => 1),
|
76
|
+
Likes::Like.new(:person => 1, :item => 5),
|
77
|
+
Likes::Like.new(:person => 1, :item => 4),
|
78
|
+
Likes::Like.new(:person => 1, :item => 7),
|
79
|
+
|
80
|
+
Likes::Like.new(:person => 4, :item => 1),
|
81
|
+
Likes::Like.new(:person => 4, :item => 5),
|
82
|
+
Likes::Like.new(:person => 4, :item => 7),
|
83
|
+
]
|
84
|
+
end
|
85
|
+
|
86
|
+
def somebody_likes_literally_everything
|
87
|
+
[
|
88
|
+
Likes::Like.new(:person => 1, :item => 1),
|
89
|
+
Likes::Like.new(:person => 1, :item => 5),
|
90
|
+
|
91
|
+
Likes::Like.new(:person => 2, :item => 1),
|
92
|
+
Likes::Like.new(:person => 2, :item => 3),
|
93
|
+
Likes::Like.new(:person => 2, :item => 5),
|
94
|
+
Likes::Like.new(:person => 2, :item => 4),
|
95
|
+
Likes::Like.new(:person => 2, :item => 17),
|
96
|
+
Likes::Like.new(:person => 2, :item => 6),
|
97
|
+
Likes::Like.new(:person => 2, :item => 19),
|
98
|
+
Likes::Like.new(:person => 2, :item => 7),
|
99
|
+
Likes::Like.new(:person => 2, :item => 9),
|
100
|
+
|
101
|
+
Likes::Like.new(:person => 3, :item => 5),
|
102
|
+
Likes::Like.new(:person => 3, :item => 4),
|
103
|
+
Likes::Like.new(:person => 3, :item => 9),
|
104
|
+
]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Likes
|
2
|
+
RSpec.describe "Set with Likes::Engines::BestIntersectionSize" do
|
3
|
+
let(:engine) { Engines::BestIntersectionSize }
|
4
|
+
|
5
|
+
let(:raw_likeset) { Fixtures.regular_likeset }
|
6
|
+
let(:likeset) { Set.new(raw_likeset, engine) }
|
7
|
+
|
8
|
+
describe "#recommendations_for" do
|
9
|
+
it "returns a list of items" do
|
10
|
+
expect(likeset.recommendations_for(1)).to be_a(Array)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns right recommendations" do
|
14
|
+
expect(likeset.recommendations_for(1).sort).to eq([3])
|
15
|
+
expect(likeset.recommendations_for(2).sort).to eq([2, 7, 9, 19])
|
16
|
+
expect(likeset.recommendations_for(3).sort).to eq([1])
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when somebody literally likes everything" do
|
20
|
+
let(:raw_likeset) { Fixtures.somebody_likes_literally_everything }
|
21
|
+
|
22
|
+
it "chooses this person as a candidate for recommendations" do
|
23
|
+
expect(likeset.recommendations_for(1).sort).to eq([3, 4, 6, 7, 9, 17, 19])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when there is an identic match with other person" do
|
28
|
+
let(:raw_likeset) { Fixtures.best_intersection_candidate_has_no_other_likings }
|
29
|
+
|
30
|
+
it "ignores identical candidates" do
|
31
|
+
expect(likeset.recommendations_for(1).sort).to eq([3])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when there is nothing to recommend" do
|
36
|
+
let(:raw_likeset) { Fixtures.there_is_nothing_to_recommend }
|
37
|
+
|
38
|
+
it "returns an empty solution" do
|
39
|
+
expect(likeset.recommendations_for(1).sort).to eq([])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Likes
|
2
|
+
RSpec.describe "Set with Likes::Engines::BestRelativeIntersectionSize" do
|
3
|
+
let(:engine) { Engines::BestRelativeIntersectionSize }
|
4
|
+
|
5
|
+
let(:raw_likeset) { Fixtures.regular_likeset }
|
6
|
+
let(:likeset) { Set.new(raw_likeset, engine) }
|
7
|
+
|
8
|
+
describe "#recommendations_for" do
|
9
|
+
it "returns a list of items" do
|
10
|
+
expect(likeset.recommendations_for(1)).to be_a(Array)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns right recommendations" do
|
14
|
+
expect(likeset.recommendations_for(1).sort).to eq([3])
|
15
|
+
expect(likeset.recommendations_for(2).sort).to eq([2, 7, 9, 19])
|
16
|
+
expect(likeset.recommendations_for(3).sort).to eq([1])
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when somebody literally likes everything" do
|
20
|
+
let(:raw_likeset) { Fixtures.somebody_likes_literally_everything }
|
21
|
+
|
22
|
+
it "doesn't choose this person as a candidate for recommendations" do
|
23
|
+
expect(likeset.recommendations_for(1).sort).to eq([4, 9])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when there is an identic match with other person" do
|
28
|
+
let(:raw_likeset) { Fixtures.best_intersection_candidate_has_no_other_likings }
|
29
|
+
|
30
|
+
it "ignores identical candidates" do
|
31
|
+
expect(likeset.recommendations_for(1).sort).to eq([3])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when there is nothing to recommend" do
|
36
|
+
let(:raw_likeset) { Fixtures.there_is_nothing_to_recommend }
|
37
|
+
|
38
|
+
it "returns an empty solution" do
|
39
|
+
expect(likeset.recommendations_for(1).sort).to eq([])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Likes
|
2
|
+
RSpec.describe "Set with Likes::Engines::FastJaccardSimilarity" do
|
3
|
+
let(:engine) { Engines::FastJaccardSimilarity }
|
4
|
+
|
5
|
+
let(:raw_likeset) { Fixtures.regular_likeset }
|
6
|
+
let(:likeset) { Set.new(raw_likeset, engine) }
|
7
|
+
|
8
|
+
describe "#recommendations_for" do
|
9
|
+
it "returns a list of items" do
|
10
|
+
expect(likeset.recommendations_for(1)).to be_a(Array)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns right recommendations" do
|
14
|
+
expect(likeset.recommendations_for(1).sort).to eq([3])
|
15
|
+
expect(likeset.recommendations_for(2).sort).to eq([2, 7, 9, 19]).or eq([7, 19]).or eq([2, 9])
|
16
|
+
expect(likeset.recommendations_for(3).sort).to eq([1])
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when somebody literally likes everything" do
|
20
|
+
let(:raw_likeset) { Fixtures.somebody_likes_literally_everything }
|
21
|
+
|
22
|
+
it "possibly can choose this person as a candidate for recommendations" do
|
23
|
+
expect(likeset.recommendations_for(1).sort).to eq([4, 9]).or eq([3, 4, 6, 7, 9, 17, 19])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when there is an identic match with other person" do
|
28
|
+
let(:raw_likeset) { Fixtures.best_intersection_candidate_has_no_other_likings }
|
29
|
+
|
30
|
+
it "ignores identical candidates" do
|
31
|
+
expect(likeset.recommendations_for(1).sort).to eq([3])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when there is nothing to recommend" do
|
36
|
+
let(:raw_likeset) { Fixtures.there_is_nothing_to_recommend }
|
37
|
+
|
38
|
+
it "returns an empty solution" do
|
39
|
+
expect(likeset.recommendations_for(1).sort).to eq([])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/spec/likes/like_spec.rb
CHANGED
@@ -2,44 +2,44 @@ module Likes
|
|
2
2
|
RSpec.describe Like do
|
3
3
|
describe "creation" do
|
4
4
|
it "can be created with person and item" do
|
5
|
-
expect(Like.new(person
|
5
|
+
expect(Like.new(:person => 1, :item => 2)).to be_a(Like)
|
6
6
|
end
|
7
7
|
|
8
8
|
it "cannot be created without person and/or item" do
|
9
|
-
expect { Like.new(person
|
10
|
-
expect { Like.new(item
|
9
|
+
expect { Like.new(:person => 1) }.to raise_error(ArgumentError, /item is required/)
|
10
|
+
expect { Like.new(:item => 2) }.to raise_error(ArgumentError, /person is required/)
|
11
11
|
expect { Like.new }.to raise_error(ArgumentError, /is required/)
|
12
12
|
end
|
13
13
|
|
14
14
|
it "ignores excessive attributes" do
|
15
|
-
expect(Like.new(person
|
15
|
+
expect(Like.new(:person => 1, :item => 3, :hello => "world")).to eq(Like.new(:person => 1, :item => 3))
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
19
|
describe "encapsulation" do
|
20
20
|
it "does not allow to access its data" do
|
21
|
-
expect { Like.new(person
|
22
|
-
expect { Like.new(person
|
21
|
+
expect { Like.new(:person => 1, :item => 2).person }.to raise_error(NoMethodError)
|
22
|
+
expect { Like.new(:person => 1, :item => 2).person = 3 }.to raise_error(NoMethodError)
|
23
23
|
|
24
|
-
expect { Like.new(person
|
25
|
-
expect { Like.new(person
|
24
|
+
expect { Like.new(:person => 1, :item => 2).item }.to raise_error(NoMethodError)
|
25
|
+
expect { Like.new(:person => 1, :item => 2).item = 3 }.to raise_error(NoMethodError)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
29
|
describe "equality" do
|
30
30
|
it "is equal to another like with same person and item" do
|
31
|
-
expect(Like.new(person
|
31
|
+
expect(Like.new(:person => 1, :item => 2)).to eq(Like.new(:person => 1, :item => 2))
|
32
32
|
end
|
33
33
|
|
34
34
|
it "is not equal to another like with different person and/or item" do
|
35
|
-
expect(Like.new(person
|
36
|
-
expect(Like.new(person
|
37
|
-
expect(Like.new(person
|
35
|
+
expect(Like.new(:person => 1, :item => 2)).not_to eq(Like.new(:person => 3, :item => 2))
|
36
|
+
expect(Like.new(:person => 1, :item => 2)).not_to eq(Like.new(:person => 1, :item => 5))
|
37
|
+
expect(Like.new(:person => 1, :item => 2)).not_to eq(Like.new(:person => 2, :item => 4))
|
38
38
|
end
|
39
39
|
|
40
40
|
it "is not equal to different objects" do
|
41
|
-
expect(Like.new(person
|
42
|
-
expect(Like.new(person
|
41
|
+
expect(Like.new(:person => 1, :item => 2)).not_to eq(nil)
|
42
|
+
expect(Like.new(:person => 1, :item => 2)).not_to eq(Object.new)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
data/spec/likes/set_spec.rb
CHANGED
@@ -1,84 +1,27 @@
|
|
1
1
|
module Likes
|
2
2
|
RSpec.describe Set do
|
3
|
-
let(:
|
4
|
-
|
5
|
-
Likes::Like.new(person: 1, item: 1),
|
6
|
-
Likes::Like.new(person: 1, item: 5),
|
7
|
-
Likes::Like.new(person: 1, item: 4),
|
8
|
-
Likes::Like.new(person: 1, item: 7),
|
3
|
+
let(:engine) { class_double(Engines::Protocol) }
|
4
|
+
let(:engine_instance) { instance_double(Engines::Protocol) }
|
9
5
|
|
10
|
-
|
11
|
-
|
12
|
-
Likes::Like.new(person: 2, item: 5),
|
13
|
-
Likes::Like.new(person: 2, item: 4),
|
6
|
+
let(:raw_likeset) { Fixtures.small_likeset }
|
7
|
+
let(:likeset) { Set.new(raw_likeset, engine) }
|
14
8
|
|
15
|
-
|
16
|
-
|
17
|
-
Likes::Like.new(person: 3, item: 5),
|
18
|
-
Likes::Like.new(person: 3, item: 4),
|
19
|
-
Likes::Like.new(person: 3, item: 9),
|
20
|
-
])
|
21
|
-
}
|
9
|
+
let(:proper_likes_of) { Fixtures.small_likeset_likes_of }
|
10
|
+
let(:proper_liked) { Fixtures.small_likeset_liked }
|
22
11
|
|
23
12
|
describe "#recommendations_for" do
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
|
28
|
-
it "returns right recommendations" do
|
29
|
-
expect(likeset.recommendations_for(1).sort).to eq([3])
|
30
|
-
expect(likeset.recommendations_for(2).sort).to eq([2, 7, 9])
|
31
|
-
expect(likeset.recommendations_for(3).sort).to eq([1])
|
32
|
-
end
|
33
|
-
|
34
|
-
context "when there is an identic match with other person" do
|
35
|
-
let(:likeset) {
|
36
|
-
Likes::Set.new([
|
37
|
-
Likes::Like.new(person: 1, item: 1),
|
38
|
-
Likes::Like.new(person: 1, item: 5),
|
39
|
-
Likes::Like.new(person: 1, item: 4),
|
40
|
-
Likes::Like.new(person: 1, item: 7),
|
41
|
-
|
42
|
-
Likes::Like.new(person: 2, item: 1),
|
43
|
-
Likes::Like.new(person: 2, item: 3),
|
44
|
-
Likes::Like.new(person: 2, item: 5),
|
45
|
-
Likes::Like.new(person: 2, item: 4),
|
46
|
-
|
47
|
-
Likes::Like.new(person: 4, item: 1),
|
48
|
-
Likes::Like.new(person: 4, item: 5),
|
49
|
-
Likes::Like.new(person: 4, item: 4),
|
50
|
-
Likes::Like.new(person: 4, item: 7),
|
51
|
-
|
52
|
-
Likes::Like.new(person: 3, item: 3),
|
53
|
-
Likes::Like.new(person: 3, item: 2),
|
54
|
-
Likes::Like.new(person: 3, item: 5),
|
55
|
-
Likes::Like.new(person: 3, item: 4),
|
56
|
-
Likes::Like.new(person: 3, item: 9),
|
57
|
-
])
|
58
|
-
}
|
59
|
-
|
60
|
-
it "ignores identical candidates" do
|
61
|
-
expect(likeset.recommendations_for(1).sort).to eq([3])
|
62
|
-
end
|
63
|
-
end
|
13
|
+
let(:person) { double("Person") }
|
14
|
+
let(:solution) { double("Solution") }
|
64
15
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
Likes::Like.new(person: 1, item: 4),
|
71
|
-
Likes::Like.new(person: 1, item: 7),
|
16
|
+
it "builds proper likes_of and liked and delegates solution to engine instance" do
|
17
|
+
expect(engine).
|
18
|
+
to receive(:new).
|
19
|
+
with(person, proper_likes_of, proper_liked).
|
20
|
+
and_return(engine_instance)
|
72
21
|
|
73
|
-
|
74
|
-
Likes::Like.new(person: 4, item: 5),
|
75
|
-
Likes::Like.new(person: 4, item: 7),
|
76
|
-
])
|
77
|
-
}
|
22
|
+
expect(engine_instance).to receive(:solve).and_return(solution)
|
78
23
|
|
79
|
-
|
80
|
-
expect(likeset.recommendations_for(1).sort).to eq([])
|
81
|
-
end
|
24
|
+
expect(likeset.recommendations_for(person)).to eq(solution)
|
82
25
|
end
|
83
26
|
end
|
84
27
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: likes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexey Fedorov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-02-
|
11
|
+
date: 2015-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -48,15 +48,26 @@ extra_rdoc_files: []
|
|
48
48
|
files:
|
49
49
|
- ".gitignore"
|
50
50
|
- ".rspec"
|
51
|
+
- ".travis.yml"
|
51
52
|
- Gemfile
|
52
53
|
- LICENSE.txt
|
53
54
|
- README.md
|
54
55
|
- Rakefile
|
55
56
|
- lib/likes.rb
|
57
|
+
- lib/likes/engines.rb
|
58
|
+
- lib/likes/engines/best_intersection_size.rb
|
59
|
+
- lib/likes/engines/best_relative_intersection_size.rb
|
60
|
+
- lib/likes/engines/fast_jaccard_similarity.rb
|
61
|
+
- lib/likes/engines/protocol.rb
|
56
62
|
- lib/likes/like.rb
|
57
63
|
- lib/likes/set.rb
|
64
|
+
- lib/likes/support.rb
|
58
65
|
- lib/likes/version.rb
|
59
66
|
- likes.gemspec
|
67
|
+
- spec/fixtures.rb
|
68
|
+
- spec/likes/engines/best_intersection_size_spec.rb
|
69
|
+
- spec/likes/engines/best_relative_intersection_size_spec.rb
|
70
|
+
- spec/likes/engines/fast_jaccard_similarity_spec.rb
|
60
71
|
- spec/likes/like_spec.rb
|
61
72
|
- spec/likes/set_spec.rb
|
62
73
|
- spec/spec_helper.rb
|
@@ -86,6 +97,10 @@ specification_version: 4
|
|
86
97
|
summary: Give it a list of people and their likings and it will tell what else could
|
87
98
|
these people like.
|
88
99
|
test_files:
|
100
|
+
- spec/fixtures.rb
|
101
|
+
- spec/likes/engines/best_intersection_size_spec.rb
|
102
|
+
- spec/likes/engines/best_relative_intersection_size_spec.rb
|
103
|
+
- spec/likes/engines/fast_jaccard_similarity_spec.rb
|
89
104
|
- spec/likes/like_spec.rb
|
90
105
|
- spec/likes/set_spec.rb
|
91
106
|
- spec/spec_helper.rb
|