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