lunar 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|