lunar 0.1.0 → 0.1.1

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
data/examples/ohm.rb CHANGED
@@ -3,21 +3,35 @@ class Item < Ohm::Model
3
3
  attribute :description
4
4
 
5
5
  def index
6
- Lunar::Index.create(
7
- self.class.name,
8
- self.id,
9
- :name => name,
10
- :description => description
11
- )
12
-
13
- index = Lunar::Index.new('Item')
14
- index.attrs = { :name => name, :description => description }
15
- index.create
6
+ Lunar::Index.create 'Item' do |i|
7
+ i.key id
8
+ i.attr :name, name
9
+ i.attr :description, description
10
+ end
11
+
12
+ # You can also do this, no problem
13
+ Lunar::Index.create Item do |i|
14
+ i.key id
15
+ i.attr :name, name
16
+ i.attr :description, description
17
+ end
16
18
 
17
- Luner::Index.create 'Item' do |i|
19
+ # Or to avoid name ties...
20
+ Lunar::Index.create self.class do |i|
18
21
  i.key id
19
22
  i.attr :name, name
20
23
  i.attr :description, description
21
24
  end
22
25
  end
23
26
  end
27
+
28
+ # Searching...
29
+ # You can just straight out search keywords
30
+ Lunar.search(Item, "iphone")
31
+
32
+ # Or opt to filter by field
33
+ Lunar.search(Item, :name => "iphone", :description => "mobile")
34
+
35
+ # Or using the pagination gem with this:
36
+ @items = Lunar.search(Item, "iphone")
37
+ paginate @items, :per_page => 10, :page => 1
data/lib/lunar.rb CHANGED
@@ -2,11 +2,28 @@ require 'base64'
2
2
  require File.join(File.dirname(__FILE__), '..', 'vendor', 'nest', 'nest')
3
3
 
4
4
  module Lunar
5
- autoload :Scoring, 'lunar/scoring'
6
- autoload :Doc, 'lunar/doc'
7
- autoload :Index, 'lunar/index'
5
+ autoload :Scoring, 'lunar/scoring'
6
+ autoload :Doc, 'lunar/doc'
7
+ autoload :Index, 'lunar/index'
8
+ autoload :Words, 'lunar/words'
9
+ autoload :Search, 'lunar/search'
10
+ autoload :Sets, 'lunar/sets'
11
+ autoload :ResultSet, 'lunar/result_set'
8
12
 
9
13
  def self.redis(connection = defined?(Ohm) ? Ohm.redis : nil)
10
14
  @connection ||= connection
11
15
  end
16
+
17
+ def self.search(prefix, keywords, &block)
18
+ search = Search.new(prefix, keywords)
19
+ search.results(&block)
20
+ end
21
+
22
+ def self.encode(str)
23
+ Base64.encode64(str).strip
24
+ end
25
+
26
+ def self.nest
27
+ Nest.new(:Lunar)
28
+ end
12
29
  end
data/lib/lunar/index.rb CHANGED
@@ -11,7 +11,7 @@ module Lunar
11
11
  end
12
12
 
13
13
  def initialize(prefix, redis = nil)
14
- @ns = Nest.new(:Lunar)[prefix]
14
+ @ns = Lunar.nest[prefix]
15
15
  @attrs = {}
16
16
  @redis = (redis || Lunar.redis)
17
17
  end
@@ -36,13 +36,13 @@ module Lunar
36
36
  words = []
37
37
  scoring.scores.each do |word, score|
38
38
  words << word
39
- @redis.zadd @ns[field][encode(word)], score, key
39
+ @redis.zadd @ns[field][Lunar.encode(word)], score, key
40
40
  @redis.sadd @ns[key][field], word
41
41
  end
42
42
 
43
43
  unused_words = @redis.smembers(@ns[key][field]) - words
44
44
  unused_words.each do |word|
45
- @redis.zrem @ns[field][encode(word)], key
45
+ @redis.zrem @ns[field][Lunar.encode(word)], key
46
46
  @redis.srem @ns[key][field], word
