elastictastic 0.5.0 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/LICENSE +1 -1
  2. data/README.md +161 -10
  3. data/lib/elastictastic/adapter.rb +84 -0
  4. data/lib/elastictastic/association.rb +6 -0
  5. data/lib/elastictastic/basic_document.rb +213 -0
  6. data/lib/elastictastic/bulk_persistence_strategy.rb +64 -19
  7. data/lib/elastictastic/callbacks.rb +18 -12
  8. data/lib/elastictastic/child_collection_proxy.rb +15 -11
  9. data/lib/elastictastic/client.rb +47 -24
  10. data/lib/elastictastic/configuration.rb +59 -4
  11. data/lib/elastictastic/dirty.rb +43 -28
  12. data/lib/elastictastic/discrete_persistence_strategy.rb +48 -23
  13. data/lib/elastictastic/document.rb +1 -85
  14. data/lib/elastictastic/embedded_document.rb +34 -0
  15. data/lib/elastictastic/errors.rb +17 -5
  16. data/lib/elastictastic/field.rb +3 -0
  17. data/lib/elastictastic/mass_assignment_security.rb +2 -4
  18. data/lib/elastictastic/middleware.rb +66 -84
  19. data/lib/elastictastic/multi_get.rb +30 -0
  20. data/lib/elastictastic/multi_search.rb +70 -0
  21. data/lib/elastictastic/nested_document.rb +3 -27
  22. data/lib/elastictastic/new_relic_instrumentation.rb +8 -8
  23. data/lib/elastictastic/observing.rb +8 -6
  24. data/lib/elastictastic/optimistic_locking.rb +57 -0
  25. data/lib/elastictastic/parent_child.rb +56 -54
  26. data/lib/elastictastic/persistence.rb +16 -16
  27. data/lib/elastictastic/properties.rb +136 -96
  28. data/lib/elastictastic/railtie.rb +1 -1
  29. data/lib/elastictastic/rotor.rb +105 -0
  30. data/lib/elastictastic/scope.rb +186 -56
  31. data/lib/elastictastic/server_error.rb +20 -1
  32. data/lib/elastictastic/test_helpers.rb +152 -97
  33. data/lib/elastictastic/thrift/constants.rb +12 -0
  34. data/lib/elastictastic/thrift/rest.rb +83 -0
  35. data/lib/elastictastic/thrift/types.rb +124 -0
  36. data/lib/elastictastic/thrift_adapter.rb +61 -0
  37. data/lib/elastictastic/transport_methods.rb +27 -0
  38. data/lib/elastictastic/validations.rb +11 -13
  39. data/lib/elastictastic/version.rb +1 -1
  40. data/lib/elastictastic.rb +148 -27
  41. data/spec/environment.rb +1 -1
  42. data/spec/examples/bulk_persistence_strategy_spec.rb +151 -23
  43. data/spec/examples/callbacks_spec.rb +65 -34
  44. data/spec/examples/dirty_spec.rb +160 -1
  45. data/spec/examples/document_spec.rb +168 -106
  46. data/spec/examples/middleware_spec.rb +1 -61
  47. data/spec/examples/multi_get_spec.rb +127 -0
  48. data/spec/examples/multi_search_spec.rb +113 -0
  49. data/spec/examples/observing_spec.rb +24 -3
  50. data/spec/examples/optimistic_locking_spec.rb +417 -0
  51. data/spec/examples/parent_child_spec.rb +73 -33
  52. data/spec/examples/properties_spec.rb +53 -0
  53. data/spec/examples/rotor_spec.rb +132 -0
  54. data/spec/examples/scope_spec.rb +78 -18
  55. data/spec/examples/search_spec.rb +26 -0
  56. data/spec/examples/validation_spec.rb +7 -1
  57. data/spec/models/author.rb +1 -1
  58. data/spec/models/blog.rb +2 -0
  59. data/spec/models/comment.rb +1 -1
  60. data/spec/models/photo.rb +9 -0
  61. data/spec/models/post.rb +3 -0
  62. metadata +97 -78
  63. data/lib/elastictastic/resource.rb +0 -4
  64. 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
- stub_elasticsearch_create('default', 'post')
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
- stub_elasticsearch_create('default', 'post')
16
- stub_elasticsearch_update('default', 'post', id)
17
- stub_elasticsearch_destroy('default', 'post', id)
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