waistband 0.8.5 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -2
- data/LICENSE.txt +1 -1
- data/README.md +81 -63
- data/lib/waistband.rb +9 -8
- data/lib/waistband/configuration.rb +20 -9
- data/lib/waistband/errors.rb +9 -0
- data/lib/waistband/index.rb +177 -43
- data/lib/waistband/result.rb +21 -0
- data/lib/waistband/search_results.rb +53 -0
- data/lib/waistband/version.rb +1 -1
- data/spec/config/waistband/waistband.yml +6 -2
- data/spec/lib/configuration_spec.rb +17 -20
- data/spec/lib/index_spec.rb +75 -35
- data/spec/lib/result_spec.rb +41 -0
- data/spec/lib/search_results_spec.rb +124 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/index_helper.rb +3 -3
- data/waistband.gemspec +3 -5
- metadata +18 -24
- data/lib/waistband/connection.rb +0 -250
- data/lib/waistband/query.rb +0 -63
- data/lib/waistband/query_result.rb +0 -24
- data/lib/waistband/quick_error.rb +0 -9
- data/spec/lib/connection_spec.rb +0 -419
- data/spec/lib/query_result_spec.rb +0 -23
- data/spec/lib/query_spec.rb +0 -161
- data/spec/lib/stringified_array_spec.rb +0 -9
- data/spec/lib/stringified_hash_spec.rb +0 -27
@@ -0,0 +1,21 @@
|
|
1
|
+
module Waistband
|
2
|
+
class Result
|
3
|
+
|
4
|
+
def initialize(result_hash)
|
5
|
+
@result_hash = result_hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def method_missing(method_name, *args, &block)
|
9
|
+
return @result_hash[method_name.to_s] if @result_hash.keys.include?(method_name.to_s)
|
10
|
+
return @result_hash['_source'][method_name.to_s] if @result_hash['_source'] && @result_hash['_source'].keys.include?(method_name.to_s)
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def respond_to_missing?(method_name, include_private = false)
|
15
|
+
return true if @result_hash.keys.include?(method_name.to_s)
|
16
|
+
return true if @result_hash['_source'] && @result_hash['_source'].keys.include?(method_name.to_s)
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'waistband/result'
|
2
|
+
|
3
|
+
module Waistband
|
4
|
+
class SearchResults
|
5
|
+
|
6
|
+
DEFAULT_PAGE_SIZE = 20
|
7
|
+
|
8
|
+
def initialize(search_hash, options = {})
|
9
|
+
@page = options[:page] || 1
|
10
|
+
@page_size = options[:page_size] || DEFAULT_PAGE_SIZE
|
11
|
+
@search_hash = search_hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def hits
|
15
|
+
raise ::Waistband::Errors::NoSearchHits.new("No search hits!") unless @search_hash['hits']
|
16
|
+
@search_hash['hits']['hits']
|
17
|
+
end
|
18
|
+
|
19
|
+
def results
|
20
|
+
raise ::Waistband::Errors::NoSearchHits.new("No search hits!") unless @search_hash['hits']
|
21
|
+
|
22
|
+
hits.map do |hit|
|
23
|
+
::Waistband::Result.new(hit)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def paginated_hits
|
28
|
+
raise "Kaminari gem not found for pagination" unless defined?(Kaminari)
|
29
|
+
Kaminari.paginate_array(hits, total_count: total_results).page(@page).per(@page_size)
|
30
|
+
end
|
31
|
+
|
32
|
+
def paginated_results
|
33
|
+
raise "Kaminari gem not found for pagination" unless defined?(Kaminari)
|
34
|
+
Kaminari.paginate_array(results, total_count: total_results).page(@page).per(@page_size)
|
35
|
+
end
|
36
|
+
|
37
|
+
def total_results
|
38
|
+
raise ::Waistband::Errors::NoSearchHits.new("No search hits!") unless @search_hash['hits']
|
39
|
+
@search_hash['hits']['total']
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing(method_name, *args, &block)
|
43
|
+
return @search_hash[method_name.to_s] if @search_hash.keys.include?(method_name.to_s)
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
def respond_to_missing?(method_name, include_private = false)
|
48
|
+
return true if @search_hash.keys.include?(method_name.to_s)
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
data/lib/waistband/version.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
development: &DEV
|
2
2
|
timeout: 2
|
3
|
+
retries: 5
|
4
|
+
reload_on_failure: true
|
3
5
|
servers:
|
4
6
|
server1:
|
5
|
-
host:
|
7
|
+
host: localhost
|
6
8
|
port: 9200
|
9
|
+
protocol: http
|
7
10
|
server2:
|
8
|
-
host:
|
11
|
+
host: 127.0.0.1
|
9
12
|
port: 9200
|
13
|
+
protocol: http
|
10
14
|
|
11
15
|
test:
|
12
16
|
<<: *DEV
|
@@ -5,40 +5,37 @@ describe Waistband::Configuration do
|
|
5
5
|
let(:config) { Waistband.config }
|
6
6
|
|
7
7
|
it "loads config yml" do
|
8
|
-
config.
|
9
|
-
config.port.should eql 9200
|
10
|
-
config.timeout.should eql 2
|
8
|
+
expect(config.timeout).to eql 2
|
11
9
|
end
|
12
10
|
|
13
11
|
it "loads indexes config" do
|
14
|
-
config.index('search').
|
15
|
-
config.index('search')['settings']['index']['number_of_shards'].
|
12
|
+
expect(config.index('search')).to be_a Hash
|
13
|
+
expect(config.index('search')['settings']['index']['number_of_shards']).to eql 1
|
16
14
|
end
|
17
15
|
|
18
16
|
it "loads multiple indexes config" do
|
19
|
-
config.index('events').
|
20
|
-
config.index('events')['settings']['index']['number_of_shards'].
|
17
|
+
expect(config.index('events')).to be_a Hash
|
18
|
+
expect(config.index('events')['settings']['index']['number_of_shards']).to eql 1
|
21
19
|
end
|
22
20
|
|
23
|
-
|
21
|
+
it "proxies the client" do
|
22
|
+
expect(::Waistband.config.client).to be_a ::Elasticsearch::Transport::Client
|
23
|
+
expect(::Waistband.client).to be_a ::Elasticsearch::Transport::Client
|
24
|
+
end
|
24
25
|
|
25
|
-
|
26
|
-
config.servers.should be_an Array
|
27
|
-
config.servers.size.should eql 2
|
26
|
+
describe '#hosts' do
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
it "returns array of all available servers' configs" do
|
29
|
+
expect(config.hosts).to be_an Array
|
30
|
+
expect(config.hosts.size).to eql 2
|
32
31
|
|
33
|
-
|
34
|
-
server['
|
32
|
+
config.hosts.each_with_index do |server, i|
|
33
|
+
expect(server['host']).to match /127\.0\.0\.1|localhost/
|
34
|
+
expect(server['port']).to eql 9200
|
35
|
+
expect(server['protocol']).to eql 'http'
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
|
-
it "servers ids should be unique" do
|
39
|
-
config.servers[0]['_id'].should_not eql config.servers[1]['_id']
|
40
|
-
end
|
41
|
-
|
42
39
|
end
|
43
40
|
|
44
41
|
end
|
data/spec/lib/index_spec.rb
CHANGED
@@ -7,66 +7,93 @@ describe Waistband::Index do
|
|
7
7
|
let(:attrs) { {'ok' => {'yeah' => true}} }
|
8
8
|
|
9
9
|
it "initializes values" do
|
10
|
-
index.instance_variable_get('@stringify').
|
10
|
+
expect(index.instance_variable_get('@stringify')).to eql true
|
11
11
|
end
|
12
12
|
|
13
13
|
it "creates the index" do
|
14
|
-
index.
|
15
|
-
expect{ index.refresh
|
14
|
+
index.delete!
|
15
|
+
expect{ index.refresh }.to raise_error(Elasticsearch::Transport::Transport::Errors::NotFound)
|
16
16
|
|
17
17
|
index.create!
|
18
|
-
expect{ index.refresh
|
18
|
+
expect{ index.refresh }.to_not raise_error
|
19
19
|
end
|
20
20
|
|
21
|
-
it "
|
22
|
-
index.
|
23
|
-
expect{ index.refresh
|
21
|
+
it "deletes the index" do
|
22
|
+
index.delete!
|
23
|
+
expect{ index.refresh }.to raise_error(Elasticsearch::Transport::Transport::Errors::NotFound)
|
24
24
|
index.create!
|
25
25
|
end
|
26
26
|
|
27
|
+
it "blows up when trying to delete an index that does not exist" do
|
28
|
+
index.delete!
|
29
|
+
expect { index.delete! }.to raise_error(::Waistband::Errors::IndexNotFound)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "updates the index's mappings" do
|
33
|
+
index.refresh
|
34
|
+
response = index.update_mapping('event')
|
35
|
+
expect(response['acknowledged']).to be_true
|
36
|
+
end
|
37
|
+
|
27
38
|
it "updates the index's settings" do
|
28
39
|
index.refresh
|
29
|
-
response =
|
30
|
-
response['
|
40
|
+
response = index.update_settings
|
41
|
+
expect(response['acknowledged']).to be_true
|
31
42
|
end
|
32
43
|
|
33
|
-
it "proxies to
|
34
|
-
index.
|
44
|
+
it "proxies to the client's search" do
|
45
|
+
result = index.search({})
|
46
|
+
expect(result).to be_a Waistband::SearchResults
|
47
|
+
expect(result.took).to be_present
|
48
|
+
expect(result.hits).to be_an Array
|
35
49
|
end
|
36
50
|
|
37
51
|
describe "storing" do
|
38
52
|
|
39
53
|
it "stores data" do
|
40
|
-
index.
|
41
|
-
index.read('__test_write').
|
54
|
+
expect(index.save('__test_write', {'ok' => 'yeah'})).to be_true
|
55
|
+
expect(index.read('__test_write')).to eql({
|
56
|
+
'_id' => '__test_write',
|
57
|
+
'_index' => 'events_test',
|
58
|
+
'_source' => {'ok' => 'yeah'},
|
59
|
+
'_type' => 'event',
|
60
|
+
'_version' => 1,
|
61
|
+
'found' => true
|
62
|
+
})
|
42
63
|
end
|
43
64
|
|
44
65
|
it "data is stringified" do
|
45
|
-
index.
|
46
|
-
index.read('__test_write').
|
66
|
+
index.save('__test_write', attrs)
|
67
|
+
expect(index.read('__test_write')[:_source]).to eql({"ok"=>"{\"yeah\"=>true}"})
|
47
68
|
end
|
48
69
|
|
49
70
|
it "data is indirectly accessible when not stringified" do
|
50
|
-
index2.
|
51
|
-
index2.read('__test_not_string')[:ok][:yeah].
|
71
|
+
index2.save('__test_not_string', attrs)
|
72
|
+
expect(index2.read('__test_not_string')[:_source][:ok][:yeah]).to eql true
|
52
73
|
end
|
53
74
|
|
54
75
|
it "deletes data" do
|
55
|
-
index.
|
56
|
-
index.
|
57
|
-
index.read('__test_write').
|
76
|
+
index.save('__test_write', attrs)
|
77
|
+
index.destroy('__test_write')
|
78
|
+
expect(index.read('__test_write')).to be_nil
|
58
79
|
end
|
59
80
|
|
60
81
|
it "returns nil on 404" do
|
61
|
-
index.read('__not_here').
|
82
|
+
expect(index.read('__not_here')).to be_nil
|
83
|
+
end
|
84
|
+
|
85
|
+
it "blows up on 404 when using the bang method" do
|
86
|
+
expect {
|
87
|
+
index.read!('__not_here')
|
88
|
+
}.to raise_error(Elasticsearch::Transport::Transport::Errors::NotFound)
|
62
89
|
end
|
63
90
|
|
64
91
|
it "doesn't mix data between two indexes" do
|
65
|
-
index.
|
66
|
-
index2.
|
92
|
+
index.save('__test_write', {'data' => 'index_1'})
|
93
|
+
index2.save('__test_write', {'data' => 'index_2'})
|
67
94
|
|
68
|
-
index.read('__test_write').
|
69
|
-
index2.read('__test_write').
|
95
|
+
expect(index.read('__test_write')[:_source]).to eql({'data' => 'index_1'})
|
96
|
+
expect(index2.read('__test_write')[:_source]).to eql({'data' => 'index_2'})
|
70
97
|
end
|
71
98
|
|
72
99
|
end
|
@@ -76,26 +103,24 @@ describe Waistband::Index do
|
|
76
103
|
let(:sharded_index) { Waistband::Index.new('events', subs: %w(2013 01)) }
|
77
104
|
|
78
105
|
it "permits subbing the index" do
|
79
|
-
sharded_index.
|
80
|
-
sharded_index.config_name.should eql 'events_test__2013_01'
|
106
|
+
expect(sharded_index.send(:config_name)).to eql 'events_test__2013_01'
|
81
107
|
end
|
82
108
|
|
83
109
|
it "permits sharding into singles" do
|
84
110
|
index = Waistband::Index.new 'events', subs: '2013'
|
85
|
-
index.
|
86
|
-
index.config_name.should eql 'events_test__2013'
|
111
|
+
expect(index.send(:config_name)).to eql 'events_test__2013'
|
87
112
|
end
|
88
113
|
|
89
114
|
it "retains the same options from the parent index config" do
|
90
115
|
config = sharded_index.send(:config)
|
91
116
|
|
92
|
-
sharded_index.base_config_name.
|
93
|
-
config['stringify'].
|
94
|
-
config['settings'].
|
117
|
+
expect(sharded_index.send(:base_config_name)).to eql 'events_test'
|
118
|
+
expect(config['stringify']).to be_true
|
119
|
+
expect(config['settings']).to be_present
|
95
120
|
end
|
96
121
|
|
97
122
|
it "creates the sharded index with the same mappings as the parent" do
|
98
|
-
sharded_index.
|
123
|
+
sharded_index.delete!
|
99
124
|
|
100
125
|
expect {
|
101
126
|
sharded_index.create!
|
@@ -106,7 +131,7 @@ describe Waistband::Index do
|
|
106
131
|
|
107
132
|
it "gets a default name that makes sense for the index when not defined" do
|
108
133
|
index = Waistband::Index.new 'events_no_name', subs: %w(2013 01)
|
109
|
-
index.config_name.
|
134
|
+
expect(index.send(:config_name)).to eql 'events_no_name_test__2013_01'
|
110
135
|
end
|
111
136
|
|
112
137
|
end
|
@@ -117,7 +142,22 @@ describe Waistband::Index do
|
|
117
142
|
|
118
143
|
it "gets a default name that makes sense for the index when not defined" do
|
119
144
|
index = Waistband::Index.new 'events_no_name'
|
120
|
-
index.base_config_name.
|
145
|
+
expect(index.send(:base_config_name)).to eql 'events_no_name_test'
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
describe 'aliases' do
|
151
|
+
|
152
|
+
it "returns the alias name with the env" do
|
153
|
+
expect(index.send(:full_alias_name, 'all_events')).to eql 'all_events_test'
|
154
|
+
end
|
155
|
+
|
156
|
+
it "if the index has a custom name, the alias name doesn't automatically append the env" do
|
157
|
+
index.stub(:config).and_return({
|
158
|
+
'name' => 'super_custom'
|
159
|
+
})
|
160
|
+
expect(index.send(:full_alias_name, 'all_events')).to eql 'all_events'
|
121
161
|
end
|
122
162
|
|
123
163
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ::Waistband::Result do
|
4
|
+
|
5
|
+
let(:result_hash) do
|
6
|
+
{
|
7
|
+
"_index" => "bus_events",
|
8
|
+
"_type" => "bus_event",
|
9
|
+
"_id" => "a2081c0ce39b25d50b0a4be3c116ee7f",
|
10
|
+
"_score" => nil,
|
11
|
+
"_source" => {
|
12
|
+
"bus_event_type" => "task_opened",
|
13
|
+
"timeline_event" => "true",
|
14
|
+
"_message" => "true"
|
15
|
+
},
|
16
|
+
"sort" => [nil]
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:result) { ::Waistband::Result.new(result_hash) }
|
21
|
+
|
22
|
+
it "provides accessors for all default fields" do
|
23
|
+
expect(result._id).to eql 'a2081c0ce39b25d50b0a4be3c116ee7f'
|
24
|
+
expect(result._score).to be_nil
|
25
|
+
expect(result._type).to eql 'bus_event'
|
26
|
+
expect(result._index).to eql 'bus_events'
|
27
|
+
expect(result.sort).to eql([nil])
|
28
|
+
expect(result._source).to eql({
|
29
|
+
"bus_event_type" => "task_opened",
|
30
|
+
"timeline_event" => "true",
|
31
|
+
"_message" => "true"
|
32
|
+
})
|
33
|
+
end
|
34
|
+
|
35
|
+
it "provides method missing interface for the _source hash" do
|
36
|
+
expect(result.bus_event_type).to eql 'task_opened'
|
37
|
+
expect(result.timeline_event).to be_true
|
38
|
+
expect(result._message).to be_true
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ::Waistband::SearchResults do
|
4
|
+
|
5
|
+
let(:search_hash) do
|
6
|
+
{
|
7
|
+
"took" => 1,
|
8
|
+
"timed_out" => false,
|
9
|
+
"_shards" => {
|
10
|
+
"total" => 12,
|
11
|
+
"successful" => 12,
|
12
|
+
"failed" => 0
|
13
|
+
},
|
14
|
+
"hits" => {
|
15
|
+
"total" => 1,
|
16
|
+
"max_score" => nil,
|
17
|
+
"hits" => [{
|
18
|
+
"_index" => "bus_events",
|
19
|
+
"_type" => "bus_event",
|
20
|
+
"_id" => "a2081c0ce39b25d50b0a4be3c116ee7f",
|
21
|
+
"_score" => nil,
|
22
|
+
"_source" => {
|
23
|
+
"bus_event_type" => "task_opened",
|
24
|
+
"timeline_event" => "true",
|
25
|
+
"_message" => "true"
|
26
|
+
},
|
27
|
+
"sort" => [nil]
|
28
|
+
}]
|
29
|
+
}
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
let(:results) { ::Waistband::SearchResults.new(search_hash) }
|
34
|
+
|
35
|
+
it "provides a method interface for the results hash array" do
|
36
|
+
expect(results.took).to eql 1
|
37
|
+
expect(results.timed_out).to be_false
|
38
|
+
expect(results._shards).to eql({
|
39
|
+
"total" => 12,
|
40
|
+
"successful" => 12,
|
41
|
+
"failed" => 0
|
42
|
+
})
|
43
|
+
end
|
44
|
+
|
45
|
+
it "provides a shortcut to get the total number of hits" do
|
46
|
+
expect(results.total_results).to eql 1
|
47
|
+
end
|
48
|
+
|
49
|
+
it "provides a shortcut to get the hits directly" do
|
50
|
+
expect(results.hits).to be_an Array
|
51
|
+
hit = results.hits.first
|
52
|
+
|
53
|
+
expect(hit['_id']).to eql 'a2081c0ce39b25d50b0a4be3c116ee7f'
|
54
|
+
expect(hit['_source']['bus_event_type']).to eql 'task_opened'
|
55
|
+
end
|
56
|
+
|
57
|
+
it "maps hits to results" do
|
58
|
+
expect(results.results).to be_an Array
|
59
|
+
|
60
|
+
result = results.results.first
|
61
|
+
|
62
|
+
expect(result).to be_a ::Waistband::Result
|
63
|
+
expect(result._id).to eql 'a2081c0ce39b25d50b0a4be3c116ee7f'
|
64
|
+
expect(result._score).to be_nil
|
65
|
+
expect(result._type).to eql 'bus_event'
|
66
|
+
expect(result._index).to eql 'bus_events'
|
67
|
+
expect(result.sort).to eql([nil])
|
68
|
+
expect(result._source).to eql({
|
69
|
+
"bus_event_type" => "task_opened",
|
70
|
+
"timeline_event" => "true",
|
71
|
+
"_message" => "true"
|
72
|
+
})
|
73
|
+
expect(result.bus_event_type).to eql 'task_opened'
|
74
|
+
expect(result.timeline_event).to be_true
|
75
|
+
expect(result._message).to be_true
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#paginated_hits' do
|
79
|
+
|
80
|
+
it "provides a paginated array from Kaminari if Kaminari is defined" do
|
81
|
+
load 'active_support/concern.rb'
|
82
|
+
load 'kaminari/config.rb'
|
83
|
+
load 'kaminari/models/page_scope_methods.rb'
|
84
|
+
load 'kaminari/models/configuration_methods.rb'
|
85
|
+
load 'kaminari/models/array_extension.rb'
|
86
|
+
|
87
|
+
expect(results.paginated_hits).to be_an ::Kaminari::PaginatableArray
|
88
|
+
expect(results.paginated_hits.total_pages).to eql 1
|
89
|
+
expect(results.paginated_hits.current_page).to eql 1
|
90
|
+
end
|
91
|
+
|
92
|
+
it "blows up when kaminari's not required" do
|
93
|
+
# if we've already required Kaminari from the test above, lets remove
|
94
|
+
Object.send(:remove_const, :Kaminari) if defined?(Kaminari)
|
95
|
+
|
96
|
+
expect { results.paginated_hits }.to raise_error RuntimeError, "Kaminari gem not found for pagination"
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
describe '#paginated_results' do
|
102
|
+
|
103
|
+
it "provides a paginated array from Kaminari if Kaminari is defined" do
|
104
|
+
load 'active_support/concern.rb'
|
105
|
+
load 'kaminari/config.rb'
|
106
|
+
load 'kaminari/models/page_scope_methods.rb'
|
107
|
+
load 'kaminari/models/configuration_methods.rb'
|
108
|
+
load 'kaminari/models/array_extension.rb'
|
109
|
+
|
110
|
+
expect(results.paginated_results).to be_an ::Kaminari::PaginatableArray
|
111
|
+
expect(results.paginated_results.total_pages).to eql 1
|
112
|
+
expect(results.paginated_results.current_page).to eql 1
|
113
|
+
end
|
114
|
+
|
115
|
+
it "blows up when kaminari's not required" do
|
116
|
+
# if we've already required Kaminari from the test above, lets remove
|
117
|
+
Object.send(:remove_const, :Kaminari) if defined?(Kaminari)
|
118
|
+
|
119
|
+
expect { results.paginated_results }.to raise_error RuntimeError, "Kaminari gem not found for pagination"
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|