lunar 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/.gitignore +2 -1
  2. data/LICENSE +1 -1
  3. data/README.markdown +116 -0
  4. data/Rakefile +6 -5
  5. data/VERSION +1 -1
  6. data/lib/lunar.rb +112 -24
  7. data/lib/lunar/connection.rb +51 -0
  8. data/lib/lunar/fuzzy_matches.rb +24 -0
  9. data/lib/lunar/fuzzy_word.rb +2 -2
  10. data/lib/lunar/index.rb +200 -94
  11. data/lib/lunar/keyword_matches.rb +32 -0
  12. data/lib/lunar/lunar_nest.rb +19 -0
  13. data/lib/lunar/range_matches.rb +28 -0
  14. data/lib/lunar/result_set.rb +85 -28
  15. data/lib/lunar/scoring.rb +4 -2
  16. data/lib/lunar/stopwords.rb +15 -0
  17. data/lib/lunar/words.rb +6 -3
  18. data/lunar.gemspec +31 -60
  19. data/test/helper.rb +4 -5
  20. data/test/test_fuzzy_indexing.rb +105 -0
  21. data/test/test_index.rb +150 -0
  22. data/test/test_lunar.rb +178 -1
  23. data/test/test_lunar_fuzzy_word.rb +4 -4
  24. data/test/test_lunar_nest.rb +46 -0
  25. data/test/{test_lunar_scoring.rb → test_scoring.rb} +5 -5
  26. metadata +72 -68
  27. data/.document +0 -5
  28. data/DATA +0 -41
  29. data/README.md +0 -80
  30. data/examples/ohm.rb +0 -40
  31. data/lib/lunar/search.rb +0 -68
  32. data/lib/lunar/sets.rb +0 -86
  33. data/test/test_lunar_fuzzy.rb +0 -118
  34. data/test/test_lunar_index.rb +0 -191
  35. data/test/test_lunar_search.rb +0 -261
  36. data/test/test_sets.rb +0 -48
  37. data/vendor/nest/nest.rb +0 -7
  38. data/vendor/redis/.gitignore +0 -9
  39. data/vendor/redis/LICENSE +0 -20
  40. data/vendor/redis/README.markdown +0 -120
  41. data/vendor/redis/Rakefile +0 -75
  42. data/vendor/redis/benchmarking/logging.rb +0 -62
  43. data/vendor/redis/benchmarking/pipeline.rb +0 -44
  44. data/vendor/redis/benchmarking/speed.rb +0 -21
  45. data/vendor/redis/benchmarking/suite.rb +0 -24
  46. data/vendor/redis/benchmarking/worker.rb +0 -71
  47. data/vendor/redis/bin/distredis +0 -33
  48. data/vendor/redis/examples/basic.rb +0 -15
  49. data/vendor/redis/examples/dist_redis.rb +0 -43
  50. data/vendor/redis/examples/incr-decr.rb +0 -17
  51. data/vendor/redis/examples/list.rb +0 -26
  52. data/vendor/redis/examples/pubsub.rb +0 -25
  53. data/vendor/redis/examples/sets.rb +0 -36
  54. data/vendor/redis/lib/edis.rb +0 -3
  55. data/vendor/redis/lib/redis.rb +0 -496
  56. data/vendor/redis/lib/redis/client.rb +0 -265
  57. data/vendor/redis/lib/redis/dist_redis.rb +0 -118
  58. data/vendor/redis/lib/redis/distributed.rb +0 -460
  59. data/vendor/redis/lib/redis/hash_ring.rb +0 -131
  60. data/vendor/redis/lib/redis/pipeline.rb +0 -13
  61. data/vendor/redis/lib/redis/raketasks.rb +0 -1
  62. data/vendor/redis/lib/redis/subscribe.rb +0 -79
  63. data/vendor/redis/profile.rb +0 -22
  64. data/vendor/redis/tasks/redis.tasks.rb +0 -140
  65. data/vendor/redis/test/db/.gitignore +0 -1
  66. data/vendor/redis/test/distributed_test.rb +0 -1131
  67. data/vendor/redis/test/redis_test.rb +0 -1134
  68. data/vendor/redis/test/test.conf +0 -8
  69. data/vendor/redis/test/test_helper.rb +0 -113
