elastictastic 0.5.0 → 0.10.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.md +161 -10
- data/lib/elastictastic/adapter.rb +84 -0
- data/lib/elastictastic/association.rb +6 -0
- data/lib/elastictastic/basic_document.rb +213 -0
- data/lib/elastictastic/bulk_persistence_strategy.rb +64 -19
- data/lib/elastictastic/callbacks.rb +18 -12
- data/lib/elastictastic/child_collection_proxy.rb +15 -11
- data/lib/elastictastic/client.rb +47 -24
- data/lib/elastictastic/configuration.rb +59 -4
- data/lib/elastictastic/dirty.rb +43 -28
- data/lib/elastictastic/discrete_persistence_strategy.rb +48 -23
- data/lib/elastictastic/document.rb +1 -85
- data/lib/elastictastic/embedded_document.rb +34 -0
- data/lib/elastictastic/errors.rb +17 -5
- data/lib/elastictastic/field.rb +3 -0
- data/lib/elastictastic/mass_assignment_security.rb +2 -4
- data/lib/elastictastic/middleware.rb +66 -84
- data/lib/elastictastic/multi_get.rb +30 -0
- data/lib/elastictastic/multi_search.rb +70 -0
- data/lib/elastictastic/nested_document.rb +3 -27
- data/lib/elastictastic/new_relic_instrumentation.rb +8 -8
- data/lib/elastictastic/observing.rb +8 -6
- data/lib/elastictastic/optimistic_locking.rb +57 -0
- data/lib/elastictastic/parent_child.rb +56 -54
- data/lib/elastictastic/persistence.rb +16 -16
- data/lib/elastictastic/properties.rb +136 -96
- data/lib/elastictastic/railtie.rb +1 -1
- data/lib/elastictastic/rotor.rb +105 -0
- data/lib/elastictastic/scope.rb +186 -56
- data/lib/elastictastic/server_error.rb +20 -1
- data/lib/elastictastic/test_helpers.rb +152 -97
- data/lib/elastictastic/thrift/constants.rb +12 -0
- data/lib/elastictastic/thrift/rest.rb +83 -0
- data/lib/elastictastic/thrift/types.rb +124 -0
- data/lib/elastictastic/thrift_adapter.rb +61 -0
- data/lib/elastictastic/transport_methods.rb +27 -0
- data/lib/elastictastic/validations.rb +11 -13
- data/lib/elastictastic/version.rb +1 -1
- data/lib/elastictastic.rb +148 -27
- data/spec/environment.rb +1 -1
- data/spec/examples/bulk_persistence_strategy_spec.rb +151 -23
- data/spec/examples/callbacks_spec.rb +65 -34
- data/spec/examples/dirty_spec.rb +160 -1
- data/spec/examples/document_spec.rb +168 -106
- data/spec/examples/middleware_spec.rb +1 -61
- data/spec/examples/multi_get_spec.rb +127 -0
- data/spec/examples/multi_search_spec.rb +113 -0
- data/spec/examples/observing_spec.rb +24 -3
- data/spec/examples/optimistic_locking_spec.rb +417 -0
- data/spec/examples/parent_child_spec.rb +73 -33
- data/spec/examples/properties_spec.rb +53 -0
- data/spec/examples/rotor_spec.rb +132 -0
- data/spec/examples/scope_spec.rb +78 -18
- data/spec/examples/search_spec.rb +26 -0
- data/spec/examples/validation_spec.rb +7 -1
- data/spec/models/author.rb +1 -1
- data/spec/models/blog.rb +2 -0
- data/spec/models/comment.rb +1 -1
- data/spec/models/photo.rb +9 -0
- data/spec/models/post.rb +3 -0
- metadata +97 -78
- data/lib/elastictastic/resource.rb +0 -4
- data/spec/examples/active_model_lint_spec.rb +0 -20
@@ -25,68 +25,8 @@ describe Elastictastic::Middleware::LogRequests do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'should log body of POST requests to logger' do
|
28
|
-
|
28
|
+
stub_es_create('default', 'post')
|
29
29
|
client.create('default', 'post', nil, {})
|
30
30
|
io.string.should == "ElasticSearch POST (3ms) /default/post {}\n"
|
31
31
|
end
|
32
32
|
end
|
33
|
-
|
34
|
-
describe Elastictastic::Middleware::Rotor do
|
35
|
-
let(:config) do
|
36
|
-
Elastictastic::Configuration.new.tap do |config|
|
37
|
-
config.hosts = ['http://es1.local', 'http://es2.local']
|
38
|
-
end
|
39
|
-
end
|
40
|
-
let(:client) { Elastictastic::Client.new(config) }
|
41
|
-
let(:last_request) { FakeWeb.last_request }
|
42
|
-
|
43
|
-
it 'should alternate requests between hosts' do
|
44
|
-
expect do
|
45
|
-
2.times do
|
46
|
-
1.upto 2 do |i|
|
47
|
-
host_status(i => true)
|
48
|
-
client.get('default', 'post', '1')
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end.not_to raise_error # We can't check the hostname of last_request in Fakeweb
|
52
|
-
end
|
53
|
-
|
54
|
-
context 'if one host fails' do
|
55
|
-
let!(:now) { Time.now.tap { |now| Time.stub(:now).and_return(now) }}
|
56
|
-
|
57
|
-
before do
|
58
|
-
host_status(1 => false, 2 => true)
|
59
|
-
end
|
60
|
-
|
61
|
-
it 'should try the next host' do
|
62
|
-
client.get('default', 'post', '1').should == { 'success' => true }
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
context 'if all hosts fail' do
|
67
|
-
let!(:now) { Time.now.tap { |now| Time.stub(:now).and_return(now) }}
|
68
|
-
|
69
|
-
before do
|
70
|
-
host_status(1 => false, 2 => false)
|
71
|
-
end
|
72
|
-
|
73
|
-
it 'should raise error if no hosts respond' do
|
74
|
-
expect { client.get('default', 'post', '1') }.to(raise_error Elastictastic::NoServerAvailable)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
|
80
|
-
def host_status(statuses)
|
81
|
-
FakeWeb.clean_registry
|
82
|
-
statuses.each_pair do |i, healthy|
|
83
|
-
url = "http://es#{i}.local/default/post/1"
|
84
|
-
if healthy
|
85
|
-
options = { :body => '{"success":true}' }
|
86
|
-
else
|
87
|
-
options = { :exception => Errno::ECONNREFUSED }
|
88
|
-
end
|
89
|
-
FakeWeb.register_uri(:get, url, options)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Elastictastic::MultiGet do
|
4
|
+
include Elastictastic::TestHelpers
|
5
|
+
|
6
|
+
let(:last_request_body) { Elastictastic.json_decode(last_request.body) }
|
7
|
+
|
8
|
+
before do
|
9
|
+
stub_es_mget(
|
10
|
+
nil,
|
11
|
+
nil,
|
12
|
+
['1', 'post', 'default'], ['2', 'post', 'my_index'], ['3', 'post', 'my_index'],
|
13
|
+
['4', 'post', 'my_index'] => nil
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'with no options' do
|
18
|
+
let!(:posts) do
|
19
|
+
Elastictastic.multi_get do |mget|
|
20
|
+
mget.add(Post, 1)
|
21
|
+
mget.add(Post.in_index('my_index'), 2, 3, 4)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should send request to base path' do
|
26
|
+
last_request.path.should == '/_mget'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should request ids with type and index' do
|
30
|
+
last_request_body.should == {
|
31
|
+
'docs' => [{
|
32
|
+
'_id' => '1',
|
33
|
+
'_type' => 'post',
|
34
|
+
'_index' => 'default'
|
35
|
+
}, {
|
36
|
+
'_id' => '2',
|
37
|
+
'_type' => 'post',
|
38
|
+
'_index' => 'my_index'
|
39
|
+
}, {
|
40
|
+
'_id' => '3',
|
41
|
+
'_type' => 'post',
|
42
|
+
'_index' => 'my_index'
|
43
|
+
}, {
|
44
|
+
'_id' => '4',
|
45
|
+
'_type' => 'post',
|
46
|
+
'_index' => 'my_index'
|
47
|
+
}]
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should return existing docs with IDs' do
|
52
|
+
posts.map(&:id).should == %w(1 2 3)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should set proper indices' do
|
56
|
+
posts.map { |post| post.index.name }.should ==
|
57
|
+
%w(default my_index my_index)
|
58
|
+
end
|
59
|
+
end # context 'with no options'
|
60
|
+
|
61
|
+
context 'with fields specified' do
|
62
|
+
let!(:posts) do
|
63
|
+
Elastictastic.multi_get do |mget|
|
64
|
+
mget.add(Post.fields('title'), '1')
|
65
|
+
mget.add(Post.in_index('my_index').fields('title'), '2', '3')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should inject fields into each identifier' do
|
70
|
+
last_request_body.should == {
|
71
|
+
'docs' => [{
|
72
|
+
'_id' => '1',
|
73
|
+
'_type' => 'post',
|
74
|
+
'_index' => 'default',
|
75
|
+
'fields' => %w(title)
|
76
|
+
}, {
|
77
|
+
'_id' => '2',
|
78
|
+
'_type' => 'post',
|
79
|
+
'_index' => 'my_index',
|
80
|
+
'fields' => %w(title)
|
81
|
+
}, {
|
82
|
+
'_id' => '3',
|
83
|
+
'_type' => 'post',
|
84
|
+
'_index' => 'my_index',
|
85
|
+
'fields' => %w(title)
|
86
|
+
}]
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'with routing specified' do
|
92
|
+
let!(:posts) do
|
93
|
+
Elastictastic.multi_get do |mget|
|
94
|
+
mget.add(Post.routing('foo'), '1')
|
95
|
+
mget.add(Post.in_index('my_index').routing('bar'), '2', '3')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should inject fields into each identifier' do
|
100
|
+
last_request_body.should == {
|
101
|
+
'docs' => [{
|
102
|
+
'_id' => '1',
|
103
|
+
'_type' => 'post',
|
104
|
+
'_index' => 'default',
|
105
|
+
'routing' => 'foo'
|
106
|
+
}, {
|
107
|
+
'_id' => '2',
|
108
|
+
'_type' => 'post',
|
109
|
+
'_index' => 'my_index',
|
110
|
+
'routing' => 'bar'
|
111
|
+
}, {
|
112
|
+
'_id' => '3',
|
113
|
+
'_type' => 'post',
|
114
|
+
'_index' => 'my_index',
|
115
|
+
'routing' => 'bar'
|
116
|
+
}]
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'with no docspecs given' do
|
122
|
+
it 'should gracefully return nothing' do
|
123
|
+
FakeWeb.clean_registry
|
124
|
+
Elastictastic.multi_get {}.to_a.should == []
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Elastictastic::MultiSearch do
|
4
|
+
include Elastictastic::TestHelpers
|
5
|
+
|
6
|
+
let(:request_components) do
|
7
|
+
[].tap do |components|
|
8
|
+
FakeWeb.last_request.body.each_line do |line|
|
9
|
+
components << Elastictastic.json_decode(line) unless line.strip.empty?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '::query' do
|
15
|
+
let(:scopes) do
|
16
|
+
[
|
17
|
+
Post.query(:query_string => { :query => 'pizza' }).size(10),
|
18
|
+
Blog.in_index('my_index').query(:term => { 'name' => 'Pasta' }).size(10),
|
19
|
+
Photo.routing('7').query(:query_string => {:query => 'pizza'}).size(10)
|
20
|
+
]
|
21
|
+
end
|
22
|
+
|
23
|
+
before do
|
24
|
+
stub_es_msearch(
|
25
|
+
Array.new(3) { |i| generate_es_hit('post', :source => { 'title' => "post #{i}" }) },
|
26
|
+
Array.new(5) { |i| generate_es_hit('blog', :source => { 'name' => "blog #{i}" }) },
|
27
|
+
Array.new(2) { |i| generate_es_hit('photo', :source => { 'caption' => "photo #{i}" }) }
|
28
|
+
)
|
29
|
+
Elastictastic::MultiSearch.query(scopes)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should send correct type, index, and search_type' do
|
33
|
+
request_components[0].should == { 'index' => 'default', 'type' => 'post', 'search_type' => 'query_then_fetch' }
|
34
|
+
request_components[2].should == { 'index' => 'my_index', 'type' => 'blog', 'search_type' => 'query_then_fetch' }
|
35
|
+
request_components[4].should == { 'index' => 'default', 'type' => 'photo', 'search_type' => 'query_then_fetch', 'routing' => '7' }
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should send correct scope params' do
|
39
|
+
request_components[1].should == scopes[0].params
|
40
|
+
request_components[3].should == scopes[1].params
|
41
|
+
request_components[5].should == scopes[2].params
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should populate scopes with results' do
|
45
|
+
scopes.first.each_with_index { |post, i| post.title.should == "post #{i}" }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '::count' do
|
50
|
+
let(:scopes) do
|
51
|
+
[
|
52
|
+
Post.query(:query_string => { :query => 'pizza' }).size(10),
|
53
|
+
Blog.in_index('my_index').query(:term => { 'name' => 'Pasta' })
|
54
|
+
]
|
55
|
+
end
|
56
|
+
|
57
|
+
before do
|
58
|
+
stub_es_msearch_count(3, 5)
|
59
|
+
Elastictastic::MultiSearch.count(scopes)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should send count search_type' do
|
63
|
+
request_components[0]['search_type'].should == 'count'
|
64
|
+
request_components[2]['search_type'].should == 'count'
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should populate counts' do
|
68
|
+
scopes.map { |scope| scope.count }.should == [3, 5]
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should not populate results' do
|
72
|
+
expect { scopes.first.to_a }.
|
73
|
+
to raise_error(FakeWeb::NetConnectNotAllowedError)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'with no components' do
|
78
|
+
it 'should not attempt request' do
|
79
|
+
expect { Elastictastic::MultiSearch.query([]) }.
|
80
|
+
to_not raise_error(FakeWeb::NetConnectNotAllowedError)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'with unbounded scopes' do
|
85
|
+
let(:scopes) do
|
86
|
+
[Post.query(:query_string => { :query => 'pizza' })]
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should throw an exception' do
|
90
|
+
expect { Elastictastic::MultiSearch.query(scopes) }.
|
91
|
+
to raise_error(ArgumentError)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'with error in response' do
|
96
|
+
let(:scopes) do
|
97
|
+
[Post.query(:bogus => {}).size(10)]
|
98
|
+
end
|
99
|
+
|
100
|
+
before do
|
101
|
+
stub_request_json(
|
102
|
+
:post,
|
103
|
+
match_es_path('/_msearch'),
|
104
|
+
'responses' => [{ 'error' => 'SearchPhaseExecutionException[Failed to execute phase [query], total failure; shardFailures {[8A2fuICBTdiry42KTfa7uQ][contact_documents-1-development-0][4]: SearchParseException[[contact_documents-1-development-0][4]: from[-1],size[-1]: Parse Failure [Failed to parse source [{\"query\":{\"bogus\": {}}}]]]; nested: QueryParsingException[[contact_documents-1-development-0] No query registered for [bogus]]; }{[8A2fuICBTdiry42KTfa7uQ][contact_documents-1-development-0][3]: SearchParseException[[contact_documents-1-development-0][3]: from[-1],size[-1]: Parse Failure [Failed to parse source [{\"query\":{\"bogus\": {}}}]]]; nested: QueryParsingException[[contact_documents-1-development-0] No query registered for [bogus]]; }]'}]
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should raise error' do
|
109
|
+
expect { Elastictastic::MultiSearch.query(scopes) }.
|
110
|
+
to raise_error(Elastictastic::ServerError::QueryParsingException)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -12,9 +12,9 @@ describe Elastictastic::Observing do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
before do
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
stub_es_create('default', 'post')
|
16
|
+
stub_es_update('default', 'post', id)
|
17
|
+
stub_es_destroy('default', 'post', id)
|
18
18
|
Elastictastic.config.observers = [:post_observer]
|
19
19
|
Elastictastic.config.instantiate_observers
|
20
20
|
end
|
@@ -59,6 +59,13 @@ describe Elastictastic::Observing do
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
+
context 'on create with observing disabled' do
|
63
|
+
it 'should not run any observers' do
|
64
|
+
post.save(:observers => false)
|
65
|
+
post.observers_that_ran.should be_empty
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
62
69
|
context 'on update' do
|
63
70
|
let(:observers) { persisted_post.observers_that_ran }
|
64
71
|
|
@@ -99,6 +106,13 @@ describe Elastictastic::Observing do
|
|
99
106
|
end
|
100
107
|
end
|
101
108
|
|
109
|
+
context 'on update with observers disabled' do
|
110
|
+
it 'should not run any observers' do
|
111
|
+
persisted_post.save(:observers => false)
|
112
|
+
persisted_post.observers_that_ran.should be_empty
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
102
116
|
context 'on destroy' do
|
103
117
|
let(:observers) { persisted_post.observers_that_ran }
|
104
118
|
|
@@ -138,4 +152,11 @@ describe Elastictastic::Observing do
|
|
138
152
|
observers.should include(:after_destroy)
|
139
153
|
end
|
140
154
|
end
|
155
|
+
|
156
|
+
context 'on destroy with observers disabled' do
|
157
|
+
it 'should not run any observers' do
|
158
|
+
persisted_post.destroy(:observers => false)
|
159
|
+
persisted_post.observers_that_ran.should be_empty
|
160
|
+
end
|
161
|
+
end
|
141
162
|
end
|