lunar 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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.1
1
+ 0.2.0
data/examples/ohm.rb CHANGED
@@ -35,3 +35,5 @@ Lunar.search(Item, :name => "iphone", :description => "mobile")
35
35
  # Or using the pagination gem with this:
36
36
  @items = Lunar.search(Item, "iphone")
37
37
  paginate @items, :per_page => 10, :page => 1
38
+
39
+ "Lunar:Item:cost:2300"
data/lib/lunar.rb CHANGED
@@ -3,7 +3,6 @@ require File.join(File.dirname(__FILE__), '..', 'vendor', 'nest', 'nest')
3
3
 
4
4
  module Lunar
5
5
  autoload :Scoring, 'lunar/scoring'
6
- autoload :Doc, 'lunar/doc'
7
6
  autoload :Index, 'lunar/index'
8
7
  autoload :Words, 'lunar/words'
9
8
  autoload :Search, 'lunar/search'
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 delete
54
- keys = @redis.keys @ns[key]['*']
55
- keys.each do |k|
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
- class Sets < Array
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.1.1"
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.rdoc"
17
+ "README.md"
18
18
  ]
19
19
  s.files = [
20
20
  ".document",
21
21
  ".gitignore",
22
22
  "LICENSE",
23
- "README.rdoc",
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",
@@ -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)
@@ -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
- - 1
8
- - 1
9
- version: 0.1.1
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.rdoc
40
+ - README.md
41
41
  files:
42
42
  - .document
43
43
  - .gitignore
44
44
  - LICENSE
45
- - README.rdoc
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
@@ -1,13 +0,0 @@
1
- module Lunar
2
- class Doc
3
- def initialize(attrs)
4
- @attrs = attrs
5
- end
6
-
7
- def attr_scores
8
- @attrs.inject(Hash.new([])) { |hash, (k, v)|
9
- (hash[k.to_sym] = Scoring.new(v).scores) && hash
10
- }
11
- end
12
- end
13
- end
@@ -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