lunar 0.4.1 → 0.5.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.
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