lunar 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -19,3 +19,4 @@ rdoc
19
19
  pkg
20
20
 
21
21
  ## PROJECT::SPECIFIC
22
+ .rvmrc
data/README.md CHANGED
@@ -40,6 +40,14 @@ Examples
40
40
  i.attr :name, name
41
41
  i.attr :description, description
42
42
  end
43
+
44
+ Lunar::Index.create Item do |i|
45
+ i.key id
46
+ i.fuzzy :name, name # this has a 100 character limit on name
47
+ # for performance reasons
48
+ i.integer :cost, cost
49
+ i.float :voting_quotient, voting_quotient
50
+ end
43
51
  end
44
52
  end
45
53
 
@@ -49,6 +57,14 @@ Examples
49
57
 
50
58
  # Or opt to filter by field
51
59
  Lunar.search(Item, :name => "iphone", :description => "mobile")
60
+
61
+ # For fuzzy declared fields you can currently only search
62
+ # using a fuzzy strategy exclusively, e.g.
63
+ Lunar.search(Item, :fuzzy => { :name => "i" })
64
+ # i, ip, iph, ipho, iphone, 3, 3g, 3gs all would match 'iPhone 3Gs'
65
+
66
+ # For integer / float types, you can do range searches on them e.g.
67
+ Lunar.search(Item, :cost => 300..500, :voting_quotient => 10..20)
52
68
 
53
69
  # Or using the pagination gem with this:
54
70
  @items = Lunar.search(Item, "iphone")
data/Rakefile CHANGED
@@ -11,6 +11,7 @@ begin
11
11
  gem.homepage = "http://github.com/cyx/lunar"
12
12
  gem.authors = ["Cyril David"]
13
13
  gem.add_development_dependency "contest"
14
+ gem.add_development_dependency "mocha"
14
15
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
16
  end
16
17
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.3.0
@@ -2,12 +2,22 @@ require 'base64'
2
2
  require File.join(File.dirname(__FILE__), '..', 'vendor', 'nest', 'nest')
3
3
 
4
4
  module Lunar
5
- autoload :Scoring, 'lunar/scoring'
6
- autoload :Index, 'lunar/index'
7
- autoload :Words, 'lunar/words'
8
- autoload :Search, 'lunar/search'
9
- autoload :Sets, 'lunar/sets'
10
- autoload :ResultSet, 'lunar/result_set'
5
+ autoload :Scoring, 'lunar/scoring'
6
+ autoload :Index, 'lunar/index'
7
+ autoload :Words, 'lunar/words'
8
+ autoload :Search, 'lunar/search'
9
+ autoload :Sets, 'lunar/sets'
10
+ autoload :SortedResultSet, 'lunar/result_set'
11
+ autoload :UnsortedResultSet, 'lunar/result_set'
12
+ autoload :FuzzyWord, 'lunar/fuzzy_word'
13
+
14
+ def self.ttl
15
+ @ttl ||= 30
16
+ end
17
+
18
+ def self.ttl=(ttl)
19
+ @ttl = ttl
20
+ end
11
21
 
12
22
  def self.redis(connection = defined?(Ohm) ? Ohm.redis : nil)
13
23
  @connection ||= connection
@@ -0,0 +1,7 @@
1
+ module Lunar
2
+ class FuzzyWord < String
3
+ def partials
4
+ (1..length).map { |i| self[0, i] }
5
+ end
6
+ end
7
+ end
@@ -1,5 +1,9 @@
1
1
  module Lunar
2
2
  class Index
3
+ FUZZY_MAX_LENGTH = 100
4
+
5
+ FuzzyFieldTooLong = Class.new(StandardError)
6
+
3
7
  attr :ns
4
8
 
5
9
  def self.create(prefix)
@@ -15,6 +19,7 @@ module Lunar
15
19
  @attrs = {}
16
20
  @redis = (redis || Lunar.redis)
17
21
  @numeric_attrs = {}
22
+ @fuzzy_attrs = {}
18
23
  end
19
24
 
20
25
  def key(key = nil)
@@ -38,8 +43,14 @@ module Lunar
38
43
  alias :integer :numeric_attr
39
44
  alias :float :numeric_attr
40
45
 