47
47
  end
48
48
  end
@@ -56,15 +56,10 @@ module Lunar
56
56
  field = k.gsub("#{ @ns[key] }:", '')
57
57
  words = @redis.smembers(k)
58
58
  words.each do |w|
59
- @redis.zrem @ns[field][encode(w)], key
59
+ @redis.zrem @ns[field][Lunar.encode(w)], key
60
60
  end
61
61
  @redis.del k
62
62
  end
63
63
  end
64
-
65
- private
66
- def encode(str)
67
- Base64.encode64(str).strip
68
- end
69
64
  end
70
65
  end
@@ -0,0 +1,27 @@
1
+ module Lunar
2
+ class ResultSet
3
+ include Enumerable
4
+
5
+ attr :sorted_set_key
6
+
7
+ def initialize(sorted_set_key, &block)
8
+ @sorted_set_key = sorted_set_key
9
+ @block = block
10
+ end
11
+
12
+ def each(&block)
13
+ all.each(&block)
14
+ end
15
+
16
+ def all(options = {})
17
+ start = Integer(options[:start] || 0)
18
+ finish = start + Integer(options[:limit] || 0) - 1
19
+
20
+ Lunar.redis.zrevrange(sorted_set_key, start, finish).map(&@block)
21
+ end
22
+
23
+ def size
24
+ Lunar.redis.zcard(sorted_set_key)
25
+ end
26
+ end
27
+ end
data/lib/lunar/scoring.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Lunar
2
2
  class Scoring
3
3
  def initialize(words)
4
- @words = words.split(/\s+/).reject { |w| w.to_s.strip.empty? }
4
+ @words = Words.new(words)
5
5
  end
6
6
 
7
7
  def scores
