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.
- data/.gitignore +2 -1
- data/LICENSE +1 -1
- data/README.markdown +116 -0
- data/Rakefile +6 -5
- data/VERSION +1 -1
- data/lib/lunar.rb +112 -24
- data/lib/lunar/connection.rb +51 -0
- data/lib/lunar/fuzzy_matches.rb +24 -0
- data/lib/lunar/fuzzy_word.rb +2 -2
- data/lib/lunar/index.rb +200 -94
- data/lib/lunar/keyword_matches.rb +32 -0
- data/lib/lunar/lunar_nest.rb +19 -0
- data/lib/lunar/range_matches.rb +28 -0
- data/lib/lunar/result_set.rb +85 -28
- data/lib/lunar/scoring.rb +4 -2
- data/lib/lunar/stopwords.rb +15 -0
- data/lib/lunar/words.rb +6 -3
- data/lunar.gemspec +31 -60
- data/test/helper.rb +4 -5
- data/test/test_fuzzy_indexing.rb +105 -0
- data/test/test_index.rb +150 -0
- data/test/test_lunar.rb +178 -1
- data/test/test_lunar_fuzzy_word.rb +4 -4
- data/test/test_lunar_nest.rb +46 -0
- data/test/{test_lunar_scoring.rb → test_scoring.rb} +5 -5
- metadata +72 -68
- data/.document +0 -5
- data/DATA +0 -41
- data/README.md +0 -80
- data/examples/ohm.rb +0 -40
- data/lib/lunar/search.rb +0 -68
- data/lib/lunar/sets.rb +0 -86
- data/test/test_lunar_fuzzy.rb +0 -118
- data/test/test_lunar_index.rb +0 -191
- data/test/test_lunar_search.rb +0 -261
- data/test/test_sets.rb +0 -48
- data/vendor/nest/nest.rb +0 -7
- data/vendor/redis/.gitignore +0 -9
- data/vendor/redis/LICENSE +0 -20
- data/vendor/redis/README.markdown +0 -120
- data/vendor/redis/Rakefile +0 -75
- data/vendor/redis/benchmarking/logging.rb +0 -62
- data/vendor/redis/benchmarking/pipeline.rb +0 -44
- data/vendor/redis/benchmarking/speed.rb +0 -21
- data/vendor/redis/benchmarking/suite.rb +0 -24
- data/vendor/redis/benchmarking/worker.rb +0 -71
- data/vendor/redis/bin/distredis +0 -33
- data/vendor/redis/examples/basic.rb +0 -15
- data/vendor/redis/examples/dist_redis.rb +0 -43
- data/vendor/redis/examples/incr-decr.rb +0 -17
- data/vendor/redis/examples/list.rb +0 -26
- data/vendor/redis/examples/pubsub.rb +0 -25
- data/vendor/redis/examples/sets.rb +0 -36
- data/vendor/redis/lib/edis.rb +0 -3
- data/vendor/redis/lib/redis.rb +0 -496
- data/vendor/redis/lib/redis/client.rb +0 -265
- data/vendor/redis/lib/redis/dist_redis.rb +0 -118
- data/vendor/redis/lib/redis/distributed.rb +0 -460
- data/vendor/redis/lib/redis/hash_ring.rb +0 -131
- data/vendor/redis/lib/redis/pipeline.rb +0 -13
- data/vendor/redis/lib/redis/raketasks.rb +0 -1
- data/vendor/redis/lib/redis/subscribe.rb +0 -79
- data/vendor/redis/profile.rb +0 -22
- data/vendor/redis/tasks/redis.tasks.rb +0 -140
- data/vendor/redis/test/db/.gitignore +0 -1
- data/vendor/redis/test/distributed_test.rb +0 -1131
- data/vendor/redis/test/redis_test.rb +0 -1134
- data/vendor/redis/test/test.conf +0 -8
- data/vendor/redis/test/test_helper.rb +0 -113
data/.gitignore
CHANGED
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{
|
9
|
-
gem.description = %Q{
|
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.
|
14
|
-
gem.
|
15
|
-
|
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.
|
1
|
+
0.5.0
|
data/lib/lunar.rb
CHANGED
@@ -1,39 +1,127 @@
|
|
1
1
|
require 'base64'
|
2
|
-
require
|
2
|
+
require 'redis'
|
3
|
+
require 'nest'
|
4
|
+
require 'text'
|
3
5
|
|
4
6
|
module Lunar
|
5
|
-
|
6
|
-
|
7
|
-
autoload :
|
8
|
-
autoload :
|
9
|
-
autoload :
|
10
|
-
autoload :
|
11
|
-
autoload :
|
12
|
-
autoload :
|
13
|
-
autoload :FuzzyWord,
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
20
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
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
|
data/lib/lunar/fuzzy_word.rb
CHANGED
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 :
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
14
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
95
|
+
(old - new).each do |metaphone|
|
96
|
+
nest[att][metaphone].zrem(id)
|
97
|
+
metaphones[id][att].srem(metaphone)
|
98
|
+
end
|
29
99
|
|
30
|
-
|
31
|
-
attrs.each { |k, v| attr k, v }
|
100
|
+
return new
|
32
101
|
end
|
33
102
|
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
57
|
-
end
|
179
|
+
words = Words.new(value).uniq
|
58
180
|
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
185
|
+
fuzzies[id][att].sadd word
|
68
186
|
end
|
69
187
|
|
70
|
-
|
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
|
-
|
82
|
-
def
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
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
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
227
|
+
fuzzies[id][att].srem word
|
127
228
|
end
|
128
229
|
end
|
129
230
|
|
130
|
-
def
|
131
|
-
|
132
|
-
|
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
|