lunar 0.5.5 → 0.6.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.
- data/VERSION +1 -1
- data/lib/lunar.rb +12 -5
- data/lib/lunar/fuzzy_matches.rb +3 -1
- data/lib/lunar/index.rb +93 -40
- data/lib/lunar/keyword_matches.rb +4 -2
- data/lib/lunar/number_matches.rb +33 -0
- data/lib/lunar/scoring.rb +3 -3
- data/lib/lunar/stopwords.rb +3 -3
- data/lib/lunar/words.rb +30 -5
- data/lunar.gemspec +9 -7
- data/test/test_index.rb +64 -9
- data/test/test_lunar_words.rb +14 -0
- data/test/test_number_search.rb +69 -0
- data/test/test_scoring.rb +1 -1
- metadata +16 -8
- data/lib/lunar/lunar_nest.rb +0 -19
- data/test/test_lunar_nest.rb +0 -46
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.0
|
data/lib/lunar.rb
CHANGED
@@ -4,15 +4,15 @@ require 'nest'
|
|
4
4
|
require 'text'
|
5
5
|
|
6
6
|
module Lunar
|
7
|
-
VERSION = '0.
|
7
|
+
VERSION = '0.6.0'
|
8
8
|
|
9
9
|
autoload :Connection, "lunar/connection"
|
10
|
-
autoload :LunarNest, "lunar/lunar_nest"
|
11
10
|
autoload :Index, "lunar/index"
|
12
11
|
autoload :Scoring, "lunar/scoring"
|
13
12
|
autoload :Words, "lunar/words"
|
14
13
|
autoload :Stopwords, "lunar/stopwords"
|
15
14
|
autoload :FuzzyWord, "lunar/fuzzy_word"
|
15
|
+
autoload :NumberMatches, "lunar/number_matches"
|
16
16
|
autoload :KeywordMatches, "lunar/keyword_matches"
|
17
17
|
autoload :RangeMatches, "lunar/range_matches"
|
18
18
|
autoload :FuzzyMatches, "lunar/fuzzy_matches"
|
@@ -36,7 +36,7 @@ module Lunar
|
|
36
36
|
# end
|
37
37
|
#
|
38
38
|
# @see Lunar::Index#initialize
|
39
|
-
# @param [String, Symbol, Class] namespace the namespace
|
39
|
+
# @param [String, Symbol, Class] namespace the namespace of this document.
|
40
40
|
# @yield [Lunar::Index] an instance of Lunar::Index.
|
41
41
|
# @return [Lunar::Index] returns the yielded Lunar::Index.
|
42
42
|
def self.index(namespace)
|
@@ -95,12 +95,12 @@ module Lunar
|
|
95
95
|
|
96
96
|
# @private abstraction of how encoding should be done for Lunar.
|
97
97
|
def self.encode(word)
|
98
|
-
Base64.encode64(word).
|
98
|
+
Base64.encode64(word).gsub("\n", "")
|
99
99
|
end
|
100
100
|
|
101
101
|
# @private convenience method for getting a scoped Nest.
|
102
102
|
def self.nest
|
103
|
-
|
103
|
+
Nest.new(:Lunar, redis)
|
104
104
|
end
|
105
105
|
|
106
106
|
private
|
@@ -115,6 +115,13 @@ private
|
|
115
115
|
end
|
116
116
|
}
|
117
117
|
sets.push(*fuzzy_matches.compact)
|
118
|
+
elsif key == :numbers
|
119
|
+
number_matches = value.map { |num_key, num_value|
|
120
|
+
unless num_value.to_s.empty?
|
121
|
+
matches = NumberMatches.new(nest[namespace], num_key, num_value)
|
122
|
+
sets << matches.distkey if matches.distkey
|
123
|
+
end
|
124
|
+
}
|
118
125
|
else
|
119
126
|
unless value.to_s.empty?
|
120
127
|
sets << KeywordMatches.new(nest[namespace], key, value).distkey
|
data/lib/lunar/fuzzy_matches.rb
CHANGED
data/lib/lunar/index.rb
CHANGED
@@ -6,7 +6,27 @@ module Lunar
|
|
6
6
|
# @see Lunar::index
|
7
7
|
# @see Lunar::delete
|
8
8
|
class Index
|
9
|
+
# This constant is in place to maintain a certain level of performance.
|
10
|
+
# Fuzzy searching internally stores an index per letter i.e. for Quentin
|
11
|
+
# q
|
12
|
+
# qu
|
13
|
+
# que
|
14
|
+
# quen
|
15
|
+
# quent
|
16
|
+
# quenti
|
17
|
+
# quentin
|
18
|
+
#
|
19
|
+
# This can become pretty unweildy very fast, so we limit the length
|
20
|
+
# of all fuzzy fields.
|
9
21
|
FUZZY_MAX_LENGTH = 100
|
22
|
+
|
23
|
+
# The following are all used to construct redis keys
|
24
|
+
TEXT = :Text
|
25
|
+
METAPHONES = :Metaphones
|
26
|
+
NUMBERS = :Numbers
|
27
|
+
SORTABLES = :Sortables
|
28
|
+
FUZZIES = :Fuzzies
|
29
|
+
FIELDS = :Fields
|
10
30
|
|
11
31
|
MissingID = Class.new(StandardError)
|
12
32
|
FuzzyFieldTooLong = Class.new(StandardError)
|
@@ -16,6 +36,7 @@ module Lunar
|
|
16
36
|
attr :numbers
|
17
37
|
attr :sortables
|
18
38
|
attr :fuzzies
|
39
|
+
attr :fields
|
19
40
|
|
20
41
|
# This is actually wrapped by `Lunar.index` and is not inteded to be
|
21
42
|
# used directly.
|
@@ -24,10 +45,11 @@ module Lunar
|
|
24
45
|
# @return [Lunar::Index]
|
25
46
|
def initialize(namespace)
|
26
47
|
@nest = Lunar.nest[namespace]
|
27
|
-
@metaphones = @nest[
|
28
|
-
@numbers = @nest[
|
29
|
-
@sortables = @nest[
|
30
|
-
@fuzzies = @nest[
|
48
|
+
@metaphones = @nest[METAPHONES]
|
49
|
+
@numbers = @nest[NUMBERS]
|
50
|
+
@sortables = @nest[SORTABLES]
|
51
|
+
@fuzzies = @nest[FUZZIES]
|
52
|
+
@fields = @nest[FIELDS]
|
31
53
|
end
|
32
54
|
|
33
55
|
# Get / Set the id of the document
|
@@ -81,23 +103,16 @@ module Lunar
|
|
81
103
|
#
|
82
104
|
# @return [Array<String>] all the metaphones added for the document.
|
83
105
|
def text(att, value)
|
84
|
-
|
85
|
-
new = []
|
106
|
+
clear_text_field(att)
|
86
107
|
|
87
108
|
Scoring.new(value).scores.each do |word, score|
|
88
109
|
metaphone = Lunar.metaphone(word)
|
89
110
|
|
90
111
|
nest[att][metaphone].zadd(score, id)
|
91
112
|
metaphones[id][att].sadd(metaphone)
|
92
|
-
new << metaphone
|
93
113
|
end
|
94
114
|
|
95
|
-
(
|
96
|
-
nest[att][metaphone].zrem(id)
|
97
|
-
metaphones[id][att].srem(metaphone)
|
98
|
-
end
|
99
|
-
|
100
|
-
return new
|
115
|
+
fields[TEXT].sadd(att)
|
101
116
|
end
|
102
117
|
|
103
118
|
# Adds a numeric index for `att` with `value`.
|
@@ -117,8 +132,20 @@ module Lunar
|
|
117
132
|
# @param [Numeric] value the numeric value of `att`.
|
118
133
|
#
|
119
134
|
# @return [Boolean] whether or not the value was added
|
120
|
-
def number(att, value)
|
135
|
+
def number(att, value, purge = true)
|
136
|
+
if value.kind_of?(Enumerable)
|
137
|
+
clear_number_field(att)
|
138
|
+
|
139
|
+
value.each { |v| number(att, v, false) } and return
|
140
|
+
end
|
141
|
+
|
142
|
+
clear_number_field(att) if purge
|
143
|
+
|
121
144
|
numbers[att].zadd(value, id)
|
145
|
+
numbers[att][value].zadd(1, id)
|
146
|
+
numbers[id][att].sadd(value)
|
147
|
+
|
148
|
+
fields[NUMBERS].sadd att
|
122
149
|
end
|
123
150
|
|
124
151
|
# Adds a sortable index for `att` with `value`.
|
@@ -156,6 +183,8 @@ module Lunar
|
|
156
183
|
# @return [String] the response from the redis server.
|
157
184
|
def sortable(att, value)
|
158
185
|
sortables[id][att].set(value)
|
186
|
+
|
187
|
+
fields[SORTABLES].sadd att
|
159
188
|
end
|
160
189
|
|
161
190
|
# Deletes everything related to an existing document given its `id`.
|
@@ -168,63 +197,80 @@ module Lunar
|
|
168
197
|
delete_numbers
|
169
198
|
delete_sortables
|
170
199
|
delete_fuzzies
|
200
|
+
|
201
|
+
delete_field_meta
|
171
202
|
end
|
172
203
|
|
173
204
|
def fuzzy(att, value)
|
174
|
-
|
175
|
-
raise FuzzyFieldTooLong,
|
176
|
-
"#{att} has a value #{value} exceeding the max #{FUZZY_MAX_LENGTH}"
|
177
|
-
end
|
205
|
+
assert_valid_fuzzy(value, att)
|
178
206
|
|
179
|
-
|
207
|
+
clear_fuzzy_field(att)
|
208
|
+
|
209
|
+
words = Words.new(value, [:downcase, :uniq])
|
180
210
|
|
181
211
|
fuzzy_words_and_parts(words) do |word, parts|
|
182
|
-
parts.each
|
183
|
-
fuzzies[att][encoded].zadd(1, id)
|
184
|
-
end
|
212
|
+
parts.each { |part, encoded| fuzzies[att][encoded].zadd(1, id) }
|
185
213
|
fuzzies[id][att].sadd word
|
186
214
|
end
|
187
215
|
|
188
|
-
|
216
|
+
fields[FUZZIES].sadd att
|
189
217
|
end
|
190
218
|
|
191
219
|
private
|
220
|
+
def assert_valid_fuzzy(value, att)
|
221
|
+
if value.to_s.length > FUZZY_MAX_LENGTH
|
222
|
+
raise FuzzyFieldTooLong,
|
223
|
+
"#{att} has a value #{value} exceeding the max #{FUZZY_MAX_LENGTH}"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
192
227
|
def delete_metaphones
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
228
|
+
fields[TEXT].smembers.each do |att|
|
229
|
+
clear_text_field(att)
|
230
|
+
end
|
231
|
+
end
|
197
232
|
|
198
|
-
|
233
|
+
def clear_text_field(att)
|
234
|
+
metaphones[id][att].smembers.each do |metaphone|
|
235
|
+
nest[att][metaphone].zrem id
|
199
236
|
end
|
237
|
+
|
238
|
+
metaphones[id][att].del
|
200
239
|
end
|
201
240
|
|
202
241
|
def delete_numbers
|
203
|
-
|
242
|
+
fields[NUMBERS].smembers.each do |att|
|
243
|
+
clear_number_field(att)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def clear_number_field(att)
|
248
|
+
numbers[id][att].smembers.each do |number|
|
249
|
+
numbers[att][number].zrem(id)
|
204
250
|
numbers[att].zrem(id)
|
205
251
|
end
|
252
|
+
|
253
|
+
numbers[id][att].del
|
206
254
|
end
|
207
255
|
|
256
|
+
|
208
257
|
def delete_sortables
|
209
|
-
|
210
|
-
Lunar.redis.del key
|
211
|
-
end
|
258
|
+
fields[SORTABLES].smembers.each { |att| sortables[id][att].del }
|
212
259
|
end
|
213
260
|
|
214
261
|
def delete_fuzzies
|
215
|
-
|
216
|
-
|
217
|
-
key.del
|
262
|
+
fields[FUZZIES].smembers.each do |att|
|
263
|
+
clear_fuzzy_field(att)
|
218
264
|
end
|
219
265
|
end
|
220
|
-
|
221
|
-
def
|
222
|
-
fuzzy_words_and_parts(
|
266
|
+
|
267
|
+
def clear_fuzzy_field(att)
|
268
|
+
fuzzy_words_and_parts(fuzzies[id][att].smembers) do |word, parts|
|
223
269
|
parts.each do |part, encoded|
|
224
|
-
next if existing_words.grep(/^#{part}/u).any?
|
225
270
|
fuzzies[att][encoded].zrem(id)
|
226
271
|
end
|
227
|
-
|
272
|
+
|
273
|
+
fuzzies[id][att].del
|
228
274
|
end
|
229
275
|
end
|
230
276
|
|
@@ -238,5 +284,12 @@ module Lunar
|
|
238
284
|
yield word, partials
|
239
285
|
end
|
240
286
|
end
|
287
|
+
|
288
|
+
def delete_field_meta
|
289
|
+
fields[TEXT].del
|
290
|
+
fields[NUMBERS].del
|
291
|
+
fields[SORTABLES].del
|
292
|
+
fields[FUZZIES].del
|
293
|
+
end
|
241
294
|
end
|
242
295
|
end
|
@@ -21,14 +21,16 @@ module Lunar
|
|
21
21
|
protected
|
22
22
|
def keys
|
23
23
|
if att == :q
|
24
|
-
metaphones.map { |m|
|
24
|
+
metaphones.map { |m|
|
25
|
+
nest[Index::FIELDS][Index::TEXT].smembers.map { |att| nest[att][m] }
|
26
|
+
}
|
25
27
|
else
|
26
28
|
metaphones.map { |m| nest[att][m] }
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
30
32
|
def metaphones
|
31
|
-
Words.new(value).map { |word| Lunar.metaphone(word) }
|
33
|
+
Words.new(value, [:stopwords]).map { |word| Lunar.metaphone(word) }
|
32
34
|
end
|
33
35
|
end
|
34
36
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Lunar
|
2
|
+
# @private Used internally by Lunar::search to get all the fuzzy matches
|
3
|
+
# given `nest`, `att` and it's `val`.
|
4
|
+
class NumberMatches
|
5
|
+
attr :nest
|
6
|
+
attr :att
|
7
|
+
attr :values
|
8
|
+
|
9
|
+
def initialize(nest, att, value)
|
10
|
+
@nest = nest
|
11
|
+
@att = att
|
12
|
+
@values = value.kind_of?(Enumerable) ? value : [value]
|
13
|
+
end
|
14
|
+
|
15
|
+
def distkey
|
16
|
+
case keys.size
|
17
|
+
when 0 then nil
|
18
|
+
when 1 then keys.first
|
19
|
+
else
|
20
|
+
nest[{ att => values }.hash].tap do |dk|
|
21
|
+
dk.zunionstore keys.flatten
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
def keys
|
28
|
+
values.
|
29
|
+
reject { |v| v.to_s.empty? }.
|
30
|
+
map { |v| nest[:Numbers][att][v] }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/lunar/scoring.rb
CHANGED
@@ -3,11 +3,11 @@ module Lunar
|
|
3
3
|
# of a given text.
|
4
4
|
class Scoring
|
5
5
|
def initialize(words)
|
6
|
-
@words = Words.new(words)
|
6
|
+
@words = Words.new(words, [:stopwords, :downcase])
|
7
7
|
end
|
8
8
|
|
9
9
|
def scores
|
10
|
-
@words.inject(Hash.new(0)) { |a, w| a[w
|
10
|
+
@words.inject(Hash.new(0)) { |a, w| a[w] += 1 and a }
|
11
11
|
end
|
12
12
|
end
|
13
|
-
end
|
13
|
+
end
|
data/lib/lunar/stopwords.rb
CHANGED
@@ -3,15 +3,15 @@ module Lunar
|
|
3
3
|
# common words like an, the, etc.
|
4
4
|
module Stopwords
|
5
5
|
def include?(word)
|
6
|
-
stopwords.include?(word)
|
6
|
+
stopwords.include?(word.downcase)
|
7
7
|
end
|
8
|
+
module_function :include?
|
8
9
|
|
9
10
|
private
|
10
11
|
def stopwords
|
11
12
|
%w(an and are as at be but by for if in into is it no not of on or s
|
12
13
|
such t that the their then there these they this to was will with)
|
13
14
|
end
|
14
|
-
|
15
|
-
module_function :stopwords, :include?
|
15
|
+
module_function :stopwords
|
16
16
|
end
|
17
17
|
end
|
data/lib/lunar/words.rb
CHANGED
@@ -4,22 +4,47 @@ module Lunar
|
|
4
4
|
# @private Internally used to determine the words given some str.
|
5
5
|
# i.e. Words.new("the quick brown") == %w(the quick brown)
|
6
6
|
class Words < Array
|
7
|
+
UnknownFilter = Class.new(ArgumentError)
|
8
|
+
|
7
9
|
SEPARATOR = /\s+/
|
10
|
+
FILTERS = [:stopwords, :downcase, :uniq]
|
8
11
|
|
9
|
-
def initialize(str,
|
12
|
+
def initialize(str, filters = [])
|
10
13
|
words = str.split(SEPARATOR).
|
11
14
|
reject { |w| w.to_s.strip.empty? }.
|
12
15
|
map { |w| sanitize(w) }
|
13
|
-
|
14
|
-
words
|
16
|
+
|
17
|
+
apply_filters(words, filters)
|
15
18
|
|
16
19
|
super(words)
|
17
20
|
end
|
18
21
|
|
19
22
|
private
|
23
|
+
def apply_filters(words, filters)
|
24
|
+
filters.each do |filter|
|
25
|
+
unless FILTERS.include?(filter)
|
26
|
+
raise UnknownFilter, "Unknown filter: #{ filter }"
|
27
|
+
end
|
28
|
+
|
29
|
+
send(filter, words)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def stopwords(words)
|
34
|
+
words.reject! { |w| Stopwords.include?(w) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def downcase(words)
|
38
|
+
words.each { |w| w.downcase! }
|
39
|
+
end
|
40
|
+
|
41
|
+
def uniq(words)
|
42
|
+
words.uniq!
|
43
|
+
end
|
44
|
+
|
20
45
|
def sanitize(str)
|
21
|
-
Iconv.iconv('UTF-8//IGNORE', 'UTF-8', str)[0].to_s.
|
22
|
-
gsub(/[
|
46
|
+
Iconv.iconv('UTF-8//IGNORE//TRANSLIT', 'UTF-8', str)[0].to_s.
|
47
|
+
gsub(/["'\.,@!$%\^&\*\(\)\[\]\+\-\_\:\;\<\>\\\/\?\`\~]/, '')
|
23
48
|
end
|
24
49
|
end
|
25
50
|
end
|
data/lunar.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{lunar}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.6.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Cyril David"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-06-30}
|
13
13
|
s.description = %q{Features full text searching via metaphones, range querying for numbers, fuzzy searching and sorting based on custom fields}
|
14
14
|
s.email = %q{cyx.ucron@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |s|
|
|
28
28
|
"lib/lunar/fuzzy_word.rb",
|
29
29
|
"lib/lunar/index.rb",
|
30
30
|
"lib/lunar/keyword_matches.rb",
|
31
|
-
"lib/lunar/
|
31
|
+
"lib/lunar/number_matches.rb",
|
32
32
|
"lib/lunar/range_matches.rb",
|
33
33
|
"lib/lunar/result_set.rb",
|
34
34
|
"lib/lunar/scoring.rb",
|
@@ -40,13 +40,14 @@ Gem::Specification.new do |s|
|
|
40
40
|
"test/test_index.rb",
|
41
41
|
"test/test_lunar.rb",
|
42
42
|
"test/test_lunar_fuzzy_word.rb",
|
43
|
-
"test/
|
43
|
+
"test/test_lunar_words.rb",
|
44
|
+
"test/test_number_search.rb",
|
44
45
|
"test/test_scoring.rb"
|
45
46
|
]
|
46
47
|
s.homepage = %q{http://github.com/sinefunc/lunar}
|
47
48
|
s.rdoc_options = ["--charset=UTF-8"]
|
48
49
|
s.require_paths = ["lib"]
|
49
|
-
s.rubygems_version = %q{1.3.
|
50
|
+
s.rubygems_version = %q{1.3.7}
|
50
51
|
s.summary = %q{A redis based full text search engine}
|
51
52
|
s.test_files = [
|
52
53
|
"test/helper.rb",
|
@@ -54,7 +55,8 @@ Gem::Specification.new do |s|
|
|
54
55
|
"test/test_index.rb",
|
55
56
|
"test/test_lunar.rb",
|
56
57
|
"test/test_lunar_fuzzy_word.rb",
|
57
|
-
"test/
|
58
|
+
"test/test_lunar_words.rb",
|
59
|
+
"test/test_number_search.rb",
|
58
60
|
"test/test_scoring.rb"
|
59
61
|
]
|
60
62
|
|
@@ -62,7 +64,7 @@ Gem::Specification.new do |s|
|
|
62
64
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
63
65
|
s.specification_version = 3
|
64
66
|
|
65
|
-
if Gem::Version.new(Gem::
|
67
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
66
68
|
s.add_runtime_dependency(%q<redis>, [">= 2.0.0"])
|
67
69
|
s.add_runtime_dependency(%q<nest>, [">= 0.0.4"])
|
68
70
|
s.add_runtime_dependency(%q<text>, [">= 0"])
|
data/test/test_index.rb
CHANGED
@@ -22,6 +22,8 @@ class IndexTest < Test::Unit::TestCase
|
|
22
22
|
|
23
23
|
assert_equal %w{APL IFN KS SMRTFN},
|
24
24
|
Lunar.nest[:Gadget][:Metaphones][1001][:title].smembers.sort
|
25
|
+
|
26
|
+
assert_equal %w(title), Lunar.redis.smembers('Lunar:Gadget:Fields:Text')
|
25
27
|
end
|
26
28
|
|
27
29
|
test "deleting non-repeating words scenario" do
|
@@ -38,6 +40,8 @@ class IndexTest < Test::Unit::TestCase
|
|
38
40
|
assert_equal 0, nest('smartphone').zcard
|
39
41
|
|
40
42
|
assert_equal 0, Lunar.nest[:Gadget][1001][:title].scard
|
43
|
+
|
44
|
+
assert ! Lunar.redis.exists('Lunar:Gadget:Fields:Text')
|
41
45
|
end
|
42
46
|
|
43
47
|
test "with multiple word instances and stopwords" do
|
@@ -86,22 +90,65 @@ class IndexTest < Test::Unit::TestCase
|
|
86
90
|
assert_equal ['1001'], nest('apple').zrangebyscore(1, 1)
|
87
91
|
assert_equal ['1001'], nest('iphone').zrangebyscore(1, 1)
|
88
92
|
assert_equal ['1001'], nest('3g').zrangebyscore(1, 1)
|
89
|
-
|
93
|
+
|
90
94
|
assert nest('3gs').zrange(0, -1).empty?
|
91
95
|
assert nest('smartphone').zrange(0, -1).empty?
|
96
|
+
|
97
|
+
assert_equal %w(title), Lunar.redis.smembers("Lunar:Gadget:Fields:Text")
|
92
98
|
end
|
93
99
|
end
|
94
100
|
|
95
101
|
describe "indexing numbers" do
|
102
|
+
def numbers
|
103
|
+
Lunar.nest[:Gadget][:Numbers]
|
104
|
+
end
|
105
|
+
|
96
106
|
test "works for integers and floats" do
|
97
107
|
Lunar.index :Gadget do |i|
|
98
108
|
i.id 1001
|
99
109
|
i.number :price, 200
|
100
110
|
i.number :score, 25.5
|
111
|
+
i.number :category_ids, %w(100 101 102)
|
101
112
|
end
|
102
113
|
|
103
|
-
assert_equal '200',
|
104
|
-
assert_equal '25.5',
|
114
|
+
assert_equal '200', numbers[:price].zscore(1001)
|
115
|
+
assert_equal '25.5', numbers[:score].zscore(1001)
|
116
|
+
|
117
|
+
assert_equal '1', numbers[:price]["200"].zscore(1001)
|
118
|
+
|
119
|
+
assert_equal '1', numbers[:category_ids]["100"].zscore(1001)
|
120
|
+
assert_equal '1', numbers[:category_ids]["101"].zscore(1001)
|
121
|
+
assert_equal '1', numbers[:category_ids]["102"].zscore(1001)
|
122
|
+
|
123
|
+
assert_equal %w(category_ids price score),
|
124
|
+
Lunar.redis.smembers("Lunar:Gadget:Fields:Numbers").sort
|
125
|
+
end
|
126
|
+
|
127
|
+
test "reindexing" do
|
128
|
+
Lunar.index :Gadget do |i|
|
129
|
+
i.id 1001
|
130
|
+
i.number :price, 200
|
131
|
+
i.number :author_ids, [10, 20, 30]
|
132
|
+
end
|
133
|
+
|
134
|
+
Lunar.index :Gadget do |i|
|
135
|
+
i.id 1001
|
136
|
+
i.number :price, 150
|
137
|
+
i.number :author_ids, [40, 50, 60]
|
138
|
+
end
|
139
|
+
|
140
|
+
assert_nil numbers[:price]["200"].zrank(1001)
|
141
|
+
assert_equal '1', numbers[:price]["150"].zscore(1001)
|
142
|
+
|
143
|
+
assert_nil numbers[:author_ids]["10"].zrank(1001)
|
144
|
+
assert_nil numbers[:author_ids]["20"].zrank(1001)
|
145
|
+
assert_nil numbers[:author_ids]["30"].zrank(1001)
|
146
|
+
assert_equal '1', numbers[:author_ids]["40"].zscore(1001)
|
147
|
+
assert_equal '1', numbers[:author_ids]["50"].zscore(1001)
|
148
|
+
assert_equal '1', numbers[:author_ids]["60"].zscore(1001)
|
149
|
+
|
150
|
+
assert_equal %w(author_ids price),
|
151
|
+
Lunar.redis.smembers("Lunar:Gadget:Fields:Numbers").sort
|
105
152
|
end
|
106
153
|
|
107
154
|
test "allows deletion" do
|
@@ -113,8 +160,11 @@ class IndexTest < Test::Unit::TestCase
|
|
113
160
|
|
114
161
|
Lunar.delete :Gadget, 1001
|
115
162
|
|
116
|
-
assert_nil
|
117
|
-
assert_nil
|
163
|
+
assert_nil numbers[:price].zrank(1001)
|
164
|
+
assert_nil numbers[:score].zrank(1001)
|
165
|
+
assert_nil numbers[:price]["200"].zrank(1001)
|
166
|
+
|
167
|
+
assert ! Lunar.redis.exists("Lunar:Gadget:Fields:Numbers")
|
118
168
|
end
|
119
169
|
end
|
120
170
|
|
@@ -130,6 +180,9 @@ class IndexTest < Test::Unit::TestCase
|
|
130
180
|
assert_equal 'iphone', Lunar.nest[:Gadget][:Sortables][1001][:name].get
|
131
181
|
assert_equal '200', Lunar.nest[:Gadget][:Sortables][1001][:price].get
|
132
182
|
assert_equal '25.5', Lunar.nest[:Gadget][:Sortables][1001][:score].get
|
183
|
+
|
184
|
+
assert_equal %w(name price score),
|
185
|
+
Lunar.redis.smembers("Lunar:Gadget:Fields:Sortables").sort
|
133
186
|
end
|
134
187
|
|
135
188
|
test "deletes sortable fields" do
|
@@ -142,9 +195,11 @@ class IndexTest < Test::Unit::TestCase
|
|
142
195
|
|
143
196
|
Lunar.delete :Gadget, 1001
|
144
197
|
|
145
|
-
assert_nil Lunar.nest[:Gadget][1001][:name].get
|
146
|
-
assert_nil Lunar.nest[:Gadget][1001][:price].get
|
147
|
-
assert_nil Lunar.nest[:Gadget][1001][:score].get
|
198
|
+
assert_nil Lunar.nest[:Gadget][:Sortables][1001][:name].get
|
199
|
+
assert_nil Lunar.nest[:Gadget][:Sortables][1001][:price].get
|
200
|
+
assert_nil Lunar.nest[:Gadget][:Sortables][1001][:score].get
|
201
|
+
|
202
|
+
assert ! Lunar.redis.exists("Lunar:Gadget:Fields:Sortables")
|
148
203
|
end
|
149
204
|
end
|
150
|
-
end
|
205
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "helper"
|
4
|
+
|
5
|
+
class LunarWordsTest < Test::Unit::TestCase
|
6
|
+
test "german words" do
|
7
|
+
str = "Der schnelle braune Fuchs springt über den faulen Hund"
|
8
|
+
metaphones = %w(TR SXNL BRN FXS SPRNKT BR TN FLN HNT)
|
9
|
+
|
10
|
+
words = Lunar::Words.new(str)
|
11
|
+
|
12
|
+
assert_equal metaphones, words.map { |w| Lunar.metaphone(w) }
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class NumberSearchTest < Test::Unit::TestCase
|
4
|
+
class Gadget < Struct.new(:id)
|
5
|
+
def self.[](id)
|
6
|
+
new(id)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
setup do
|
11
|
+
Lunar.index Gadget do |i|
|
12
|
+
i.id 1001
|
13
|
+
i.number :price, 200
|
14
|
+
i.number :category_ids, %w(100 101 102)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
test "doing straight searches for numbers" do
|
19
|
+
search = Lunar.search Gadget, numbers: { price: 200 }
|
20
|
+
|
21
|
+
assert_equal ["1001"], search.map(&:id)
|
22
|
+
end
|
23
|
+
|
24
|
+
test "also doing range searches" do
|
25
|
+
search = Lunar.search Gadget, price: 150..200
|
26
|
+
|
27
|
+
assert_equal ["1001"], search.map(&:id)
|
28
|
+
end
|
29
|
+
|
30
|
+
test "searching both" do
|
31
|
+
search = Lunar.search Gadget, price: 150..200, numbers: { price: 200 }
|
32
|
+
|
33
|
+
assert_equal ["1001"], search.map(&:id)
|
34
|
+
end
|
35
|
+
|
36
|
+
test "searching for a multi-id individually" do
|
37
|
+
search100 = Lunar.search Gadget, numbers: { category_ids: 100 }
|
38
|
+
search101 = Lunar.search Gadget, numbers: { category_ids: 101 }
|
39
|
+
search102 = Lunar.search Gadget, numbers: { category_ids: 102 }
|
40
|
+
|
41
|
+
assert_equal ["1001"], search100.map(&:id)
|
42
|
+
assert_equal ["1001"], search101.map(&:id)
|
43
|
+
assert_equal ["1001"], search102.map(&:id)
|
44
|
+
end
|
45
|
+
|
46
|
+
test "searching for a valid multi-id at the same time" do
|
47
|
+
search = Lunar.search Gadget, numbers: { category_ids: %w(100 101 102) }
|
48
|
+
|
49
|
+
assert_equal ["1001"], search.map(&:id)
|
50
|
+
end
|
51
|
+
|
52
|
+
test "searching with one invalid multi-id at the same time" do
|
53
|
+
search = Lunar.search Gadget, numbers: { category_ids: %w(100 101 102 103) }
|
54
|
+
|
55
|
+
assert_equal ["1001"], search.map(&:id)
|
56
|
+
end
|
57
|
+
|
58
|
+
test "searching with all invalid multi-id" do
|
59
|
+
search = Lunar.search Gadget, numbers: { category_ids: %w(103 104 105) }
|
60
|
+
assert_equal 0, search.size
|
61
|
+
end
|
62
|
+
|
63
|
+
test "searching with valid category_ids and an empty price array" do
|
64
|
+
search = Lunar.search(Gadget, numbers: { category_ids: %w(100 101 102),
|
65
|
+
price: [""]})
|
66
|
+
|
67
|
+
assert_equal ["1001"], search.map(&:id)
|
68
|
+
end
|
69
|
+
end
|
data/test/test_scoring.rb
CHANGED
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 6
|
8
|
+
- 0
|
9
|
+
version: 0.6.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Cyril David
|
@@ -14,13 +14,14 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-06-30 00:00:00 +08:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: redis
|
22
22
|
prerelease: false
|
23
23
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
24
25
|
requirements:
|
25
26
|
- - ">="
|
26
27
|
- !ruby/object:Gem::Version
|
@@ -35,6 +36,7 @@ dependencies:
|
|
35
36
|
name: nest
|
36
37
|
prerelease: false
|
37
38
|
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
38
40
|
requirements:
|
39
41
|
- - ">="
|
40
42
|
- !ruby/object:Gem::Version
|
@@ -49,6 +51,7 @@ dependencies:
|
|
49
51
|
name: text
|
50
52
|
prerelease: false
|
51
53
|
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
52
55
|
requirements:
|
53
56
|
- - ">="
|
54
57
|
- !ruby/object:Gem::Version
|
@@ -61,6 +64,7 @@ dependencies:
|
|
61
64
|
name: contest
|
62
65
|
prerelease: false
|
63
66
|
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
64
68
|
requirements:
|
65
69
|
- - ">="
|
66
70
|
- !ruby/object:Gem::Version
|
@@ -90,7 +94,7 @@ files:
|
|
90
94
|
- lib/lunar/fuzzy_word.rb
|
91
95
|
- lib/lunar/index.rb
|
92
96
|
- lib/lunar/keyword_matches.rb
|
93
|
-
- lib/lunar/
|
97
|
+
- lib/lunar/number_matches.rb
|
94
98
|
- lib/lunar/range_matches.rb
|
95
99
|
- lib/lunar/result_set.rb
|
96
100
|
- lib/lunar/scoring.rb
|
@@ -102,7 +106,8 @@ files:
|
|
102
106
|
- test/test_index.rb
|
103
107
|
- test/test_lunar.rb
|
104
108
|
- test/test_lunar_fuzzy_word.rb
|
105
|
-
- test/
|
109
|
+
- test/test_lunar_words.rb
|
110
|
+
- test/test_number_search.rb
|
106
111
|
- test/test_scoring.rb
|
107
112
|
has_rdoc: true
|
108
113
|
homepage: http://github.com/sinefunc/lunar
|
@@ -114,6 +119,7 @@ rdoc_options:
|
|
114
119
|
require_paths:
|
115
120
|
- lib
|
116
121
|
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
117
123
|
requirements:
|
118
124
|
- - ">="
|
119
125
|
- !ruby/object:Gem::Version
|
@@ -121,6 +127,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
127
|
- 0
|
122
128
|
version: "0"
|
123
129
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
124
131
|
requirements:
|
125
132
|
- - ">="
|
126
133
|
- !ruby/object:Gem::Version
|
@@ -130,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
137
|
requirements: []
|
131
138
|
|
132
139
|
rubyforge_project:
|
133
|
-
rubygems_version: 1.3.
|
140
|
+
rubygems_version: 1.3.7
|
134
141
|
signing_key:
|
135
142
|
specification_version: 3
|
136
143
|
summary: A redis based full text search engine
|
@@ -140,5 +147,6 @@ test_files:
|
|
140
147
|
- test/test_index.rb
|
141
148
|
- test/test_lunar.rb
|
142
149
|
- test/test_lunar_fuzzy_word.rb
|
143
|
-
- test/
|
150
|
+
- test/test_lunar_words.rb
|
151
|
+
- test/test_number_search.rb
|
144
152
|
- test/test_scoring.rb
|
data/lib/lunar/lunar_nest.rb
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
module Lunar
|
2
|
-
# @private Provides convenience methods to look up keys.
|
3
|
-
# Since the Redis KEYS command is actually pretty slow,
|
4
|
-
# i'm considering an alternative approach of manually maintaining
|
5
|
-
# the sets to manage all the groups of keys.
|
6
|
-
class LunarNest < Nest
|
7
|
-
def keys
|
8
|
-
redis.keys self
|
9
|
-
end
|
10
|
-
|
11
|
-
def matches
|
12
|
-
regex = Regexp.new(self.gsub('*', '(.*)'))
|
13
|
-
keys.map { |key|
|
14
|
-
match = key.match(regex)
|
15
|
-
[LunarNest.new(key, redis), *match[1, match.size - 1]]
|
16
|
-
}
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
data/test/test_lunar_nest.rb
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
require "helper"
|
2
|
-
|
3
|
-
class LunarNestTest < Test::Unit::TestCase
|
4
|
-
test "retrieving keys for a pattern" do
|
5
|
-
Lunar.redis.set("Foo:1:Bar", 1)
|
6
|
-
Lunar.redis.set("Foo:2:Bar", 2)
|
7
|
-
|
8
|
-
nest = Lunar::LunarNest.new("Foo", Lunar.redis)['*']["Bar"]
|
9
|
-
|
10
|
-
assert_equal ['Foo:1:Bar', 'Foo:2:Bar'], nest.keys.sort
|
11
|
-
end
|
12
|
-
|
13
|
-
test "retrieving keys and their matches" do
|
14
|
-
Lunar.redis.set("Foo:1:Bar", 1)
|
15
|
-
Lunar.redis.set("Foo:2:Bar", 2)
|
16
|
-
|
17
|
-
nest = Lunar::LunarNest.new("Foo", Lunar.redis)['*']["Bar"]
|
18
|
-
|
19
|
-
matches = []
|
20
|
-
|
21
|
-
nest.matches.each do |key, m|
|
22
|
-
matches << m
|
23
|
-
end
|
24
|
-
|
25
|
-
assert_equal ['1', '2'], matches.sort
|
26
|
-
end
|
27
|
-
|
28
|
-
test "retrieving keys and their matches when more than one *" do
|
29
|
-
Lunar.redis.set("Foo:1:Bar:3:Baz", 1)
|
30
|
-
Lunar.redis.set("Foo:2:Bar:4:Baz", 2)
|
31
|
-
|
32
|
-
nest = Lunar::LunarNest.new("Foo", Lunar.redis)['*']["Bar"]["*"]["Baz"]
|
33
|
-
|
34
|
-
matches1 = []
|
35
|
-
matches2 = []
|
36
|
-
|
37
|
-
nest.matches.each do |key, m1, m2|
|
38
|
-
matches1 << m1
|
39
|
-
matches2 << m2
|
40
|
-
end
|
41
|
-
|
42
|
-
assert_equal ['1', '2'], matches1.sort
|
43
|
-
assert_equal ['3', '4'], matches2.sort
|
44
|
-
end
|
45
|
-
|
46
|
-
end
|