ion 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +280 -0
- data/Rakefile +13 -0
- data/lib/ion.rb +84 -0
- data/lib/ion/config.rb +16 -0
- data/lib/ion/entity.rb +48 -0
- data/lib/ion/helpers.rb +11 -0
- data/lib/ion/index.rb +56 -0
- data/lib/ion/indices.rb +18 -0
- data/lib/ion/indices/metaphone.rb +7 -0
- data/lib/ion/indices/number.rb +31 -0
- data/lib/ion/indices/sort.rb +41 -0
- data/lib/ion/indices/text.rb +41 -0
- data/lib/ion/options.rb +46 -0
- data/lib/ion/scope.rb +163 -0
- data/lib/ion/search.rb +83 -0
- data/lib/ion/stringer.rb +17 -0
- data/test/irb_helpers.rb +9 -0
- data/test/p_helper.rb +31 -0
- data/test/redis_debug.rb +8 -0
- data/test/test_helper.rb +81 -0
- data/test/unit/boost_test.rb +28 -0
- data/test/unit/config_test.rb +25 -0
- data/test/unit/hash_test.rb +48 -0
- data/test/unit/ion_test.rb +112 -0
- data/test/unit/metaphone_test.rb +37 -0
- data/test/unit/number_test.rb +46 -0
- data/test/unit/options_test.rb +18 -0
- data/test/unit/range_test.rb +76 -0
- data/test/unit/score_test.rb +43 -0
- data/test/unit/sort_test.rb +36 -0
- data/test/unit/subscope_test.rb +44 -0
- data/test/unit/ttl_test.rb +40 -0
- data/test/unit/update_test.rb +42 -0
- metadata +121 -0
data/lib/ion/stringer.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Ion::Stringer
|
2
|
+
def self.sanitize(str)
|
3
|
+
return '' unless str.is_a?(String)
|
4
|
+
str.downcase.force_encoding('UTF-8')
|
5
|
+
end
|
6
|
+
|
7
|
+
# "Hey, yes you." => %w(hey yes you)
|
8
|
+
def self.keywords(str)
|
9
|
+
return Array.new unless str.is_a?(String)
|
10
|
+
self.sanitize(str).scan(/\w+/)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.classify(name)
|
14
|
+
str = name.to_s
|
15
|
+
str.scan(/\b(\w)(\w*)/).map { |(w, ord)| w.upcase + ord.downcase }.join('')
|
16
|
+
end
|
17
|
+
end
|
data/test/irb_helpers.rb
ADDED
data/test/p_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# Overrides `p` to be pretty -- useful for tests.
|
4
|
+
module Kernel
|
5
|
+
def p(*args)
|
6
|
+
# Catch source
|
7
|
+
begin
|
8
|
+
raise Error
|
9
|
+
rescue => e
|
10
|
+
file, line, _ = e.backtrace[2].split(':')
|
11
|
+
source = "%s:%s" % [ File.basename(file), line ]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Source format
|
15
|
+
pre = "\033[0;33m%20s |\033[0;m "
|
16
|
+
lines = Array.new
|
17
|
+
|
18
|
+
# YAMLify the last arg if it's YAMLable
|
19
|
+
if args.last.is_a?(Hash) || args.last.is_a?(Array)
|
20
|
+
lines = args.pop.to_yaml.split("\n")[1..-1].map { |s| " #{s}" }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Inspect everything else
|
24
|
+
lines.unshift(args.map { |a| a.is_a?(String) ? a : a.inspect }.join(' ')) if args.any?
|
25
|
+
|
26
|
+
# Print
|
27
|
+
print "\n"
|
28
|
+
puts pre % [source] + (lines.shift || "nil")
|
29
|
+
puts lines.map { |s| pre % [''] + s }.join("\n") if lines.any?
|
30
|
+
end
|
31
|
+
end
|
data/test/redis_debug.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
$:.unshift File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
|
3
|
+
require 'ohm'
|
4
|
+
require 'ohm/contrib'
|
5
|
+
require 'ion'
|
6
|
+
require 'contest'
|
7
|
+
require_relative './p_helper'
|
8
|
+
#require_relative './redis_debug'
|
9
|
+
|
10
|
+
Ion.connect url: (ENV['REDIS_URL'] || 'redis://127.0.0.1:6379/0')
|
11
|
+
|
12
|
+
class Test::Unit::TestCase
|
13
|
+
def setup
|
14
|
+
re = Redis.current
|
15
|
+
keys = re.keys("Ion:*") + re.keys("IT::*")
|
16
|
+
re.del(*keys) if keys.any?
|
17
|
+
end
|
18
|
+
|
19
|
+
def redis
|
20
|
+
Ion.redis
|
21
|
+
end
|
22
|
+
|
23
|
+
def scores_for(search)
|
24
|
+
Hash[*search.key.zrange(0, -1, with_scores: true)]
|
25
|
+
end
|
26
|
+
|
27
|
+
def lorem
|
28
|
+
(0..5).map { lorem_words[(lorem_words.length * rand).to_i] }.join(' ')
|
29
|
+
end
|
30
|
+
|
31
|
+
def lorem_words
|
32
|
+
@w ||=
|
33
|
+
%w(lorem ipsum dolor sit amet consecteteur adicicising elit sed do eiusmod) +
|
34
|
+
%w(tempor incidudunt nam posture magna aliqua ut labore et dolore) +
|
35
|
+
%w(cum sociis nostrud aequitas verificium)
|
36
|
+
end
|
37
|
+
|
38
|
+
def ids(keys)
|
39
|
+
keys.map { |k| @items[k.to_sym].id }
|
40
|
+
end
|
41
|
+
|
42
|
+
def assert_ids(keys, args={})
|
43
|
+
if args[:ordered]
|
44
|
+
assert_equal ids(keys), args[:for].ids
|
45
|
+
else
|
46
|
+
assert_equal ids(keys).sort, args[:for].ids.sort
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def assert_score(hash)
|
51
|
+
hash.each do |key, score|
|
52
|
+
assert_equal score.to_f, @scores[@items[key.to_sym].id].to_f
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module IT
|
58
|
+
end
|
59
|
+
|
60
|
+
class IT::Album < Ohm::Model
|
61
|
+
include Ion::Entity
|
62
|
+
include Ohm::Callbacks
|
63
|
+
|
64
|
+
attribute :title
|
65
|
+
attribute :body
|
66
|
+
attribute :play_count
|
67
|
+
|
68
|
+
ion {
|
69
|
+
text :title
|
70
|
+
text :body
|
71
|
+
text(:also_title) { self.title }
|
72
|
+
number :play_count
|
73
|
+
sort :title
|
74
|
+
}
|
75
|
+
|
76
|
+
after :save, :update_ion_indices
|
77
|
+
before :delete, :delete_ion_indices
|
78
|
+
end
|
79
|
+
|
80
|
+
Album = IT::Album
|
81
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class BoostTest < Test::Unit::TestCase
|
4
|
+
setup do
|
5
|
+
# Fake entries that should NOT be returned
|
6
|
+
5.times { Album.create title: lorem, body: '' }
|
7
|
+
|
8
|
+
@items = {
|
9
|
+
:a => Album.create(title: "Morning Scifi"),
|
10
|
+
:b => Album.create(title: "Intensify", body: 'Special Edition'),
|
11
|
+
:c => Album.create(title: "BT Emotional Technology", body: 'Special Edition'),
|
12
|
+
:d => Album.create(title: "BT Movement in Still Life")
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
test "scores" do
|
17
|
+
search = Album.ion.search {
|
18
|
+
text :title, "bt"
|
19
|
+
boost(2.5) { text :body, "special edition" }
|
20
|
+
}
|
21
|
+
|
22
|
+
assert_ids %w(c d), for: search, ordered: true
|
23
|
+
|
24
|
+
@scores = scores_for search
|
25
|
+
assert_score c: 3.5
|
26
|
+
assert_score d: 1.0
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class ConfigTest < Test::Unit::TestCase
|
4
|
+
setup do
|
5
|
+
# Mock erasing the config
|
6
|
+
Ion.instance_variable_set :@config, nil
|
7
|
+
@config = Ion.config
|
8
|
+
end
|
9
|
+
|
10
|
+
test "config" do
|
11
|
+
assert @config.is_a?(Ion::Config)
|
12
|
+
end
|
13
|
+
|
14
|
+
test "ignored words" do
|
15
|
+
assert @config.ignored_words.include?('a')
|
16
|
+
end
|
17
|
+
|
18
|
+
test "question mark" do
|
19
|
+
assert @config.ignored_words?
|
20
|
+
|
21
|
+
assert ! @config.foobar?
|
22
|
+
@config.foobar = 2
|
23
|
+
assert @config.foobar?
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class HashTest < Test::Unit::TestCase
|
4
|
+
setup do
|
5
|
+
@search1 = Album.ion.search {
|
6
|
+
text :title, "Vloop"
|
7
|
+
text :body, "Secher betrib"
|
8
|
+
any_of {
|
9
|
+
text :body, "betrib"
|
10
|
+
text :body, "betrib"
|
11
|
+
any_of {
|
12
|
+
text :body, "betrib"
|
13
|
+
text :body, "betrib"
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
@search2 = Album.ion.search {
|
19
|
+
text :title, "Vloop"
|
20
|
+
text :body, "Secher betrib"
|
21
|
+
any_of {
|
22
|
+
text :body, "betrib"
|
23
|
+
text :body, "betrib"
|
24
|
+
any_of {
|
25
|
+
text :body, "betrib"
|
26
|
+
text :body, "betrib"
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
@search3 = Album.ion.search {
|
32
|
+
text :title, "Vloop"
|
33
|
+
text :body, "Secher betrib"
|
34
|
+
any_of {
|
35
|
+
text :body, "betrib"
|
36
|
+
text :body, "betrib"
|
37
|
+
any_of {
|
38
|
+
text :body, "betrib"
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
test "hash test" do
|
45
|
+
assert_equal @search1.search_hash, @search2.search_hash
|
46
|
+
assert @search3.search_hash != @search2.search_hash
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class IonTest < Test::Unit::TestCase
|
4
|
+
setup do
|
5
|
+
# Fake entries that should NOT be returned
|
6
|
+
5.times { Album.create title: lorem, body: '' }
|
7
|
+
end
|
8
|
+
|
9
|
+
test "single result" do
|
10
|
+
item = Album.create title: "First Lady of Song", body: "Ella Fitzgerald"
|
11
|
+
search = Album.ion.search { text :title, "lady" }
|
12
|
+
|
13
|
+
assert_equal [item.id], search.ids
|
14
|
+
end
|
15
|
+
|
16
|
+
test "lambda index" do
|
17
|
+
item = Album.create title: "First Lady of Song", body: "Ella Fitzgerald"
|
18
|
+
search = Album.ion.search { text :also_title, "lady" }
|
19
|
+
|
20
|
+
assert_equal [item.id], search.ids
|
21
|
+
end
|
22
|
+
|
23
|
+
test "stripping punctuations" do
|
24
|
+
item = Album.create title: "Leapin-and-Lopin'"
|
25
|
+
search = Album.ion.search { text :title, "leapin lopin" }
|
26
|
+
|
27
|
+
assert_equal [item.id], search.ids
|
28
|
+
end
|
29
|
+
|
30
|
+
test "many results" do
|
31
|
+
albums = (0..10).map {
|
32
|
+
Album.create title: "Hi #{lorem}"
|
33
|
+
}
|
34
|
+
|
35
|
+
search = Album.ion.search { text :title, "Hi" }
|
36
|
+
|
37
|
+
assert_equal albums.size, search.size
|
38
|
+
assert_equal albums.map(&:id).sort, search.ids.sort
|
39
|
+
end
|
40
|
+
|
41
|
+
test "multi keywords" do
|
42
|
+
album = Album.create title: "Mingus at the Bohemia"
|
43
|
+
search = Album.ion.search { text :title, "mingus bohemia" }
|
44
|
+
|
45
|
+
assert_equal [album.id], search.ids.sort
|
46
|
+
end
|
47
|
+
|
48
|
+
test "multi keywords fail" do
|
49
|
+
album = Album.create title: "Mingus at the Bohemia"
|
50
|
+
search = Album.ion.search { text :title, "bohemia is it" }
|
51
|
+
|
52
|
+
assert_equal [], search.ids.sort
|
53
|
+
end
|
54
|
+
|
55
|
+
test "search with arity" do
|
56
|
+
item = Album.create title: "Maiden Voyage"
|
57
|
+
search = Album.ion.search { |q| q.text :title, "maiden voyage" }
|
58
|
+
|
59
|
+
assert_equal [item.id], search.ids.sort
|
60
|
+
end
|
61
|
+
|
62
|
+
test "search within one index only" do
|
63
|
+
album1 = Album.create title: "Future 2 Future", body: "Herbie Hancock"
|
64
|
+
album2 = Album.create title: "Best of Herbie", body: "VA"
|
65
|
+
|
66
|
+
search = Album.ion.search { text :title, "herbie" }
|
67
|
+
|
68
|
+
assert_equal [album2.id], search.ids.sort
|
69
|
+
end
|
70
|
+
|
71
|
+
test "count" do
|
72
|
+
5.times { Album.create(title: "Bebel Gilberto #{lorem}") }
|
73
|
+
|
74
|
+
search = Album.ion.search { text :title, "Bebel Gilberto" }
|
75
|
+
assert_equal 5, search.count
|
76
|
+
end
|
77
|
+
|
78
|
+
test "scores" do
|
79
|
+
@items = {
|
80
|
+
a: Album.create(title: "Future 2 Future", body: "Herbie Hancock"),
|
81
|
+
b: Album.create(title: "Best of Herbie", body: "Herbie Hancock")
|
82
|
+
}
|
83
|
+
|
84
|
+
search = Album.ion.search {
|
85
|
+
any_of {
|
86
|
+
text :title, "herbie"
|
87
|
+
text :body, "herbie"
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
# Album2 will go first because it matches both
|
92
|
+
assert_ids %w(a b), for: search
|
93
|
+
|
94
|
+
# Check if the scores are right
|
95
|
+
@scores = scores_for search
|
96
|
+
assert_score b: 2.0
|
97
|
+
assert_score a: 1.0
|
98
|
+
end
|
99
|
+
|
100
|
+
test "right scoring" do
|
101
|
+
@items = {
|
102
|
+
a: Album.create(title: "Best of Herbie", body: "Herbie Hancock")
|
103
|
+
}
|
104
|
+
|
105
|
+
search = Album.ion.search {
|
106
|
+
text :body, "Herbie Hancock"
|
107
|
+
}
|
108
|
+
|
109
|
+
@scores = scores_for search
|
110
|
+
assert_score a: 1.0
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class IT::Song < Ohm::Model
|
4
|
+
include Ion::Entity
|
5
|
+
include Ohm::Callbacks
|
6
|
+
|
7
|
+
attribute :title
|
8
|
+
attribute :body
|
9
|
+
|
10
|
+
ion {
|
11
|
+
text :title
|
12
|
+
metaphone :body
|
13
|
+
}
|
14
|
+
|
15
|
+
after :save, :update_ion_indices
|
16
|
+
end
|
17
|
+
|
18
|
+
Song = IT::Song
|
19
|
+
|
20
|
+
class MetaphoneTest < Test::Unit::TestCase
|
21
|
+
setup do
|
22
|
+
# Fake entries that should NOT be returned
|
23
|
+
5.times { Song.create title: lorem, body: '' }
|
24
|
+
|
25
|
+
@items = {
|
26
|
+
a: Song.create(body: "Stephanie"),
|
27
|
+
b: Song.create(body: "Ztephanno..!")
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
test "metaphone" do
|
32
|
+
search = Song.ion.search { metaphone :body, "Stefan" }
|
33
|
+
|
34
|
+
assert_ids %w(a b), for: search
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class Number < Test::Unit::TestCase
|
4
|
+
setup do
|
5
|
+
@items = {
|
6
|
+
:a => Album.create(play_count: 1),
|
7
|
+
:b => Album.create(play_count: 2),
|
8
|
+
:c => Album.create(play_count: 3),
|
9
|
+
:d => Album.create(play_count: 4),
|
10
|
+
:e => Album.create(play_count: 4),
|
11
|
+
:f => Album.create(play_count: 4),
|
12
|
+
:g => Album.create(play_count: 5),
|
13
|
+
:h => Album.create(play_count: 5)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
test "numb3rs" do
|
18
|
+
search = Album.ion.search { number :play_count, 4 }
|
19
|
+
assert_ids %w(d e f), for: search
|
20
|
+
end
|
21
|
+
|
22
|
+
test "greater than" do
|
23
|
+
search = Album.ion.search { number :play_count, gt: 3 }
|
24
|
+
assert_ids %w(d e f g h), for: search
|
25
|
+
end
|
26
|
+
|
27
|
+
test "greater than or equal" do
|
28
|
+
search = Album.ion.search { number :play_count, min: 3 }
|
29
|
+
assert_ids %w(c d e f g h), for: search
|
30
|
+
end
|
31
|
+
|
32
|
+
test "less than or equal" do
|
33
|
+
search = Album.ion.search { number :play_count, max: 3 }
|
34
|
+
assert_ids %w(a b c), for: search
|
35
|
+
end
|
36
|
+
|
37
|
+
test "greater than and less than" do
|
38
|
+
search = Album.ion.search { number :play_count, lt: 5, gt: 1 }
|
39
|
+
assert_ids %w(b c d e f), for: search
|
40
|
+
end
|
41
|
+
|
42
|
+
test "greater than equal and less than equal" do
|
43
|
+
search = Album.ion.search { number :play_count, min: 1, max: 3 }
|
44
|
+
assert_ids %w(a b c), for: search
|
45
|
+
end
|
46
|
+
end
|