@@ -0,0 +1,44 @@
1
+ module Lunar
2
+ class Search
3
+ attr :sets, :prefix, :search_identifier
4
+
5
+ def initialize(prefix, keywords)
6
+ if keywords.is_a?(Hash)
7
+ @sets = keywords.inject([]) { |a, (field, query)| a | Sets.new(prefix, query, field) }
8
+ @search_identifier = keywords.hash
9
+ else
10
+ @sets = Sets.new(prefix, keywords)
11
+ @search_identifier = keywords.hash
12
+ end
13
+
14
+ @prefix = prefix
15
+
16
+ # Default finder, uses Ohm style finding
17
+ @finder = lambda { |id| prefix[id] }
18
+ end
19
+
20
+ def results(&block)
21
+ Lunar.redis.zunion dist_key, sets.size, *sets
22
+
23
+ if block_given?
24
+ ResultSet.new(dist_key, &block)
25
+ else
26
+ ResultSet.new(dist_key, &@finder)
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
+ end
37
+
38
+ protected
39
+ def dist_key
40
+ Lunar.nest[:Results][search_identifier]
41
+ end
42
+ end
43
+ end
44
+
data/lib/lunar/sets.rb ADDED
@@ -0,0 +1,26 @@
1
+ module Lunar
2
+ class Sets < Array
3
+ attr :prefix, :words, :field
4
+
5
+ def initialize(prefix, keywords, field = '*')
6
+ @prefix = prefix
7
+ @words = Words.new(keywords)
8
+ @field = field
9
+
10
+ super(redis_set_keys)
11
+ end
12
+
13
+ protected
14
+ def redis_set_keys
15
+ keys_for_each_word.map { |key| Lunar.redis.keys(key) }.flatten
16
+ end
17
+
18
+ def keys_for_each_word
19
+ words.map { |w| ns[Lunar.encode(w)] }
20
+ end
21
+
22
+ def ns
23
+ @ns ||= Lunar.nest[prefix][field]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ require 'iconv'
2
+
3
+ module Lunar
4
+ class Words < Array
5
+ SEPARATOR = /\s+/
6
+
7
+ def initialize(str)
8
+ words = str.split(SEPARATOR).
9
+ reject { |w| w.to_s.strip.empty? }.
10
+ map { |w| sanitize(w) }
11
+
12
+ super(words)
13
+ end
14
+
15
+ private
16
+ def sanitize(str)
17
+ Iconv.iconv('UTF-8//IGNORE', 'UTF-8', str)[0].to_s.
18
+ gsub(/[^a-zA-Z0-9\-_]/, '')
19
+ end
20
+ end
21
+ end
data/lunar.gemspec ADDED
@@ -0,0 +1,106 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{lunar}
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Cyril David"]
12
+ s.date = %q{2010-05-02}
13
+ s.description = %q{uses sorted sets and sets, sorting by score}
14
+ s.email = %q{cyx.ucron@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "examples/ohm.rb",
27
+ "lib/lunar.rb",
28
+ "lib/lunar/doc.rb",
29
+ "lib/lunar/index.rb",
30
+ "lib/lunar/result_set.rb",
31
+ "lib/lunar/scoring.rb",
32
+ "lib/lunar/search.rb",
33
+ "lib/lunar/sets.rb",
34
+ "lib/lunar/words.rb",
35
+ "lunar.gemspec",
36
+ "test/helper.rb",
37
+ "test/test_lunar.rb",
38
+ "test/test_lunar_document.rb",
39
+ "test/test_lunar_index.rb",
40
+ "test/test_lunar_scoring.rb",
41
+ "test/test_lunar_search.rb",
42
+ "test/test_sets.rb",
43
+ "vendor/nest/nest.rb",
44
+ "vendor/redis/.gitignore",
45
+ "vendor/redis/LICENSE",
46
+ "vendor/redis/README.markdown",
47
+ "vendor/redis/Rakefile",
48
+ "vendor/redis/benchmarking/logging.rb",
49
+ "vendor/redis/benchmarking/pipeline.rb",
50
+ "vendor/redis/benchmarking/speed.rb",
51
+ "vendor/redis/benchmarking/suite.rb",
52
+ "vendor/redis/benchmarking/worker.rb",
53
+ "vendor/redis/bin/distredis",
54
+ "vendor/redis/examples/basic.rb",
55
+ "vendor/redis/examples/dist_redis.rb",
56
+ "vendor/redis/examples/incr-decr.rb",
57
+ "vendor/redis/examples/list.rb",
58
+ "vendor/redis/examples/pubsub.rb",
59
+ "vendor/redis/examples/sets.rb",
60
+ "vendor/redis/lib/edis.rb",
61
+ "vendor/redis/lib/redis.rb",
62
+ "vendor/redis/lib/redis/client.rb",
63
+ "vendor/redis/lib/redis/dist_redis.rb",
64
+ "vendor/redis/lib/redis/distributed.rb",
65
+ "vendor/redis/lib/redis/hash_ring.rb",
66
+ "vendor/redis/lib/redis/pipeline.rb",
67
+ "vendor/redis/lib/redis/raketasks.rb",
68
+ "vendor/redis/lib/redis/subscribe.rb",
69
+ "vendor/redis/profile.rb",
70
+ "vendor/redis/tasks/redis.tasks.rb",
71
+ "vendor/redis/test/db/.gitignore",
72
+ "vendor/redis/test/distributed_test.rb",
73
+ "vendor/redis/test/redis_test.rb",
74
+ "vendor/redis/test/test.conf",
75
+ "vendor/redis/test/test_helper.rb"
76
+ ]
77
+ s.homepage = %q{http://github.com/cyx/lunar}
78
+ s.rdoc_options = ["--charset=UTF-8"]
79
+ s.require_paths = ["lib"]
80
+ s.rubygems_version = %q{1.3.6}
81
+ s.summary = %q{a minimalistic full text search implementation in redis}
82
+ s.test_files = [
83
+ "test/helper.rb",
84
+ "test/test_lunar.rb",
85
+ "test/test_lunar_document.rb",
86
+ "test/test_lunar_index.rb",
87
+ "test/test_lunar_scoring.rb",
88
+ "test/test_lunar_search.rb",
89
+ "test/test_sets.rb",
90
+ "examples/ohm.rb"
91
+ ]
92
+
93
+ if s.respond_to? :specification_version then
94
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
95
+ s.specification_version = 3
96
+
97
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
98
+ s.add_development_dependency(%q<contest>, [">= 0"])
99
+ else
100
+ s.add_dependency(%q<contest>, [">= 0"])
101
+ end
102
+ else
103
+ s.add_dependency(%q<contest>, [">= 0"])
104
+ end
105
+ end
106
+
@@ -163,9 +163,10 @@ class LunarIndexTest < Test::Unit::TestCase
163
163
  assert ! Lunar.redis.exists("Lunar:Item:1001:description")
164
164
  end
165
165
  end
166
+
166
167
  protected
167
168
  def encode(str)
168
- Base64.encode64(str).strip
169
+ Lunar.encode(str)
169
170
  end
170
171
 
171
172
  def zscore(key, value)
@@ -23,4 +23,14 @@ class LunarScoringTest < Test::Unit::TestCase
23
23
  assert_equal 3, scoring.scores['brown']
24
24
  end
25
25
  end
26
+
27
+ describe 'scores of apple macbook pro 17"' do
28
+ should "return a hash of apple macbook pro 17" do
29
+ scoring = Lunar::Scoring.new('apple macbook pro 17"')
30
+ assert_equal 1, scoring.scores['apple']
31
+ assert_equal 1, scoring.scores['macbook']
32
+ assert_equal 1, scoring.scores['pro']
33
+ assert_equal 1, scoring.scores['17']
34
+ end
35
+ end
26
36
  end
@@ -0,0 +1,77 @@
1
+ require "helper"
2
+
3
+ class Item < Struct.new(:id)
4
+ def self.[](id)
5
+ new(id)
6
+ end
7
+ end
8
+
9
+ class LunarSearchTest < Test::Unit::TestCase
10
+ context "given an Item named iphone 3GS and apple macbook pro 17 exists" do
11
+ setup do
12
+ Lunar::Index.create "Item" do |i|
13
+ i.key 1001
14
+ i.attr :name, 'iphone 3GS'
15
+ end
16
+
17
+ Lunar::Index.create "Item" do |i|
18
+ i.key 1002
19
+ i.attr :name, 'apple macbook pro 17"'
20
+ i.attr :desc, 'iphone'
21
+ end
22
+ end
23
+
24
+ should "be able to search by keyword iphone" do
25
+ items = Lunar.search(Item, "iphone") { |id| Item.new(id) }
26
+
27
+ assert items.map(&:id).include?('1001')
28
+ assert items.map(&:id).include?('1002')
29
+ end
30
+
31
+ should "be able to search by keyword iphone apple and return the 2 items" do
32
+ items = Lunar.search(Item, "iphone apple") { |id| Item.new(id) }
33
+
34
+ assert items.map(&:id).include?('1001')
35
+ assert items.map(&:id).include?('1002')
36
+ end
37
+
38
+ should "be able to search 17 and return the macbook pro" do
39
+ items = Lunar.search(Item, "17") { |id| Item.new(id) }
40
+
41
+ assert items.map(&:id).include?('1002')
42
+ end
43
+
44
+ should 'be able to search 17 with quote and return the macbook pro' do
45
+ items = Lunar.search(Item, '17"') { |id| Item.new(id) }
46
+
47
+ assert items.map(&:id).include?('1002')
48
+ end
49
+
50
+ should "not return item 1001 when searching apple only" do
51
+ items = Lunar.search(Item, "apple") { |id| Item.new(id) }
52
+
53
+ assert ! items.map(&:id).include?('1001')
54
+ end
55
+
56
+ should "by default try and find using a bracket syntax" do
57
+ items = Lunar.search(Item, "apple")
58
+
59
+ assert items.map(&:id).include?('1002')
60
+ end
61
+
62
+ should "be able to search by name: 'iphone'" do
63
+ items = Lunar.search(Item, :name => 'iphone')
64
+
65
+ assert items.map(&:id).include?('1001')
66
+ assert ! items.map(&:id).include?('1002')
67
+ end
68
+
69
+ should "be able to search by name: 'iphone' desc: 'iphone'" do
70
+ items = Lunar.search(Item, :name => 'iphone', :desc => 'iphone')
71
+
72
+ assert items.map(&:id).include?('1001')
73
+ assert items.map(&:id).include?('1002')
74
+ end
75
+
76
+ end
77
+ end
data/test/test_sets.rb ADDED
@@ -0,0 +1,48 @@
1
+ require "helper"
2
+
3
+ class LunarSetsTest < Test::Unit::TestCase
4
+ setup do
5
+ Lunar.redis(Redis.new(:host => "127.0.0.1", :port => "6380"))
6
+ Lunar.redis.flushdb
7
+
8
+ @keys = [
9
+ "Lunar:Item:name:#{ Lunar.encode('iphone') }",
10
+ "Lunar:Item:desc:#{ Lunar.encode('iphone') }",
11
+ "Lunar:Item:tags:#{ Lunar.encode('asian') }",
12
+ "Lunar:Item:tags:#{ Lunar.encode('hacker') }",
13
+ "Lunar:Item:tags:#{ Lunar.encode('ninja') }",
14
+ "Lunar:Item:body:#{ Lunar.encode('ninja') }",
15
+ "Lunar:Item:body:#{ Lunar.encode('assassin') }"
16
+ ]
17
+
18
+ @keys.each { |key| Lunar.redis.zadd key, 1, 1001 }
19
+ end
20
+
21
+ context "given prefix Item, keywords iphone" do
22
+ should "return both keys" do
23
+ sets = Lunar::Sets.new("Item", "iphone")
24
+
25
+ assert sets.include?(@keys[0])
26
+ assert sets.include?(@keys[1])
27
+ end
28
+ end
29
+
30
+ context "given prefix Item, keywords iphone, field name" do
31
+ should "return only the name key" do
32
+ sets = Lunar::Sets.new("Item", "iphone", "name")
33
+
34
+ assert sets.include?(@keys[0])
35
+ assert ! sets.include?(@keys[1])
36
+ end
37
+ end
38
+
39
+ context "given asian ninja assassin" do
40
+ should "return all relevant keys" do
41
+ sets = Lunar::Sets.new("Item", "asian ninja assassin", "tags")
42
+
43
+ assert sets.include?(@keys[2])
44
+ assert sets.include?(@keys[4])
45
+ end
46
+ end
47
+
48
+ end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 0
9
- version: 0.1.0
8
+ - 1
9
+ version: 0.1.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Cyril David
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-05-01 00:00:00 +08:00
17
+ date: 2010-05-02 00:00:00 +08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -49,12 +49,19 @@ files:
49
49
  - lib/lunar.rb
50
50
  - lib/lunar/doc.rb
51
51
  - lib/lunar/index.rb
52
+ - lib/lunar/result_set.rb
52
53
  - lib/lunar/scoring.rb
54
+ - lib/lunar/search.rb
55
+ - lib/lunar/sets.rb
56
+ - lib/lunar/words.rb
57
+ - lunar.gemspec
53
58
  - test/helper.rb
54
59
  - test/test_lunar.rb
55
60
  - test/test_lunar_document.rb
56
61
  - test/test_lunar_index.rb
57
62
  - test/test_lunar_scoring.rb
63
+ - test/test_lunar_search.rb
64
+ - test/test_sets.rb
58
65
  - vendor/nest/nest.rb
59
66
  - vendor/redis/.gitignore
60
67
  - vendor/redis/LICENSE
@@ -124,4 +131,6 @@ test_files:
124
131
  - test/test_lunar_document.rb
125
132
  - test/test_lunar_index.rb
126
133
  - test/test_lunar_scoring.rb
134
+ - test/test_lunar_search.rb
135
+ - test/test_sets.rb
127
136
  - examples/ohm.rb