lunar 0.1.0 → 0.1.1

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