46
+ def fuzzy(field, value = nil)
47
+ @fuzzy_attrs[field.to_sym] = value if value
48
+ @fuzzy_attrs
49
+ end
50
+
41
51
  def index
42
52
  index_str_attrs
53
+ index_fuzzy_attrs
43
54
  index_numeric_attrs
44
55
 
45
56
  return self
@@ -53,7 +64,17 @@ module Lunar
53
64
  words.each do |w|
54
65
  @redis.zrem @ns[field][Lunar.encode(w)], key
55
66
  end
56
- @redis.del k
67
+ @redis.del k
68
+ end
69
+
70
+ fuzzy_keys = @redis.keys @ns[:Fuzzy][key]['*']
71
+ fuzzy_keys.each do |k|
72
+ field = k.gsub("#{ @ns[:Fuzzy][key] }:", '')
73
+ words = @redis.smembers(k)
74
+ words.each do |w|
75
+ remove_fuzzy_values field
76
+ end
77
+ @redis.del k
57
78
  end
58
79
  end
59
80
 
@@ -76,11 +97,40 @@ module Lunar
76
97
  end
77
98
  end
78
99
 
100
+ def index_fuzzy_attrs
101
+ @fuzzy_attrs.each do |field, value|
102
+ if value.to_s.length > FUZZY_MAX_LENGTH
103
+ raise FuzzyFieldTooLong,
104
+ "#{field} has a value #{value} exceeding #{FUZZY_MAX_LENGTH} chars"
105
+ end
106
+
107
+ words = Words.new(value).uniq
108
+ words.each do |word|
109
+ FuzzyWord.new(word).partials.each do |partial|
110
+ @redis.sadd @ns[:Fuzzy][field][Lunar.encode(partial)], key
111
+ end
112
+ @redis.sadd @ns[:Fuzzy][key][field], word
113
+ end
114
+
115
+ remove_fuzzy_values field, words
116
+ end
117
+ end
118
+
119
+ def remove_fuzzy_values(field, words = [])
120
+ unused_words = @redis.smembers(@ns[:Fuzzy][key][field]) - words
121
+ unused_words.each do |word|
122
+ FuzzyWord.new(word).partials.each do |partial|
123
+ next if words.grep(/^#{partial}/u).any?
124
+ @redis.srem @ns[:Fuzzy][field][Lunar.encode(partial)], key
125
+ end
126
+ @redis.srem @ns[:Fuzzy][key][field], word
127
+ end
128
+ end
129
+
79
130
  def index_numeric_attrs
80
131
  @numeric_attrs.each do |field, value|
81
132
  @redis.zadd @ns[field], value, key
82
133
  end
83
134
  end
84
-
85
135
  end
86
136
  end
@@ -2,26 +2,43 @@ module Lunar
2
2
  class ResultSet
3
3
  include Enumerable
4
4
 
5
- attr :sorted_set_key
5
+ attr :key
6
6
 
7
- def initialize(sorted_set_key, &block)
8
- @sorted_set_key = sorted_set_key
9
- @block = block
7
+ def initialize(key, &block)
8
+ @key = key
9
+ @block = block
10
10
  end
11
11
 
12
12
  def each(&block)
13
13
  all.each(&block)
14
14
  end
15
+ end
15
16
 
17
+ class SortedResultSet < ResultSet
16
18
  def all(options = {})
17
19
  start = Integer(options[:start] || 0)
18
- finish = start + Integer(options[:limit] || 0) - 1
20
+ limit = Integer(options[:limit] || 0)
21
+ finish = start + limit - 1
22
+
23
+ puts "Getting: #{key}, #{Lunar.redis.zrange(key, start, finish)}" if $GAME
24
+ Lunar.redis.zrevrange(key, start, finish).map(&@block)
25
+ end
26
+
27
+ def size
28
+ Lunar.redis.zcard(key)
29
+ end
30
+ end
19
31
 
20
- Lunar.redis.zrevrange(sorted_set_key, start, finish).map(&@block)
32
+ class UnsortedResultSet < ResultSet
33
+ def all(options = {})
34
+ start = Integer(options[:start] || 0)
35
+ limit = Integer(options[:limit] || 100)
36
+
37
+ Lunar.redis.sort(key, :limit => [start, limit]).map(&@block)
21
38
  end
22
39
 
23
40
  def size
24
- Lunar.redis.zcard(sorted_set_key)
41
+ Lunar.redis.scard(key)
25
42
  end
26
43
  end
27
44
  end
@@ -1,11 +1,20 @@
1
1
  module Lunar
2
2
  class Search
3
- attr :sets, :prefix, :search_identifier
3
+ attr :sets, :prefix, :search_identifier, :fuzzy_sets
4
4
 
5
5
  def initialize(prefix, keywords)
6
6
  if keywords.is_a?(Hash)
7
- @sets = keywords.inject([]) { |a, (field, query)| a | Sets.new(prefix, query, field) }
8
- @search_identifier = keywords.hash
7
+ if fuzzy_hash = keywords.delete(:fuzzy)
8
+ @fuzzy_sets = fuzzy_hash.inject([]) { |a, (field, query)|
9
+ a | FuzzySets.new(prefix, query, field)
10
+ }
11
+ @search_identifier = fuzzy_hash.hash
12
+ else
13
+ @sets = keywords.inject([]) { |a, (field, query)|
14
+ a | Sets.new(prefix, query, field)
15
+ }
16
+ @search_identifier = keywords.hash
17
+ end
9
18
  else
10
19
  @sets = Sets.new(prefix, keywords)
11
20
  @search_identifier = keywords.hash
@@ -18,15 +27,28 @@ module Lunar
18
27
  end
19
28
 
20
29
  def results(&block)
21
- return [] if sets.empty?
30
+ block ||= @finder
22
31
 
23
- # TODO : cache this for X seconds, X seconds being customizable
24
- Lunar.redis.zunion dist_key, sets.size, *sets
25
-
26
- if block_given?
27
- ResultSet.new(dist_key, &block)
28
- else
29
- ResultSet.new(dist_key, &@finder)
32
+ if sets
33
+ if sets.empty?
34
+ return []
35
+ else
36
+ if not Lunar.redis.exists(dist_key)
37
+ Lunar.redis.zunion dist_key, sets.size, *sets
38
+ Lunar.redis.expire dist_key, Lunar.ttl
39
+ end
40
+ SortedResultSet.new(dist_key, &block)
41
+ end
42
+ elsif fuzzy_sets
43
+ if fuzzy_sets.empty?
44
+ return []
45
+ else
46
+ if not Lunar.redis.exists(dist_key)
47
+ Lunar.redis.sunionstore dist_key, *fuzzy_sets
48
+ Lunar.redis.expire dist_key, Lunar.ttl
49
+ end
50
+ UnsortedResultSet.new(dist_key, &block)
51
+ end
30
52
  end
31
53
  end
32
54
 
@@ -14,6 +14,27 @@ module Lunar
14
14
  end
15
15
  end
16
16
 
17
+ class FuzzySets < Array
18
+ attr :prefix, :words, :field
19
+
20
+ def initialize(prefix, keywords, field)
21
+ @prefix = prefix
22
+ @field = field
23
+ @words = Words.new(keywords)
24
+
25
+ super(redis_set_keys)
26
+ end
27
+
28
+ protected
29
+ def redis_set_keys
30
+ words.map { |w| ns[Lunar.encode(w)] }
31
+ end
32
+
33
+ def ns
34
+ @ns ||= Lunar.nest[prefix][:Fuzzy][field]
35
+ end
36
+ end
37
+
17
38
  class RangeSets < Array
18
39
  attr :prefix, :field, :range
19
40
 
@@ -28,10 +49,8 @@ module Lunar
28
49
  def write_and_retrieve_key
29
50
  zrange = Lunar.redis.zrangebyscore(Lunar.nest[prefix][field],
30
51
  @range.first, @range.last)
31
- zrange.each do |id|
32
- Lunar.redis.zadd key, 1, id
33
- end
34
- # TODO :expire the key in X seconds where X is customizable
52
+
53
+ zrange.each { |id| Lunar.redis.zadd key, 1, id }
35
54
  key.to_s
36
55
  end
37
56
 
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{lunar}
8
- s.version = "0.2.3"
8
+ s.version = "0.3.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-03}
12
+ s.date = %q{2010-05-06}
13
13
  s.description = %q{uses sorted sets and sets, sorting by score}
14
14
  s.email = %q{cyx.ucron@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
25
25
  "VERSION",
26
26
  "examples/ohm.rb",
27
27
  "lib/lunar.rb",
28
+ "lib/lunar/fuzzy_word.rb",
28
29
  "lib/lunar/index.rb",
29
30
  "lib/lunar/result_set.rb",
30
31
  "lib/lunar/scoring.rb",
@@ -34,6 +35,8 @@ Gem::Specification.new do |s|
34
35
  "lunar.gemspec",
35
36
  "test/helper.rb",
36
37
  "test/test_lunar.rb",
38
+ "test/test_lunar_fuzzy.rb",
39
+ "test/test_lunar_fuzzy_word.rb",
37
40
  "test/test_lunar_index.rb",
38
41
  "test/test_lunar_scoring.rb",
39
42
  "test/test_lunar_search.rb",
@@ -80,6 +83,8 @@ Gem::Specification.new do |s|
80
83
  s.test_files = [
81
84
  "test/helper.rb",
82
85
  "test/test_lunar.rb",
86
+ "test/test_lunar_fuzzy.rb",
87
+ "test/test_lunar_fuzzy_word.rb",
83
88
  "test/test_lunar_index.rb",
84
89
  "test/test_lunar_scoring.rb",
85
90
  "test/test_lunar_search.rb",
@@ -93,11 +98,14 @@ Gem::Specification.new do |s|
93
98
 
94
99
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
95
100
  s.add_development_dependency(%q<contest>, [">= 0"])
101
+ s.add_development_dependency(%q<mocha>, [">= 0"])
96
102
  else
97
103
  s.add_dependency(%q<contest>, [">= 0"])
104
+ s.add_dependency(%q<mocha>, [">= 0"])
98
105
  end
99
106
  else
100
107
  s.add_dependency(%q<contest>, [">= 0"])
108
+ s.add_dependency(%q<mocha>, [">= 0"])
101
109
  end
102
110
  end
103
111
 
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'test/unit'
3
3
  require 'contest'
4
+ require 'mocha'
4
5
 
5
6
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
7
  $LOAD_PATH.unshift(File.dirname(__FILE__))
@@ -0,0 +1,118 @@
1
+ #
2
+ # module Lunar
3
+ # module Fuzzy
4
+ #
5
+ # end
6
+ # end
7
+ require "helper"
8
+
9
+ class LunarFuzzyTest < Test::Unit::TestCase
10
+ setup do
11
+ Lunar.redis(Redis.new(:host => '127.0.0.1', :port => '6380'))
12
+ Lunar.redis.flushdb
13
+ end
14
+
15
+ context "when setting fuzzy name, 'Yukihiro Matsumoto'" do
16
+ setup do
17
+ @index = Lunar::Index.create 'Item' do |i|
18
+ i.key 1001
19
+ i.fuzzy :name, 'Yukihiro Matsumoto'
20
+ end
21
+ end
22
+
23
+ should "store Lunar:Item:name:Y up to o and M up to o" do
24
+ fname, lname = 'yukihiro', 'matsumoto'
25
+
26
+ (1..fname.length).each do |length|
27
+ key = "Lunar:Item:Fuzzy:name:#{ encode(fname[0, length]) }"
28
+ assert Lunar.redis.smembers(key).include?('1001')
29
+ end
30
+
31
+ (1..lname.length).each do |length|
32
+ key = "Lunar:Item:Fuzzy:name:#{ encode(lname[0, length]) }"
33
+ assert Lunar.redis.smembers(key).include?('1001')
34
+ end
35
+ end
36
+ end
37
+
38
+ context "when creating an index that already exists" do
39
+ setup do
40
+ @index = Lunar::Index.create 'Item' do |i|
41
+ i.key 1001
42
+ i.fuzzy :name, 'Yukihiro Matsumoto'
43
+ end
44
+
45
+ @index = Lunar::Index.create 'Item' do |i|
46
+ i.key 1001
47
+ i.fuzzy :name, 'Martin Fowler Yuki'
48
+ end
49
+ end
50
+
51
+ should "remove all fuzzy entries for Yukihiro Matsumoto" do
52
+ fname, lname = 'yukihiro', 'matsumoto'
53
+
54
+ (5..fname.length).each do |length|
55
+ key = "Lunar:Item:Fuzzy:name:#{ encode(fname[0, length]) }"
56
+ assert ! Lunar.redis.smembers(key).include?('1001')
57
+ end
58
+
59
+ (3..lname.length).each do |length|
60
+ key = "Lunar:Item:Fuzzy:name:#{ encode(lname[0, length]) }"
61
+ assert ! Lunar.redis.smembers(key).include?('1001')
62
+ end
63
+ end
64
+
65
+ should "store Lunar:Item:name:M up to n and F up to r etc..." do
66
+ fname, lname, triple = 'martin', 'fowler', 'yuki'
67
+
68
+ (1..fname.length).each do |length|
69
+ key = "Lunar:Item:Fuzzy:name:#{ encode(fname[0, length]) }"
70
+ assert Lunar.redis.smembers(key).include?('1001')
71
+ end
72
+
73
+ (1..lname.length).each do |length|
74
+ key = "Lunar:Item:Fuzzy:name:#{ encode(lname[0, length]) }"
75
+ assert Lunar.redis.smembers(key).include?('1001')
76
+ end
77
+
78
+ (1..triple.length).each do |length|
79
+ key = "Lunar:Item:Fuzzy:name:#{ encode(triple[0, length]) }"
80
+ assert Lunar.redis.smembers(key).include?('1001')
81
+ end
82
+ end
83
+ end
84
+
85
+ context "on delete" do
86
+ setup do
87
+ @index = Lunar::Index.create 'Item' do |i|
88
+ i.key 1001
89
+ i.fuzzy :name, 'Yukihiro Matsumoto'
90
+ end
91
+
92
+ Lunar::Index.delete('Item', 1001)
93
+ end
94
+
95
+ should "remove all fuzzy entries for Yukihiro Matsumoto" do
96
+ fname, lname = 'yukihiro', 'matsumoto'
97
+
98
+ (0..fname.length).each do |length|
99
+ key = "Lunar:Item:Fuzzy:name:#{ encode(fname[0, length]) }"
100
+ assert ! Lunar.redis.smembers(key).include?('1001')
101
+ end
102
+
103
+ (0..lname.length).each do |length|
104
+ key = "Lunar:Item:Fuzzy:name:#{ encode(lname[0, length]) }"
105
+ assert ! Lunar.redis.smembers(key).include?('1001')
106
+ end
107
+ end
108
+
109
+ should "also remove the key Lunar:Item:Fuzzy:1001:name" do
110
+ assert ! Lunar.redis.exists("Lunar:Item:Fuzzy:1001:name")
111
+ end
112
+ end
113
+
114
+ protected
115
+ def encode(str)
116
+ Lunar.encode(str)
117
+ end
118
+ end
@@ -0,0 +1,14 @@
1
+ require "helper"
2
+
3
+ class LunarFuzzyWordTest < Test::Unit::TestCase
4
+ context "the word 'dictionary'" do
5
+ setup do
6
+ @w = Lunar::FuzzyWord.new('dictionary')
7
+ end
8
+
9
+ should "have d, di, ... dictionary as it's partials" do
10
+ assert_equal ['d', 'di', 'dic', 'dict', 'dicti', 'dictio',
11
+ 'diction', 'dictiona', 'dictionar', 'dictionary'], @w.partials
12
+ end
13
+ end
14
+ end
@@ -1,6 +1,11 @@
1
1
  require "helper"
2
2
 
3
3
  class LunarIndexTest < Test::Unit::TestCase
4
+ setup do
5
+ Lunar.redis(Redis.new(:host => '127.0.0.1', :port => '6380'))
6
+ Lunar.redis.flushdb
7
+ end
8
+
4
9
  context "given Item" do
5
10
  setup do
6
11
  @index = Lunar::Index.new('Item')
@@ -32,9 +37,6 @@ class LunarIndexTest < Test::Unit::TestCase
32
37
 
33
38
  context "on create" do
34
39
  setup do
35
- Lunar.redis(Redis.new(:host => '127.0.0.1', :port => '6380'))
36
- Lunar.redis.flushdb
37
-
38
40
  @index = Lunar::Index.create 'Item' do |i|
39
41
  i.key 1001
40
42
  i.attr :name, 'iphone 3G'
@@ -82,9 +84,6 @@ class LunarIndexTest < Test::Unit::TestCase
82
84
 
83
85
  context "when creating an index that already exists" do
84
86
  setup do
85
- Lunar.redis(Redis.new(:host => '127.0.0.1', :port => '6380'))
86
- Lunar.redis.flushdb
87
-
88
87
  @index = Lunar::Index.create 'Item' do |i|
89
88
  i.key 1001
90
89
  i.attr :name, 'iphone 3G'
@@ -135,9 +134,6 @@ class LunarIndexTest < Test::Unit::TestCase
135
134
 
136
135
  context "on delete" do
137
136
  setup do
138
- Lunar.redis(Redis.new(:host => '127.0.0.1', :port => '6380'))
139
- Lunar.redis.flushdb
140
-
141
137
  @index = Lunar::Index.create 'Item' do |i|
142
138
  i.key 1001
143
139
  i.attr :name, 'iphone 3G'
@@ -166,9 +162,6 @@ class LunarIndexTest < Test::Unit::TestCase
166
162
 
167
163
  context "on create of an index with numeric scores" do
168
164
  setup do
169
- Lunar.redis(Redis.new(:host => '127.0.0.1', :port => '6380'))
170
- Lunar.redis.flushdb
171
-
172
165
  @index = Lunar::Index.create 'Item' do |i|
173
166
  i.key 1001
174
167
  i.integer :cost, 2700
@@ -6,7 +6,19 @@ class Item < Struct.new(:id)
6
6
  end
7
7
  end
8
8
 
9
+ class Person < Struct.new(:id)
10
+ def self.[](id)
11
+ new(id)
12
+ end
13
+ end
14
+
15
+
16
+
9
17
  class LunarSearchTest < Test::Unit::TestCase
18
+ setup do
19
+ Lunar.redis.flushdb
20
+ end
21
+
10
22
  context "searching when there exists no index yet" do
11
23
  should "return an empty set" do
12
24
  items = Lunar.search(Item, "foobar")
@@ -155,4 +167,89 @@ class LunarSearchTest < Test::Unit::TestCase
155
167
  assert items.map(&:id).include?('1003')
156
168
  end
157
169
  end
170
+
171
+ context "given Martin Fowler, Chad Fowler, and Frank Macallen" do
172
+ setup do
173
+ Lunar::Index.create "Person" do |i|
174
+ i.key 1001
175
+ i.fuzzy :name, 'Martin Fowler'
176
+ end
177
+
178
+ Lunar::Index.create "Person" do |i|
179
+ i.key 1002
180
+ i.fuzzy :name, 'Chad Fowler'
181
+ end
182
+
183
+ Lunar::Index.create "Person" do |i|
184
+ i.key 1003
185
+ i.fuzzy :name, 'Frank Macallen'
186
+ end
187
+ end
188
+
189
+ should "return Martin and Frank when searching M, and Ma" do
190
+ res1 = Lunar.search(Person, :fuzzy => { :name => 'M' })
191
+ res2 = Lunar.search(Person, :fuzzy => { :name => 'Ma' })
192
+
193
+ assert_equal %w{1001 1003}, res1.map(&:id)
194
+ assert_equal %w{1001 1003}, res2.map(&:id)
195
+ end
196
+
197
+ should "return only Martin when searching Mar up to Martin" do
198
+ res1 = Lunar.search(Person, :fuzzy => { :name => 'Mar' })
199
+ res2 = Lunar.search(Person, :fuzzy => { :name => 'Mart' })
200
+ res3 = Lunar.search(Person, :fuzzy => { :name => 'Marti' })
201
+ res4 = Lunar.search(Person, :fuzzy => { :name => 'Martin' })
202
+
203
+ assert_equal %w{1001}, res1.map(&:id)
204
+ assert_equal %w{1001}, res2.map(&:id)
205
+ assert_equal %w{1001}, res3.map(&:id)
206
+ assert_equal %w{1001}, res4.map(&:id)
207
+ end
208
+
209
+ should "return only Frank when searching Mac up to Macallen" do
210
+ res1 = Lunar.search(Person, :fuzzy => { :name => 'Mac' })
211
+ res2 = Lunar.search(Person, :fuzzy => { :name => 'Maca' })
212
+ res3 = Lunar.search(Person, :fuzzy => { :name => 'Macal' })
213
+ res4 = Lunar.search(Person, :fuzzy => { :name => 'Macall' })
214
+ res5 = Lunar.search(Person, :fuzzy => { :name => 'Macalle' })
215
+ res6 = Lunar.search(Person, :fuzzy => { :name => 'Macallen' })
216
+
217
+ assert_equal %w{1003}, res1.map(&:id)
218
+ assert_equal %w{1003}, res2.map(&:id)
219
+ assert_equal %w{1003}, res3.map(&:id)
220
+ assert_equal %w{1003}, res4.map(&:id)
221
+ assert_equal %w{1003}, res5.map(&:id)
222
+ assert_equal %w{1003}, res6.map(&:id)
223
+ end
224
+
225
+ should "return the three of them when searching F" do
226
+ res1 = Lunar.search(Person, :fuzzy => { :name => 'F' })
227
+
228
+ assert_equal %w{1001 1002 1003}, res1.map(&:id)
229
+ end
230
+
231
+ should "return the 2 fowlers when searching fo up to fowler" do
232
+ res1 = Lunar.search(Person, :fuzzy => { :name => 'Fo' })
233
+ res2 = Lunar.search(Person, :fuzzy => { :name => 'Fow' })
234
+ res3 = Lunar.search(Person, :fuzzy => { :name => 'Fowl' })
235
+ res4 = Lunar.search(Person, :fuzzy => { :name => 'Fowle' })
236
+ res5 = Lunar.search(Person, :fuzzy => { :name => 'Fowler' })
237
+
238
+ assert_equal %w{1001 1002}, res1.map(&:id)
239
+ assert_equal %w{1001 1002}, res2.map(&:id)
240
+ assert_equal %w{1001 1002}, res3.map(&:id)
241
+ assert_equal %w{1001 1002}, res4.map(&:id)
242
+ assert_equal %w{1001 1002}, res5.map(&:id)
243
+ end
244
+
245
+ should "return be able to expire the stored union" do
246
+ Lunar.stubs(:ttl).returns(1)
247
+ search = Lunar::Search.new(Person, :fuzzy => { :name => 'Fo' })
248
+ search.results
249
+
250
+ assert Lunar.redis.exists search.send(:dist_key)
251
+ sleep 2
252
+ assert ! Lunar.redis.exists(search.send(:dist_key))
253
+ end
254
+ end
158
255
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 2
8
7
  - 3
9
- version: 0.2.3
8
+ - 0
9
+ version: 0.3.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Cyril David
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-05-03 00:00:00 +08:00
17
+ date: 2010-05-06 00:00:00 +08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -29,6 +29,18 @@ dependencies:
29
29
  version: "0"
30
30
  type: :development
31
31
  version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: mocha
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :development
43
+ version_requirements: *id002
32
44
  description: uses sorted sets and sets, sorting by score
33
45
  email: cyx.ucron@gmail.com
34
46
  executables: []
@@ -47,6 +59,7 @@ files:
47
59
  - VERSION
48
60
  - examples/ohm.rb
49
61
  - lib/lunar.rb
62
+ - lib/lunar/fuzzy_word.rb
50
63
  - lib/lunar/index.rb
51
64
  - lib/lunar/result_set.rb
52
65
  - lib/lunar/scoring.rb
@@ -56,6 +69,8 @@ files:
56
69
  - lunar.gemspec
57
70
  - test/helper.rb
58
71
  - test/test_lunar.rb
72
+ - test/test_lunar_fuzzy.rb
73
+ - test/test_lunar_fuzzy_word.rb
59
74
  - test/test_lunar_index.rb
60
75
  - test/test_lunar_scoring.rb
61
76
  - test/test_lunar_search.rb
@@ -126,6 +141,8 @@ summary: a minimalistic full text search implementation in redis
126
141
  test_files:
127
142
  - test/helper.rb
128
143
  - test/test_lunar.rb
144
+ - test/test_lunar_fuzzy.rb
145
+ - test/test_lunar_fuzzy_word.rb
129
146
  - test/test_lunar_index.rb
130
147
  - test/test_lunar_scoring.rb
131
148
  - test/test_lunar_search.rb