@@ -0,0 +1,32 @@
1
+ module Lunar
2
+ # @private Used internally by Lunar::search to get all the word matches
3
+ # given `nest`, `att` and it's `val`.
4
+ class KeywordMatches
5
+ attr :nest
6
+ attr :att
7
+ attr :value
8
+
9
+ def initialize(nest, att, value)
10
+ @nest, @att, @value = nest, att.to_sym, value
11
+ end
12
+
13
+ def distkey
14
+ nest[{ att => value }.hash].tap do |dk|
15
+ dk.zunionstore keys.flatten
16
+ end
17
+ end
18
+
19
+ protected
20
+ def keys
21
+ if att == :q
22
+ metaphones.map { |m| nest['*'][m].keys }
23
+ else
24
+ metaphones.map { |m| nest[att][m] }
25
+ end
26
+ end
27
+
28
+ def metaphones
29
+ Words.new(value).map { |word| Lunar.metaphone(word) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,28 @@
1
+ module Lunar
2
+ # @private Used internally by Lunar::search to get all the range matches
3
+ # given `nest`, `att` and it's `val`.
4
+ class RangeMatches
5
+ MAX_RESULTS = 10_000
6
+
7
+ attr :nest
8
+ attr :att
9
+ attr :range
10
+
11
+ def initialize(nest, att, range)
12
+ @nest, @att, @range = nest, att.to_sym, range
13
+ end
14
+
15
+
16
+ def distkey
17
+ nest[{ att => range }.hash].tap do |dk|
18
+ res = zset.zrangebyscore(range.first, range.last, :limit => [0, MAX_RESULTS])
19
+ res.each { |val| dk.zadd(1, val) }
20
+ end
21
+ end
22
+
23
+ private
24
+ def zset
25
+ @sortedset ||= nest[:Numbers][att]
26
+ end
27
+ end
28
+ end
@@ -1,43 +1,100 @@
1
1
  module Lunar
2
+ # This wraps a Lunar search result set. You can do all the
3
+ # expected operations with an Enumerable.
4
+ #
5
+ # @example:
6
+ #
7
+ # results = Lunar.search(Gadget, :q => "Apple Macbook Pro")
8
+ # results.class == Lunar::ResultSet
9
+ # # => true
10
+ #
11
+ # results.kind_of?(Enumerable)
12
+ # # => true
13
+ #
14
+ # `sort` and `sort_by` commands are available and directly calls
15
+ # the Redis SORT command.
16
+ #
17
+ # results = Lunar.search(Gadget, :q => "Apple Macbook Pro")
18
+ # results.sort(:by => :name, :order => "ALPHA")
19
+ # results.sort(:by => :name, :order => "ALPHA ASC")
20
+ # results.sort(:by => :name, :order => "ALPHA DESC")
21
+ #
22
+ # @see http://code.google.com/p/redis/wiki/SortCommand
2
23
  class ResultSet
3
24
  include Enumerable
4
25
 
5
- attr :key
26
+ attr :distkey
27
+ attr :nest
28
+ attr :sortables
6
29
 
7
- def initialize(key, &block)
8
- @key = key
9
- @block = block
30
+ def initialize(distkey, nest, finder)
31
+ @distkey = distkey
32
+ @nest = nest
33
+ @finder = finder
34
+ @sortables = @nest[:Sortables]['*']
10
35
  end
11
36
 
12
- def each(&block)
13
- all.each(&block)
14
- end
15
- end
16
-
17
- class SortedResultSet < ResultSet
18
- def all(options = {})
19
- start = Integer(options[:start] || 0)
20
- limit = Integer(options[:limit] || 0)
21
- finish = start + limit - 1
22
-
23
- Lunar.redis.zrevrange(key, start, finish).map(&@block)
37
+ def each
38
+ objects(distkey.zrange(0, -1)).each { |e| yield e }
24
39
  end
25
40
 
26
- def size
27
- Lunar.redis.zcard(key)
41
+ # Provides syntatic sugar for `sort`.
42
+ #
43
+ # @example:
44
+ #
45
+ # results = Lunar.search(Gadget, :q => "apple")
46
+ # results.sort(:by => :votes)
47
+ # results.sort_by(:votes)
48
+ #
49
+ # @param [#to_s] att the field in the namespace you want to sort by.
50
+ # @param [Hash] opts the various opts to pass to Redis SORT command.
51
+ # @option opts [#to_s] :order the direction you want to sort i.e. ASC DESC ALPHA
52
+ # @option opts [Array] :limit offset and max results to return.
53
+ #
54
+ # @return [Array] Array of objects as defined by the `finder`.
55
+ # @see http://code.google.com/p/redis/wiki/SortCommand
56
+ def sort_by(att, opts = {})
57
+ sort(opts.merge(:by => att))
28
58
  end
29
- end
30
59
 
31
- class UnsortedResultSet < ResultSet
32
- def all(options = {})
33
- start = Integer(options[:start] || 0)
34
- limit = Integer(options[:limit] || 100)
35
-
36
- Lunar.redis.sort(key, :limit => [start, limit]).map(&@block)
60
+ # Gives the ability to sort the search results via a `sortable` field
61
+ # in your index.
62
+ #
63
+ # @example:
64
+ #
65
+ # Lunar.index Gadget do |i|
66
+ # i.id 1001
67
+ # i.text :title, "Apple Macbook Pro"
68
+ # i.sortable :votes, 10
69
+ # end
70
+ #
71
+ # Lunar.index Gadget do |i|
72
+ # i.id 1002
73
+ # i.text :title, "Apple iPad"
74
+ # i.sortable :votes, 50
75
+ # end
76
+ #
77
+ # results = Lunar.search(Gadget, :q => "apple")
78
+ # sorted = results.sort(:by => :votes, :order => "DESC")
79
+ #
80
+ # sorted == [Gadget[1002], Gadget[1001]]
81
+ # # => true
82
+ #
83
+ # @param [Hash] opts the various opts to pass to Redis SORT command.
84
+ # @option opts [#to_s] :by the field in the namespace you want to sort by.
85
+ # @option opts [#to_s] :order the direction you want to sort i.e. ASC DESC ALPHA
86
+ # @option opts [Array] :limit offset and max results to return.
87
+ #
88
+ # @return [Array] Array of objects as defined by the `finder`.
89
+ # @see http://code.google.com/p/redis/wiki/SortCommand
90
+ def sort(opts = {})
91
+ opts[:by] = sortables[opts[:by]] if opts[:by]
92
+ objects(distkey.sort(opts))
37
93
  end
38
94
 
39
- def size
40
- Lunar.redis.scard(key)
95
+ protected
96
+ def objects(ids)
97
+ ids.map(&@finder)
41
98
  end
42
99
  end
43
- end
100
+ end
data/lib/lunar/scoring.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  module Lunar
2
+ # @private internally used by Lunar::Index to count the words
3
+ # of a given text.
2
4
  class Scoring
3
5
  def initialize(words)
4
6
  @words = Words.new(words)
5
7
  end
6
8
 
7
9
  def scores
8
- @words.inject(Hash.new(0)) { |a, w| a[w.downcase] += 1; a }
10
+ @words.inject(Hash.new(0)) { |a, w| a[w.downcase] += 1 and a }
9
11
  end
10
12
  end
11
- end
13
+ end
@@ -0,0 +1,15 @@
1
+ module Lunar
2
+ module Stopwords
3
+ def include?(word)
4
+ stopwords.include?(word)
5
+ end
6
+
7
+ private
8
+ def stopwords
9
+ %w(an and are as at be but by for if in into is it no not of on or s
10
+ such t that the their then there these they this to was will with)
11
+ end
12
+
13
+ module_function :stopwords, :include?
14
+ end
15
+ end
data/lib/lunar/words.rb CHANGED
@@ -1,14 +1,17 @@
1
1
  require 'iconv'
2
2
 
3
3
  module Lunar
4
+ # @private Internally used to determine the words given some str.
5
+ # i.e. Words.new("the quick brown") == %w(the quick brown)
4
6
  class Words < Array
5
7
  SEPARATOR = /\s+/
6
8
 
7
9
  def initialize(str)
8
10
  words = str.split(SEPARATOR).
9
11
  reject { |w| w.to_s.strip.empty? }.
10
- map { |w| sanitize(w) }
11
-
12
+ map { |w| sanitize(w) }.
13
+ reject { |w| Stopwords.include?(w) }
14
+
12
15
  super(words)
13
16
  end
14
17
 
@@ -18,4 +21,4 @@ module Lunar
18
21
  gsub(/[^a-zA-Z0-9\-_]/, '').downcase
19
22
  end
20
23
  end
21
- end
24
+ end
data/lunar.gemspec CHANGED
@@ -5,92 +5,57 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{lunar}
8
- s.version = "0.4.1"
8
+ s.version = "0.5.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-20}
13
- s.description = %q{uses sorted sets and sets, sorting by score}
12
+ s.date = %q{2010-05-21}
13
+ s.description = %q{Features full text searching via metaphones, range querying for numbers, fuzzy searching and sorting based on customer fields}
14
14
  s.email = %q{cyx.ucron@gmail.com}
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE",
17
- "README.md"
17
+ "README.markdown"
18
18
  ]
