lunar 0.2.3 → 0.3.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/.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