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 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