19
19
  s.files = [
20
- ".document",
21
- ".gitignore",
22
- "DATA",
20
+ ".gitignore",
23
21
  "LICENSE",
24
- "README.md",
22
+ "README.markdown",
25
23
  "Rakefile",
26
24
  "VERSION",
27
- "examples/ohm.rb",
28
25
  "lib/lunar.rb",
26
+ "lib/lunar/connection.rb",
27
+ "lib/lunar/fuzzy_matches.rb",
29
28
  "lib/lunar/fuzzy_word.rb",
30
29
  "lib/lunar/index.rb",
30
+ "lib/lunar/keyword_matches.rb",
31
+ "lib/lunar/lunar_nest.rb",
32
+ "lib/lunar/range_matches.rb",
31
33
  "lib/lunar/result_set.rb",
32
34
  "lib/lunar/scoring.rb",
33
- "lib/lunar/search.rb",
34
- "lib/lunar/sets.rb",
35
+ "lib/lunar/stopwords.rb",
35
36
  "lib/lunar/words.rb",
36
37
  "lunar.gemspec",
37
38
  "test/helper.rb",
39
+ "test/test_fuzzy_indexing.rb",
40
+ "test/test_index.rb",
38
41
  "test/test_lunar.rb",
39
- "test/test_lunar_fuzzy.rb",
40
42
  "test/test_lunar_fuzzy_word.rb",
41
- "test/test_lunar_index.rb",
42
- "test/test_lunar_scoring.rb",
43
- "test/test_lunar_search.rb",
44
- "test/test_sets.rb",
45
- "vendor/nest/nest.rb",
46
- "vendor/redis/.gitignore",
47
- "vendor/redis/LICENSE",
48
- "vendor/redis/README.markdown",
49
- "vendor/redis/Rakefile",
50
- "vendor/redis/benchmarking/logging.rb",
51
- "vendor/redis/benchmarking/pipeline.rb",
52
- "vendor/redis/benchmarking/speed.rb",
53
- "vendor/redis/benchmarking/suite.rb",
54
- "vendor/redis/benchmarking/worker.rb",
55
- "vendor/redis/bin/distredis",
56
- "vendor/redis/examples/basic.rb",
57
- "vendor/redis/examples/dist_redis.rb",
58
- "vendor/redis/examples/incr-decr.rb",
59
- "vendor/redis/examples/list.rb",
60
- "vendor/redis/examples/pubsub.rb",
61
- "vendor/redis/examples/sets.rb",
62
- "vendor/redis/lib/edis.rb",
63
- "vendor/redis/lib/redis.rb",
64
- "vendor/redis/lib/redis/client.rb",
65
- "vendor/redis/lib/redis/dist_redis.rb",
66
- "vendor/redis/lib/redis/distributed.rb",
67
- "vendor/redis/lib/redis/hash_ring.rb",
68
- "vendor/redis/lib/redis/pipeline.rb",
69
- "vendor/redis/lib/redis/raketasks.rb",
70
- "vendor/redis/lib/redis/subscribe.rb",
71
- "vendor/redis/profile.rb",
72
- "vendor/redis/tasks/redis.tasks.rb",
73
- "vendor/redis/test/db/.gitignore",
74
- "vendor/redis/test/distributed_test.rb",
75
- "vendor/redis/test/redis_test.rb",
76
- "vendor/redis/test/test.conf",
77
- "vendor/redis/test/test_helper.rb"
43
+ "test/test_lunar_nest.rb",
44
+ "test/test_scoring.rb"
78
45
  ]
