lunar 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.5
1
+ 0.6.0
@@ -4,15 +4,15 @@ require 'nest'
4
4
  require 'text'
5
5
 
6
6
  module Lunar
7
- VERSION = '0.5.3'
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 if this document.
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).strip
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
- LunarNest.new(:Lunar, redis)
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
@@ -20,7 +20,9 @@ module Lunar
20
20
 
21
21
  protected
22
22
  def keys
23
- Words.new(value, false).map { |w| nest[:Fuzzies][att][Lunar.encode(w)] }
23
+ Words.new(value, [:downcase]).map { |w|
24
+ nest[:Fuzzies][att][Lunar.encode(w)]
25
+ }
24
26
  end
25
27
  end
26
28
  end
@@ -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[:Metaphones]
28
- @numbers = @nest[:Numbers]
29
- @sortables = @nest[:Sortables]
30
- @fuzzies = @nest[:Fuzzies]
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
- old = metaphones[id][att].smembers
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
- (old - new).each do |metaphone|
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
- if value.to_s.length > FUZZY_MAX_LENGTH
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
- words = Words.new(value).uniq
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 do |part, encoded|
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
- delete_fuzzies_for(att, fuzzies[id][att].smembers - words, words)
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
- metaphones[id]['*'].matches.each do |key, att|
194
- key.smembers.each do |metaphone|
195
- nest[att][metaphone].zrem id
196
- end
228
+ fields[TEXT].smembers.each do |att|
229
+ clear_text_field(att)
230
+ end
231
+ end
197
232
 
198
- key.del
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
- numbers['*'].matches.each do |key, att|
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
- sortables[id]['*'].keys.each do |key|
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
- fuzzies[id]['*'].matches.each do |key, att|
216
- delete_fuzzies_for(att, key.smembers)
217
- key.del
262
+ fields[FUZZIES].smembers.each do |att|
263
+ clear_fuzzy_field(att)
218
264
  end
219
265
  end
220
-
221
- def delete_fuzzies_for(att, words_to_delete, existing_words = [])
222
- fuzzy_words_and_parts(words_to_delete) do |word, 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
- fuzzies[id][att].srem word
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| nest['*'][m].keys }
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
@@ -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.downcase] += 1 and a }
10
+ @words.inject(Hash.new(0)) { |a, w| a[w] += 1 and a }
11
11
  end
12
12
  end
13
- end
13
+ end
@@ -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
@@ -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, stopwords = true)
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.reject! { |w| Stopwords.include?(w) } if stopwords
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(/[^a-zA-Z0-9\-_]/, '').downcase
46
+ Iconv.iconv('UTF-8//IGNORE//TRANSLIT', 'UTF-8', str)[0].to_s.
47
+ gsub(/["'\.,@!$%\^&\*\(\)\[\]\+\-\_\:\;\<\>\\\/\?\`\~]/, '')
23
48
  end
24
49
  end
25
50
  end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{lunar}
8
- s.version = "0.5.5"
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-05-31}
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/lunar_nest.rb",
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/test_lunar_nest.rb",
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.6}
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/test_lunar_nest.rb",
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::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
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"])
@@ -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', Lunar.nest[:Gadget][:Numbers][:price].zscore(1001)
104
- assert_equal '25.5', Lunar.nest[:Gadget][:Numbers][:score].zscore(1001)
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 Lunar.nest[:Gadget][:Numbers][:price].zrank(1001)
117
- assert_nil Lunar.nest[:Gadget][:Numbers][:score].zrank(1001)
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
@@ -33,4 +33,4 @@ class LunarScoringTest < Test::Unit::TestCase
33
33
  assert_equal 1, scoring.scores['17']
34
34
  end
35
35
  end
36
- end
36
+ end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 5
8
- - 5
9
- version: 0.5.5
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-05-31 00:00:00 +08:00
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/lunar_nest.rb
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/test_lunar_nest.rb
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.6
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/test_lunar_nest.rb
150
+ - test/test_lunar_words.rb
151
+ - test/test_number_search.rb
144
152
  - test/test_scoring.rb
@@ -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
@@ -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