elastictastic 0.5.0 → 0.10.2

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.
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