ion 0.0.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/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
|