79
46
  s.homepage = %q{http://github.com/sinefunc/lunar}
80
47
  s.rdoc_options = ["--charset=UTF-8"]
81
48
  s.require_paths = ["lib"]
82
- s.rubygems_version = %q{1.3.5}
83
- s.summary = %q{a minimalistic full text search implementation in redis}
49
+ s.rubygems_version = %q{1.3.6}
50
+ s.summary = %q{A redis based full text search engine}
84
51
  s.test_files = [
85
52
  "test/helper.rb",
53
+ "test/test_fuzzy_indexing.rb",
54
+ "test/test_index.rb",
86
55
  "test/test_lunar.rb",
87
- "test/test_lunar_fuzzy.rb",
88
56
  "test/test_lunar_fuzzy_word.rb",
89
- "test/test_lunar_index.rb",
90
- "test/test_lunar_scoring.rb",
91
- "test/test_lunar_search.rb",
92
- "test/test_sets.rb",
93
- "examples/ohm.rb"
57
+ "test/test_lunar_nest.rb",
58
+ "test/test_scoring.rb"
94
59
  ]
95
60
 
96
61
  if s.respond_to? :specification_version then
@@ -98,15 +63,21 @@ Gem::Specification.new do |s|
98
63
  s.specification_version = 3
99
64
 
100
65
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
66
+ s.add_runtime_dependency(%q<redis>, [">= 2.0.0"])
67
+ s.add_runtime_dependency(%q<nest>, [">= 0.0.4"])
68
+ s.add_runtime_dependency(%q<text>, [">= 0"])
101
69
  s.add_development_dependency(%q<contest>, [">= 0"])
102
- s.add_development_dependency(%q<mocha>, [">= 0"])
103
70
  else
71
+ s.add_dependency(%q<redis>, [">= 2.0.0"])
72
+ s.add_dependency(%q<nest>, [">= 0.0.4"])
73
+ s.add_dependency(%q<text>, [">= 0"])
104
74
  s.add_dependency(%q<contest>, [">= 0"])
105
- s.add_dependency(%q<mocha>, [">= 0"])
106
75
  end
107
76
  else
77
+ s.add_dependency(%q<redis>, [">= 2.0.0"])
78
+ s.add_dependency(%q<nest>, [">= 0.0.4"])
79
+ s.add_dependency(%q<text>, [">= 0"])
108
80
  s.add_dependency(%q<contest>, [">= 0"])
109
- s.add_dependency(%q<mocha>, [">= 0"])
110
81
  end
111
82
  end
112
83
 
data/test/helper.rb CHANGED
@@ -1,14 +1,13 @@
1
1
  require 'rubygems'
2
2
  require 'test/unit'
3
3
  require 'contest'
4
- require 'mocha'
5
4
 
6
5
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
6
  $LOAD_PATH.unshift(File.dirname(__FILE__))
8
7
  require 'lunar'
9
8
 
10
- $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../vendor/redis/lib')
11
- require 'redis'
12
-
13
9
  class Test::Unit::TestCase
14
- end
10
+ def setup
11
+ Lunar.redis.flushdb
12
+ end
13
+ end
@@ -0,0 +1,105 @@
1
+ require 'helper'
2
+
3
+ class LunarFuzzyTest < Test::Unit::TestCase
4
+ context "when setting fuzzy name, 'Yukihiro Matsumoto'" do
5
+ setup do
6
+ @index = Lunar.index 'Item' do |i|
7
+ i.id 1001
8
+ i.fuzzy :name, 'Yukihiro Matsumoto'
9
+ end
10
+ end
11
+
12
+ should "store Lunar:Item:Fuzzies:name:Y up to o and M up to o" do
13
+ fname, lname = 'yukihiro', 'matsumoto'
14
+ nest = Lunar.nest[:Item][:Fuzzies][:name]
15
+
16
+ (1..fname.length).each do |length|
17
+ key = nest[encode(fname[0, length])]
18
+ assert_equal '1', key.zscore(1001)
19
+ end
20
+
21
+ (1..lname.length).each do |length|
22
+ key = nest[encode(lname[0, length])]
23
+ assert_equal '1', key.zscore(1001)
24
+ end
25
+
26
+ assert_equal %w{matsumoto yukihiro},
27
+ Lunar.nest[:Item][:Fuzzies][1001][:name].smembers.sort
28
+ end
29
+ end
30
+
31
+ context "when creating an index that already exists" do
32
+ setup do
33
+ @index = Lunar.index 'Item' do |i|
34
+ i.id 1001
35
+ i.fuzzy :name, 'Yukihiro Matsumoto'
36
+ end
37
+
38
+ @index = Lunar.index 'Item' do |i|
39
+ i.id 1001
40
+ i.fuzzy :name, 'Martin Fowler Yuki'
41
+ end
42
+ end
43
+
44
+ should "remove all fuzzy entries for Yukihiro Matsumoto" do
45
+ fname, lname = 'yukihiro', 'matsumoto'
46
+ nest = Lunar.nest[:Item][:Fuzzies][:name]
47
+
48
+ (5..fname.length).each do |length|
49
+ key = nest[encode(fname[0, length])]
50
+ assert_nil key.zscore('1001')
51
+ end
52
+
53
+ (3..lname.length).each do |length|
54
+ key = nest[encode(lname[0, length])]
55
+ assert_nil key.zscore('1001')
56
+ end
57
+
58
+ assert_equal %w{fowler martin yuki},
59
+ Lunar.nest[:Item][:Fuzzies][1001][:name].smembers.sort
60
+ end
61
+
62
+ should "store Lunar:Item:name:M up to n and F up to r etc..." do
63
+ fname, lname, triple = 'martin', 'fowler', 'yuki'
64
+ nest = Lunar.nest[:Item][:Fuzzies][:name]
65
+
66
+ %w{martin fowler yuki}.each do |word|
67
+ (1..word.length).each do |length|
68
+ key = nest[encode(word[0, length])]
69
+ assert_equal '1', key.zscore(1001)
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ context "on delete" do
76
+ setup do
77
+ @index = Lunar.index 'Item' do |i|
78
+ i.id 1001
79
+ i.fuzzy :name, 'Yukihiro Matsumoto'
80
+ end
81
+
82
+ Lunar.delete('Item', 1001)
83
+ end
84
+
85
+ should "remove all fuzzy entries for Yukihiro Matsumoto" do
86
+ nest = Lunar.nest[:Item][:Fuzzies][:name]
87
+
88
+ %w{yukihiro matsumoto}.each do |word|
89
+ (1..word.length).each do |length|
90
+ key = nest[encode(word[0, length])]
91
+ assert_nil key.zscore(1001)
92
+ end
93
+ end
94
+ end
95
+
96
+ should "also remove the key Lunar:Item:Fuzzies:1001:name" do
97
+ assert ! Lunar.redis.exists("Lunar:Item:Fuzzies:1001:name")
98
+ end
99
+ end
100
+
101
+ protected
102
+ def encode(str)
103
+ Lunar.encode(str)
104
+ end
105
+ end