lunar 0.1.1 → 0.2.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/README.md +55 -0
- data/VERSION +1 -1
- data/examples/ohm.rb +2 -0
- data/lib/lunar.rb +0 -1
- data/lib/lunar/index.rb +32 -11
- data/lib/lunar/search.rb +0 -8
- data/lib/lunar/sets.rb +41 -2
- data/lunar.gemspec +3 -6
- data/test/test_lunar_index.rb +23 -0
- data/test/test_lunar_search.rb +33 -0
- metadata +5 -8
- data/README.rdoc +0 -17
- data/lib/lunar/doc.rb +0 -13
- data/test/test_lunar_document.rb +0 -20
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
Lunar
|
2
|
+
=====
|
3
|
+
|
4
|
+
A minimalistic search index implemented using Redis, taking advantage of its
|
5
|
+
powerful `sorted sets` and the ability to do set intersection and union.
|
6
|
+
|
7
|
+
Results are sorted by their word score, which is stored as the score in the
|
8
|
+
Redis sorted set.
|
9
|
+
|
10
|
+
Examples
|
11
|
+
--------
|
12
|
+
|
13
|
+
class Item < Ohm::Model
|
14
|
+
attribute :name
|
15
|
+
attribute :description
|
16
|
+
|
17
|
+
protected
|
18
|
+
def write
|
19
|
+
super
|
20
|
+
index
|
21
|
+
end
|
22
|
+
|
23
|
+
def index
|
24
|
+
Lunar::Index.create 'Item' do |i|
|
25
|
+
i.key id
|
26
|
+
i.attr :name, name
|
27
|
+
i.attr :description, description
|
28
|
+
end
|
29
|
+
|
30
|
+
# You can also do this, no problem
|
31
|
+
Lunar::Index.create Item do |i|
|
32
|
+
i.key id
|
33
|
+
i.attr :name, name
|
34
|
+
i.attr :description, description
|
35
|
+
end
|
36
|
+
|
37
|
+
# Or to avoid name ties...
|
38
|
+
Lunar::Index.create self.class do |i|
|
39
|
+
i.key id
|
40
|
+
i.attr :name, name
|
41
|
+
i.attr :description, description
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Searching...
|
47
|
+
# You can just straight out search keywords
|
48
|
+
Lunar.search(Item, "iphone")
|
49
|
+
|
50
|
+
# Or opt to filter by field
|
51
|
+
Lunar.search(Item, :name => "iphone", :description => "mobile")
|
52
|
+
|
53
|
+
# Or using the pagination gem with this:
|
54
|
+
@items = Lunar.search(Item, "iphone")
|
55
|
+
paginate @items, :per_page => 10, :page => 1
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/examples/ohm.rb
CHANGED
data/lib/lunar.rb
CHANGED
data/lib/lunar/index.rb
CHANGED
@@ -14,6 +14,7 @@ module Lunar
|
|
14
14
|
@ns = Lunar.nest[prefix]
|
15
15
|
@attrs = {}
|
16
16
|
@redis = (redis || Lunar.redis)
|
17
|
+
@numeric_attrs = {}
|
17
18
|
end
|
18
19
|
|
19
20
|
def key(key = nil)
|
@@ -30,7 +31,34 @@ module Lunar
|
|
30
31
|
@attrs[field.to_sym]
|
31
32
|
end
|
32
33
|
|
34
|
+
def numeric_attr(field, value = nil)
|
35
|
+
@numeric_attrs[field.to_sym] = value if value
|
36
|
+
@numeric_attrs[field.to_sym]
|
37
|
+
end
|
38
|
+
alias :integer :numeric_attr
|
39
|
+
alias :float :numeric_attr
|
40
|
+
|
33
41
|
def index
|
42
|
+
index_str_attrs
|
43
|
+
index_numeric_attrs
|
44
|
+
|
45
|
+
return self
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete
|
49
|
+
keys = @redis.keys @ns[key]['*']
|
50
|
+
keys.each do |k|
|
51
|
+
field = k.gsub("#{ @ns[key] }:", '')
|
52
|
+
words = @redis.smembers(k)
|
53
|
+
words.each do |w|
|
54
|
+
@redis.zrem @ns[field][Lunar.encode(w)], key
|
55
|
+
end
|
56
|
+
@redis.del k
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
def index_str_attrs
|
34
62
|
@attrs.each do |field, value|
|
35
63
|
scoring = Scoring.new(value)
|
36
64
|
words = []
|
@@ -46,20 +74,13 @@ module Lunar
|
|
46
74
|
@redis.srem @ns[key][field], word
|
47
75
|
end
|
48
76
|
end
|
49
|
-
|
50
|
-
return self
|
51
77
|
end
|
52
78
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
field = k.gsub("#{ @ns[key] }:", '')
|
57
|
-
words = @redis.smembers(k)
|
58
|
-
words.each do |w|
|
59
|
-
@redis.zrem @ns[field][Lunar.encode(w)], key
|
60
|
-
end
|
61
|
-
@redis.del k
|
79
|
+
def index_numeric_attrs
|
80
|
+
@numeric_attrs.each do |field, value|
|
81
|
+
@redis.zadd @ns[field], value, key
|
62
82
|
end
|
63
83
|
end
|
84
|
+
|
64
85
|
end
|
65
86
|
end
|
data/lib/lunar/search.rb
CHANGED
@@ -25,14 +25,6 @@ module Lunar
|
|
25
25
|
else
|
26
26
|
ResultSet.new(dist_key, &@finder)
|
27
27
|
end
|
28
|
-
|
29
|
-
# ids = Lunar.redis.zrevrange dist_key, start, finish
|
30
|
-
|
31
|
-
# if block_given?
|
32
|
-
# ids.map { |e| block.call(e) }
|
33
|
-
# else
|
34
|
-
# ids.map { |e| @@finder.call(prefix, e) }
|
35
|
-
# end
|
36
28
|
end
|
37
29
|
|
38
30
|
protected
|
data/lib/lunar/sets.rb
CHANGED
@@ -1,11 +1,50 @@
|
|
1
1
|
module Lunar
|
2
|
-
|
2
|
+
module Sets
|
3
|
+
def self.new(prefix, keywords, field = '*')
|
4
|
+
case keywords
|
5
|
+
when String
|
6
|
+
KeywordSets.new(prefix, keywords, field)
|
7
|
+
when Range
|
8
|
+
RangeSets.new(prefix, keywords, field)
|
9
|
+
else
|
10
|
+
raise TypeError, ":keywords should only be a String or Range"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class RangeSets < Array
|
16
|
+
attr :prefix, :field, :range
|
17
|
+
|
18
|
+
def initialize(prefix, range, field)
|
19
|
+
@prefix = prefix
|
20
|
+
@field = field
|
21
|
+
@range = range
|
22
|
+
|
23
|
+
super [write_and_retrieve_key]
|
24
|
+
end
|
25
|
+
|
26
|
+
def write_and_retrieve_key
|
27
|
+
zrange = Lunar.redis.zrangebyscore(Lunar.nest[prefix][field],
|
28
|
+
@range.first, @range.last)
|
29
|
+
zrange.each do |id|
|
30
|
+
Lunar.redis.zadd key, 1, id
|
31
|
+
end
|
32
|
+
# TODO :expire the key in X seconds where X is customizable
|
33
|
+
key.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def key
|
37
|
+
@key ||= Lunar.nest[prefix][field]["#{ range.first }_TO_#{ range.last }"]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class KeywordSets < Array
|
3
42
|
attr :prefix, :words, :field
|
4
43
|
|
5
44
|
def initialize(prefix, keywords, field = '*')
|
6
45
|
@prefix = prefix
|
7
|
-
@words = Words.new(keywords)
|
8
46
|
@field = field
|
47
|
+
@words = Words.new(keywords)
|
9
48
|
|
10
49
|
super(redis_set_keys)
|
11
50
|
end
|
data/lunar.gemspec
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{lunar}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.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"]
|
@@ -14,18 +14,17 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.email = %q{cyx.ucron@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE",
|
17
|
-
"README.
|
17
|
+
"README.md"
|
18
18
|
]
|
19
19
|
s.files = [
|
20
20
|
".document",
|
21
21
|
".gitignore",
|
22
22
|
"LICENSE",
|
23
|
-
"README.
|
23
|
+
"README.md",
|
24
24
|
"Rakefile",
|
25
25
|
"VERSION",
|
26
26
|
"examples/ohm.rb",
|
27
27
|
"lib/lunar.rb",
|
28
|
-
"lib/lunar/doc.rb",
|
29
28
|
"lib/lunar/index.rb",
|
30
29
|
"lib/lunar/result_set.rb",
|
31
30
|
"lib/lunar/scoring.rb",
|
@@ -35,7 +34,6 @@ Gem::Specification.new do |s|
|
|
35
34
|
"lunar.gemspec",
|
36
35
|
"test/helper.rb",
|
37
36
|
"test/test_lunar.rb",
|
38
|
-
"test/test_lunar_document.rb",
|
39
37
|
"test/test_lunar_index.rb",
|
40
38
|
"test/test_lunar_scoring.rb",
|
41
39
|
"test/test_lunar_search.rb",
|
@@ -82,7 +80,6 @@ Gem::Specification.new do |s|
|
|
82
80
|
s.test_files = [
|
83
81
|
"test/helper.rb",
|
84
82
|
"test/test_lunar.rb",
|
85
|
-
"test/test_lunar_document.rb",
|
86
83
|
"test/test_lunar_index.rb",
|
87
84
|
"test/test_lunar_scoring.rb",
|
88
85
|
"test/test_lunar_search.rb",
|
data/test/test_lunar_index.rb
CHANGED
@@ -164,6 +164,29 @@ class LunarIndexTest < Test::Unit::TestCase
|
|
164
164
|
end
|
165
165
|
end
|
166
166
|
|
167
|
+
context "on create of an index with numeric scores" do
|
168
|
+
setup do
|
169
|
+
Lunar.redis(Redis.new(:host => '127.0.0.1', :port => '6380'))
|
170
|
+
Lunar.redis.flushdb
|
171
|
+
|
172
|
+
@index = Lunar::Index.create 'Item' do |i|
|
173
|
+
i.key 1001
|
174
|
+
i.integer :cost, 2700
|
175
|
+
i.float :voting_quotient, 35.5
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
should "store Lunar:Item:cost 1001 2700" do
|
180
|
+
assert_equal "2700", zscore("Lunar:Item:cost", 1001)
|
181
|
+
end
|
182
|
+
|
183
|
+
should "store Lunar:Item:voting_quotient 1001 35.5" do
|
184
|
+
assert_equal "35.5", zscore("Lunar:Item:voting_quotient", 1001)
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
end
|
189
|
+
|
167
190
|
protected
|
168
191
|
def encode(str)
|
169
192
|
Lunar.encode(str)
|
data/test/test_lunar_search.rb
CHANGED
@@ -19,6 +19,15 @@ class LunarSearchTest < Test::Unit::TestCase
|
|
19
19
|
i.attr :name, 'apple macbook pro 17"'
|
20
20
|
i.attr :desc, 'iphone'
|
21
21
|
end
|
22
|
+
|
23
|
+
Lunar::Index.create "Item" do |i|
|
24
|
+
i.key 1003
|
25
|
+
i.attr :name, 'apple macbook pro 17"'
|
26
|
+
i.attr :desc, 'iphone'
|
27
|
+
i.integer :cost, '2300'
|
28
|
+
i.float :voting_quotient, '35.5'
|
29
|
+
end
|
30
|
+
|
22
31
|
end
|
23
32
|
|
24
33
|
should "be able to search by keyword iphone" do
|
@@ -73,5 +82,29 @@ class LunarSearchTest < Test::Unit::TestCase
|
|
73
82
|
assert items.map(&:id).include?('1002')
|
74
83
|
end
|
75
84
|
|
85
|
+
should "be able to search by cost range: 2000..2700" do
|
86
|
+
items = Lunar.search(Item, :cost => 2000..2700)
|
87
|
+
assert items.map(&:id).include?('1003')
|
88
|
+
end
|
89
|
+
|
90
|
+
should "be able to exclude by cost range: 2000..2200" do
|
91
|
+
items = Lunar.search(Item, :cost => 2000..2200)
|
92
|
+
assert ! items.map(&:id).include?('1003')
|
93
|
+
end
|
94
|
+
|
95
|
+
should "be able to search by range of cost and name" do
|
96
|
+
items = Lunar.search(Item, :cost => 2000..2300, :name => 'apple')
|
97
|
+
assert items.map(&:id).include?('1003')
|
98
|
+
end
|
99
|
+
|
100
|
+
should "be able to search by range of cost and exclude by name" do
|
101
|
+
items = Lunar.search(Item, :cost => 2000..2200, :name => 'foobar')
|
102
|
+
assert ! items.map(&:id).include?('1003')
|
103
|
+
end
|
104
|
+
|
105
|
+
should "be able to search by voting_quotient" do
|
106
|
+
items = Lunar.search(Item, :voting_quotient => 35..40)
|
107
|
+
assert items.map(&:id).include?('1003')
|
108
|
+
end
|
76
109
|
end
|
77
110
|
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 0.2.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Cyril David
|
@@ -37,17 +37,16 @@ extensions: []
|
|
37
37
|
|
38
38
|
extra_rdoc_files:
|
39
39
|
- LICENSE
|
40
|
-
- README.
|
40
|
+
- README.md
|
41
41
|
files:
|
42
42
|
- .document
|
43
43
|
- .gitignore
|
44
44
|
- LICENSE
|
45
|
-
- README.
|
45
|
+
- README.md
|
46
46
|
- Rakefile
|
47
47
|
- VERSION
|
48
48
|
- examples/ohm.rb
|
49
49
|
- lib/lunar.rb
|
50
|
-
- lib/lunar/doc.rb
|
51
50
|
- lib/lunar/index.rb
|
52
51
|
- lib/lunar/result_set.rb
|
53
52
|
- lib/lunar/scoring.rb
|
@@ -57,7 +56,6 @@ files:
|
|
57
56
|
- lunar.gemspec
|
58
57
|
- test/helper.rb
|
59
58
|
- test/test_lunar.rb
|
60
|
-
- test/test_lunar_document.rb
|
61
59
|
- test/test_lunar_index.rb
|
62
60
|
- test/test_lunar_scoring.rb
|
63
61
|
- test/test_lunar_search.rb
|
@@ -128,7 +126,6 @@ summary: a minimalistic full text search implementation in redis
|
|
128
126
|
test_files:
|
129
127
|
- test/helper.rb
|
130
128
|
- test/test_lunar.rb
|
131
|
-
- test/test_lunar_document.rb
|
132
129
|
- test/test_lunar_index.rb
|
133
130
|
- test/test_lunar_scoring.rb
|
134
131
|
- test/test_lunar_search.rb
|
data/README.rdoc
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
= lunar
|
2
|
-
|
3
|
-
Description goes here.
|
4
|
-
|
5
|
-
== Note on Patches/Pull Requests
|
6
|
-
|
7
|
-
* Fork the project.
|
8
|
-
* Make your feature addition or bug fix.
|
9
|
-
* Add tests for it. This is important so I don't break it in a
|
10
|
-
future version unintentionally.
|
11
|
-
* Commit, do not mess with rakefile, version, or history.
|
12
|
-
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
-
* Send me a pull request. Bonus points for topic branches.
|
14
|
-
|
15
|
-
== Copyright
|
16
|
-
|
17
|
-
Copyright (c) 2010 Cyril David. See LICENSE for details.
|
data/lib/lunar/doc.rb
DELETED
data/test/test_lunar_document.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
require "helper"
|
2
|
-
|
3
|
-
class LunarDocTest < Test::Unit::TestCase
|
4
|
-
describe "given title => iPhone, description => apple Cellphone Mobile" do
|
5
|
-
setup do
|
6
|
-
@doc = Lunar::Doc.new(:title => 'iPhone',
|
7
|
-
:description => 'apple Cellphone Mobile')
|
8
|
-
end
|
9
|
-
|
10
|
-
should "return attr_scores with title iphone 1" do
|
11
|
-
assert_equal 1, @doc.attr_scores[:title]["iphone"]
|
12
|
-
end
|
13
|
-
|
14
|
-
should "return attr_scores with description apple cellphone mobile 1" do
|
15
|
-
assert_equal 1, @doc.attr_scores[:description]["apple"]
|
16
|
-
assert_equal 1, @doc.attr_scores[:description]["cellphone"]
|
17
|
-
assert_equal 1, @doc.attr_scores[:description]["mobile"]
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|