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
data/.gitignore CHANGED
@@ -19,4 +19,5 @@ rdoc
19
19
  pkg
20
20
 
21
21
  ## PROJECT::SPECIFIC
22
- .rvmrc
22
+ /.yardoc
23
+ /doc
data/LICENSE CHANGED
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
17
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
18
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
19
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,116 @@
1
+ Lunar: full text searching on top of Redis
2
+ ==========================================
3
+
4
+ But why?
5
+ --------
6
+ We have been using Redis as our datastore exclusively for a lot of projects.
7
+ But searching is still something that is required by most projects. Given
8
+ those requirements and what we have, we could:
9
+
10
+ 1. Use SOLR and install it / manage it on every application we build.
11
+ 2. Use Sphinx (mind you we don't have MySQL running in our server too).
12
+ 3. Build our own (this appears to be something prevalent nowadays :D)
13
+
14
+ Sphinx vs Lunar vs SOLR
15
+ -----------------------
16
+ SOLR is definitely the way to go if you have heavyweight searching requirements.
17
+ Sphinx on the other hand, is probably an even match against Lunar.
18
+
19
+ Features
20
+ --------
21
+
22
+ 1. Full text search matching using metaphones.
23
+ 2. Ability to search by a specific field.
24
+ 3. Range matching for `number`s.
25
+ 4. Fuzzy matching for address book style autocompletion needs.
26
+
27
+ Probably the most unique thing about Lunar is its Fuzzy matching
28
+ capability. SOLR and Sphinx don't do this out of the box.
29
+
30
+ Example
31
+ -------
32
+ Given you want to index a document with a `namespace` Gadget.
33
+
34
+ Lunar.index Gadget do |i|
35
+ i.text :name, 'iphone 3gs'
36
+ i.text :tags, 'mobile apple smartphone'
37
+
38
+ i.number :price, 200
39
+ i.number :rating, 25.5
40
+
41
+ i.sortable :votes, 50
42
+ end
43
+
44
+ # with this declaration, you can now search:
45
+ Lunar.search Gadget, :q => 'iphone'
46
+ Lunar.search Gadget, :name => 'iphone', :tags => 'mobile'
47
+ Lunar.search Gadget, :price => 150..250
48
+
49
+ # If you need fuzzy matching you can also do that:
50
+ Lunar.index Customer do |i|
51
+ i.id 1001
52
+ i.fuzzy :name, "Abraham Lincoln"
53
+ end
54
+
55
+ Lunar.index Customer do |i|
56
+ i.id 1002
57
+ i.fuzzy :name, "Barack Obama"
58
+ end
59
+
60
+ Lunar.search Customer, :fuzzy => { :name => "A" }
61
+ # returns [Customer[1001]]
62
+
63
+ Lunar.search Customer, :fuzzy => { :name => "B" }
64
+ # returns [Customer[1002]]
65
+
66
+ # for sorting, you can do it on the `ResultSet` returned:
67
+ results = Lunar.search Gadget :q => 'iphone', :tags => 'mobile'
68
+ results.sort(:by => :votes, :order => "ASC")
69
+ results.sort(:by => :votes, :order => "DESC")
70
+
71
+ # this is also compatible with the pagination gem of course:
72
+ # let's say in our sinatra handler we do something like:
73
+
74
+ get '/gadget/search' do
75
+ @gadgets = paginate(Lunar.search(Gadget, :q => params[:q]),
76
+ :per_page => 10, :page => params[:page],
77
+ :sort_by => :votes, :order => 'DESC'
78
+ )
79
+ end
80
+
81
+ # see http://github.com/sinefunc/pagination for more info.
82
+
83
+ Under the Hood?
84
+ ---------------
85
+ A quick rundown of what happens when we do fulltext indexing:
86
+
87
+ Lunar.index :Gadget do |i|
88
+ i.id 1001
89
+ i.text :title, "apple apple apple macbook macbook pro"
90
+ end
91
+
92
+ # Executes the ff: in redis:
93
+ #
94
+ # ZADD Lunar:Gadget:title:APL 3 1001
95
+ # ZADD Lunar:Gadget:title:MKBK 2 1001
96
+ # ZADD Lunar:Gadget:title:PR 1 1001
97
+ #
98
+ # In addition a reference of all the words are stored
99
+ # SMEMBERS Lunar:Gadget:Metaphones:1001:title
100
+ # => (APL, MKBK, PR)
101
+
102
+ Note on Patches/Pull Requests
103
+ -----------------------------
104
+
105
+ * Fork the project.
106
+ * Make your feature addition or bug fix.
107
+ * Add tests for it. This is important so I don't break it in a
108
+ future version unintentionally.
109
+ * Commit, do not mess with rakefile, version, or history.
110
+ (if you want to have your own version, that is fine but bump version in a
111
+ commit by itself I can ignore when I pull)
112
+ * Send me a pull request. Bonus points for topic branches.
113
+
114
+ ### Copyright
115
+
116
+ Copyright (c) 2010 Cyril David. See LICENSE for details.
data/Rakefile CHANGED
@@ -5,14 +5,15 @@ begin
5
5
  require 'jeweler'
6
6
  Jeweler::Tasks.new do |gem|
7
7
  gem.name = "lunar"
8
- gem.summary = %Q{a minimalistic full text search implementation in redis}
9
- gem.description = %Q{uses sorted sets and sets, sorting by score}
8
+ gem.summary = %Q{A redis based full text search engine}
9
+ gem.description = %Q{Features full text searching via metaphones, range querying for numbers, fuzzy searching and sorting based on customer fields}
10
10
  gem.email = "cyx.ucron@gmail.com"
11
11
  gem.homepage = "http://github.com/sinefunc/lunar"
12
12
  gem.authors = ["Cyril David"]
13
- gem.add_development_dependency "contest"
14
- gem.add_development_dependency "mocha"
15
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
13
+ gem.add_dependency "redis", ">= 2.0.0"
14
+ gem.add_dependency "nest", ">= 0.0.4"
15
+ gem.add_dependency "text", ">= 0"
16
+ gem.add_development_dependency "contest", ">= 0"
16
17
  end
17
18
  Jeweler::GemcutterTasks.new
18
19
  rescue LoadError
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.1
1
+ 0.5.0
data/lib/lunar.rb CHANGED
@@ -1,39 +1,127 @@
1
1
  require 'base64'
2
- require File.join(File.dirname(__FILE__), '..', 'vendor', 'nest', 'nest')
2
+ require 'redis'
3
+ require 'nest'
4
+ require 'text'
3
5
 
4
6
  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 :FuzzySets, 'lunar/sets'
11
- autoload :SortedResultSet, 'lunar/result_set'
12
- autoload :UnsortedResultSet, 'lunar/result_set'
13
- autoload :FuzzyWord, 'lunar/fuzzy_word'
14
-
15
- def self.ttl
16
- @ttl ||= 30
7
+ VERSION = '0.5.0'
8
+
9
+ autoload :Connection, "lunar/connection"
10
+ autoload :LunarNest, "lunar/lunar_nest"
11
+ autoload :Index, "lunar/index"
12
+ autoload :Scoring, "lunar/scoring"
13
+ autoload :Words, "lunar/words"
14
+ autoload :Stopwords, "lunar/stopwords"
15
+ autoload :FuzzyWord, "lunar/fuzzy_word"
16
+ autoload :KeywordMatches, "lunar/keyword_matches"
17
+ autoload :RangeMatches, "lunar/range_matches"
18
+ autoload :FuzzyMatches, "lunar/fuzzy_matches"
19
+ autoload :ResultSet, "lunar/result_set"
20
+
21
+ extend Connection
22
+
23
+ # Index any document using a namespace. The namespace can
24
+ # be a class, or a plain Symbol/String.
25
+ #
26
+ # @example:
27
+ #
28
+ # Lunar.index Gadget do |i|
29
+ # i.text :name, 'iphone 3gs'
30
+ # i.text :tags, 'mobile apple smartphone'
31
+ #
32
+ # i.number :price, 200
33
+ # i.number :rating, 25.5
34
+ #
35
+ # i.sortable :votes, 50
36
+ # end
37
+ #
38
+ # @see Lunar::Index#initialize
39
+ # @param [String, Symbol, Class] namespace the namespace if this document.
40
+ # @yield [Lunar::Index] an instance of Lunar::Index.
41
+ # @return [Lunar::Index] returns the yielded Lunar::Index.
42
+ def self.index(namespace)
43
+ Index.new(namespace).tap { |i| yield i }
17
44
  end
18
45
 
19
- def self.ttl=(ttl)
20
- @ttl = ttl
46
+ # Delete a document identified by its namespace and id.
47
+ #
48
+ # @param [#to_s] namespace the namespace of the document to delete.
49
+ # @param [#to_s] id the id of the document to delete.
50
+ # @return [nil]
51
+ def self.delete(namespace, id)
52
+ Index.new(namespace).delete(id)
21
53
  end
22
54
 
23
- def self.redis(connection = defined?(Ohm) ? Ohm.redis : nil)
24
- @connection ||= connection
55
+ # Search for a document, scoped under a namespace.
56
+ #
57
+ # @example:
58
+ #
59
+ # Lunar.search Gadget, :q => "apple"
60
+ # # returns all gadgets with `text` apple.
61
+ #
62
+ # Lunar.search Gadget, :q => "apple", :description => "cool"
63
+ # # returns all gadgets with `text` apple and description:cool
64
+ #
65
+ # Lunar.search Gadget, :q => "phone", :price => 200..250
66
+ # # returns all gadgets with `text` phone priced between 200 to 250
67
+ #
68
+ # Lunar.search Customer, :fuzzy => { :name => "ad" }
69
+ # # returns all customers with their first / last name beginning with 'ad'
70
+ #
71
+ # Lunar.search Customer, :fuzzy => { :name => "ad" }, :age => 20..25
72
+ # # returns all customers with name starting with 'ad' aged 20 to 25.
73
+ #
74
+ # @param [#to_s] namespace search under which scope e.g. Gadget
75
+ # @param [Hash] options i.e. :q, :field1, :field2, :fuzzy
76
+ # @option options [Symbol] :q keywords e.g. `apple iphone 3g`
77
+ # @option options [Symbol] :field1 any field you indexed and a value
78
+ # @option options [Symbol] :fuzzy hash of :key => :value pairs
79
+ # @param [#to_proc] finder (optional) for cases where `Gadget[1]` isn't the
80
+ # method of finding. You can for example use an ActiveRecord model and
81
+ # pass in lambda { |id| Gadget.find(id) }.
82
+ #
83
+ # @return Lunar::ResultSet an Enumerable object.
84
+ def self.search(namespace, options, finder = lambda { |id| namespace[id] })
85
+ ns = nest[namespace]
86
+
87
+ sets =
88
+ options.map do |key, value|
89
+ if value.is_a?(Range)
90
+ RangeMatches.new(nest[namespace], key, value).distkey
91
+ elsif key == :fuzzy
92
+ value.map do |fuzzy_key, fuzzy_value|
93
+ FuzzyMatches.new(nest[namespace], fuzzy_key, fuzzy_value).distkey
94
+ end
95
+ else
96
+ KeywordMatches.new(nest[namespace], key, value).distkey
97
+ end
98
+ end
99
+
100
+ sets = sets.flatten
101
+
102
+ key =
103
+ if sets.size == 1
104
+ sets.first
105
+ else
106
+ ns[options.hash].zinterstore sets
107
+ ns[options.hash]
108
+ end
109
+
110
+ ResultSet.new(key, ns, finder)
25
111
  end
26
112
 
27
- def self.search(prefix, keywords, &block)
28
- search = Search.new(prefix, keywords)
29
- search.results(&block)
113
+ # @private internally used for determining the metaphone of a word.
114
+ def self.metaphone(word)
115
+ Text::Metaphone.metaphone(word)
30
116
  end
31
117
 
32
- def self.encode(str)
33
- Base64.encode64(str).strip
118
+ # @private abstraction of how encoding should be done for Lunar.
119
+ def self.encode(word)
120
+ Base64.encode64(word).strip
34
121
  end
35
122
 
123
+ # @private convenience method for getting a scoped Nest.
36
124
  def self.nest
37
- Nest.new(:Lunar)
125
+ LunarNest.new(:Lunar, redis)
38
126
  end
39
- end
127
+ end
@@ -0,0 +1,51 @@
1
+ module Lunar
2
+ module Connection
3
+ # Connect to a redis database.
4
+ #
5
+ # @param options [Hash] options to create a message with.
6
+ # @option options [#to_s] :host ('127.0.0.1') Host of the redis database.
7
+ # @option options [#to_s] :port (6379) Port number.
8
+ # @option options [#to_s] :db (0) Database number.
9
+ # @option options [#to_s] :timeout (0) Database timeout in seconds.
10
+ # @example Connect to a database in port 6380.
11
+ # Lunar.connect(:port => 6380)
12
+ def connect(*options)
13
+ self.redis = nil
14
+ @options = options
15
+ end
16
+
17
+ # @private Provides access to the Redis database. This is shared accross all
18
+ # models and instances.
19
+ #
20
+ # @return [Redis] an instance of Redis
21
+ def redis
22
+ threaded[:redis] ||= connection(*options)
23
+ end
24
+
25
+ # @private Set the Redis database connection
26
+ # @param [Redis] connection the redis connection
27
+ def redis=(connection)
28
+ threaded[:redis] = connection
29
+ end
30
+
31
+ private
32
+ # @private internally used for connection thread saftey
33
+ def threaded
34
+ Thread.current[:lunar] ||= {}
35
+ end
36
+
37
+ # @private Return a connection to Redis.
38
+ #
39
+ # This is a wapper around Redis.new(options)
40
+ def connection(*options)
41
+ Redis.new(*options)
42
+ end
43
+
44
+ # @private Return a connection to Redis.
45
+ #
46
+ # stores the connection options. used by Lunar::Connection::connect
47
+ def options
48
+ @options || []
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,24 @@
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 FuzzyMatches
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
+ Words.new(value).map { |w| nest[:Fuzzies][att][Lunar.encode(w)] }
22
+ end
23
+ end
24
+ end
@@ -1,7 +1,7 @@
1
1
  module Lunar
2
2
  class FuzzyWord < String
3
3
  def partials
4
- (1..length).map { |i| self[0, i] }
4
+ (1..length).map { |i| self[0, i].downcase }
5
5
  end
6
6
  end
7
- end
7
+ end
data/lib/lunar/index.rb CHANGED
@@ -1,136 +1,242 @@
1
1
  module Lunar
2
+ # Handles the management of indices for a given namespace.
3
+ # Although you may use this directly, it's much more convenient
4
+ # to use `Lunar::index` and `Lunar::delete`.
5
+ #
6
+ # @see Lunar::index
7
+ # @see Lunar::delete
2
8
  class Index
3
9
  FUZZY_MAX_LENGTH = 100
4
10
 
11
+ MissingID = Class.new(StandardError)
5
12
  FuzzyFieldTooLong = Class.new(StandardError)
6
13
 
7
- attr :ns
8
-
9
- def self.create(prefix)
10
- new(prefix).tap { |i| yield i }.index
14
+ attr :nest
15
+ attr :metaphones
16
+ attr :numbers
17
+ attr :sortables
18
+ attr :fuzzies
19
+
20
+ # This is actually wrapped by `Lunar.index` and is not inteded to be
21
+ # used directly.
22
+ # @see Lunar::index
23
+ # @param [#to_s] the namespace of the document you want to index.
24
+ # @return [Lunar::Index]
25
+ def initialize(namespace)
26
+ @nest = Lunar.nest[namespace]
27
+ @metaphones = @nest[:Metaphones]
28
+ @numbers = @nest[:Numbers]
29
+ @sortables = @nest[:Sortables]
30
+ @fuzzies = @nest[:Fuzzies]
11
31
  end
12
32
 
13
- def self.delete(prefix, key)
14
- new(prefix).tap { |i| i.key key }.delete
33
+ # Get / Set the id of the document
34
+ # @example:
35
+ #
36
+ # # Wrong usage:
37
+ # Lunar.index :Gadget do |i|
38
+ # i.text :name, 'iPad'
39
+ # end
40
+ # # => raise MissingID
41
+ #
42
+ # # Correct usage:
43
+ # Lunar.index :Gadget do |i|
44
+ # i.id 1001 # this appears before anything.
45
+ # i.text :name, 'iPad' # ok now you can set other fields.
46
+ # end
47
+ #
48
+ # The `id` is used for all other keys to define the structure of the
49
+ # keys, therefore it's imperative that you set it first.
50
+ #
51
+ # @param [#to_s] the id of the document you want to index.
52
+ # @raise [Lunar::Index::MissingID] when you access without setting it first
53
+ # @return [String] the `id` you set.
54
+ def id(value = nil)
55
+ @id = value.to_s if value
56
+ @id or raise MissingID, "In order to index a document, you need an `id`"
15
57
  end
16
58
 
17
- def initialize(prefix, redis = nil)
18
- @ns = Lunar.nest[prefix]
19
- @attrs = {}
20
- @redis = (redis || Lunar.redis)
21
- @numeric_attrs = {}
22
- @fuzzy_attrs = {}
23
- end
59
+ # Indexes all the metaphone equivalents of the words
60
+ # in value except for words included in Stopwords.
61
+ #
62
+ # @example
63
+ #
64
+ # Lunar.index :Gadget do |i|
65
+ # i.id 1001
66
+ # i.text :title, "apple macbook pro"
67
+ # end
68
+ #
69
+ # # Executes the ff: in redis:
70
+ # #
71
+ # # ZADD Lunar:Gadget:title:APL 1 1001
72
+ # # ZADD Lunar:Gadget:title:MKBK 1 1001
73
+ # # ZADD Lunar:Gadget:title:PR 1 1001
74
+ # #
75
+ # # In addition a reference of all the words are stored
76
+ # # SMEMBERS Lunar:Gadget:Metaphones:1001:title
77
+ # # => (APL, MKBK, PR)
78
+ #
79
+ # @param [Symbol] att the field name in your document
80
+ # @param [String] value the content of the field name
81
+ #
82
+ # @return [Array<String>] all the metaphones added for the document.
83
+ def text(att, value)
84
+ old = metaphones[id][att].smembers
85
+ new = []
86
+
87
+ Scoring.new(value).scores.each do |word, score|
88
+ metaphone = Lunar.metaphone(word)
89
+
90
+ nest[att][metaphone].zadd(score, id)
91
+ metaphones[id][att].sadd(metaphone)
92
+ new << metaphone
93
+ end
24
94
 
25
- def key(key = nil)
26
- @key = key if key
27
- @key
28
- end
95
+ (old - new).each do |metaphone|
96
+ nest[att][metaphone].zrem(id)
97
+ metaphones[id][att].srem(metaphone)
98
+ end
29
99
 
30
- def attrs=(attrs)
31
- attrs.each { |k, v| attr k, v }
100
+ return new
32
101
  end
33
102
 
34
- def attr(field, value = nil)
35
- @attrs[field.to_sym] = value if value
36
- @attrs[field.to_sym]
103
+ # Adds a numeric index for `att` with `value`.
104
+ #
105
+ # @example
106
+ #
107
+ # Lunar.index :Gadget do |i|
108
+ # i.id 1001
109
+ # i.number :price, 200
110
+ # end
111
+ #
112
+ # # Executes the ff: in redis:
113
+ # #
114
+ # # ZADD Lunar:Gadget:price 200
115
+ #
116
+ # @param [Symbol] att the field name in your document.
117
+ # @param [Numeric] value the numeric value of `att`.
118
+ #
119
+ # @return [Boolean] whether or not the value was added
120
+ def number(att, value)
121
+ numbers[att].zadd(value, id)
37
122
  end
38
123
 
39
- def numeric_attr(field, value = nil)
40
- @numeric_attrs[field.to_sym] = value if value
41
- @numeric_attrs[field.to_sym]
124
+ # Adds a sortable index for `att` with `value`.
125
+ #
126
+ # @example
127
+ #
128
+ # class Gadget
129
+ # def self.[](id)
130
+ # # find the gadget using id here
131
+ # end
132
+ # end
133
+ #
134
+ # Lunar.index Gadget do |i|
135
+ # i.id 1001
136
+ # i.text 'apple macbook pro'
137
+ # i.sortable :votes, 50
138
+ # end
139
+ #
140
+ # Lunar.index Gadget do |i|
141
+ # i.id 1002
142
+ # i.text 'apple iphone 3g'
143
+ # i.sortable :votes, 20
144
+ # end
145
+ #
146
+ # results = Lunar.search(Gadget, :q => 'apple')
147
+ # results.sort(:by => :votes, :order => 'DESC')
148
+ # # returns [Gadget[1001], Gadget[1002]]
149
+ #
150
+ # results.sort(:by => :votes, :order => 'ASC')
151
+ # # returns [Gadget[1002], Gadget[1001]]
152
+ #
153
+ # @param [Symbol] att the field you want to have sortability.
154
+ # @param [String, Numeric] value the value of the sortable field.
155
+ #
156
+ # @return [String] the response from the redis server.
157
+ def sortable(att, value)
158
+ sortables[id][att].set(value)
42
159
  end
43
- alias :integer :numeric_attr
44
- alias :float :numeric_attr
45
160
 
46
- def fuzzy(field, value = nil)
47
- @fuzzy_attrs[field.to_sym] = value if value
48
- @fuzzy_attrs
161
+ # Deletes everything related to an existing document given its `id`.
162
+ #
163
+ # @param [#to_s] id the document's id.
164
+ # @return [nil]
165
+ def delete(existing_id)
166
+ id(existing_id)
167
+ delete_metaphones
168
+ delete_numbers
169
+ delete_sortables
170
+ delete_fuzzies
49
171
  end
50
172
 
51
- def index
52
- index_str_attrs
53
- index_fuzzy_attrs
54
- index_numeric_attrs
173
+ 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
55
178
 
56
- return self
57
- end
179
+ words = Words.new(value).uniq
58
180
 
59
- def delete
60
- keys = @redis.keys @ns[key]['*']
61
- keys.each do |k|
62
- field = k.gsub("#{ @ns[key] }:", '')
63
- words = @redis.smembers(k)
64
- words.each do |w|
65
- @redis.zrem @ns[field][Lunar.encode(w)], key
181
+ fuzzy_words_and_parts(words) do |word, parts|
182
+ parts.each do |part, encoded|
183
+ fuzzies[att][encoded].zadd(1, id)
66
184
  end
67
- @redis.del k
185
+ fuzzies[id][att].sadd word
68
186
  end
69
187
 
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
78
- end
188
+ delete_fuzzies_for(att, fuzzies[id][att].smembers - words, words)
79
189
  end
80
190
 
81
- protected
82
- def index_str_attrs
83
- @attrs.each do |field, value|
84
- scoring = Scoring.new(value)
85
- words = []
86
- scoring.scores.each do |word, score|
87
- words << word
88
- @redis.zadd @ns[field][Lunar.encode(word)], score, key
89
- @redis.sadd @ns[key][field], word
191
+ private
192
+ def delete_metaphones
193
+ metaphones[id]['*'].matches.each do |key, att|
194
+ key.smembers.each do |metaphone|
195
+ nest[att][metaphone].zrem id
90
196
  end
91
197
 
92
- unused_words = @redis.smembers(@ns[key][field]) - words
93
- unused_words.each do |word|
94
- @redis.zrem @ns[field][Lunar.encode(word)], key
95
- @redis.srem @ns[key][field], word
96
- end
198
+ key.del
97
199
  end
98
200
  end
99
201
 
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
202
+ def delete_numbers
203
+ numbers['*'].matches.each do |key, att|
204
+ numbers[att].zrem(id)
205
+ end
206
+ end
106
207
 
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
208
+ def delete_sortables
209
+ sortables[id]['*'].keys.each do |key|
210
+ Lunar.redis.del key
211
+ end
212
+ end
213
+
214
+ def delete_fuzzies
215
+ fuzzies[id]['*'].matches.each do |key, att|
216
+ delete_fuzzies_for(att, key.smembers)
217
+ key.del
116
218
  end
117
219
  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
220
+
221
+ def delete_fuzzies_for(att, words_to_delete, existing_words = [])
222
+ fuzzy_words_and_parts(words_to_delete) do |word, parts|
223
+ parts.each do |part, encoded|
224
+ next if existing_words.grep(/^#{part}/u).any?
225
+ fuzzies[att][encoded].zrem(id)
125
226
  end
126
- @redis.srem @ns[:Fuzzy][key][field], word
227
+ fuzzies[id][att].srem word
127
228
  end
128
229
  end
129
230
 
130
- def index_numeric_attrs
131
- @numeric_attrs.each do |field, value|
132
- @redis.zadd @ns[field], value, key
231
+ def fuzzy_words_and_parts(words)
232
+ words.each do |word|
233
+ partials =
234
+ FuzzyWord.new(word).partials.map do |partial|
235
+ [partial, Lunar.encode(partial)]
236
+ end
237
+
238
+ yield word, partials
133
239
  end
134
240
  end
135
241
  end
136
- end
242
+ end