elastictastic 0.5.0

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 (57) hide show
  1. data/LICENSE +19 -0
  2. data/README.md +326 -0
  3. data/lib/elastictastic/association.rb +21 -0
  4. data/lib/elastictastic/bulk_persistence_strategy.rb +70 -0
  5. data/lib/elastictastic/callbacks.rb +30 -0
  6. data/lib/elastictastic/child_collection_proxy.rb +56 -0
  7. data/lib/elastictastic/client.rb +101 -0
  8. data/lib/elastictastic/configuration.rb +35 -0
  9. data/lib/elastictastic/dirty.rb +130 -0
  10. data/lib/elastictastic/discrete_persistence_strategy.rb +52 -0
  11. data/lib/elastictastic/document.rb +98 -0
  12. data/lib/elastictastic/errors.rb +7 -0
  13. data/lib/elastictastic/field.rb +38 -0
  14. data/lib/elastictastic/index.rb +19 -0
  15. data/lib/elastictastic/mass_assignment_security.rb +15 -0
  16. data/lib/elastictastic/middleware.rb +119 -0
  17. data/lib/elastictastic/nested_document.rb +29 -0
  18. data/lib/elastictastic/new_relic_instrumentation.rb +26 -0
  19. data/lib/elastictastic/observer.rb +3 -0
  20. data/lib/elastictastic/observing.rb +21 -0
  21. data/lib/elastictastic/parent_child.rb +115 -0
  22. data/lib/elastictastic/persistence.rb +67 -0
  23. data/lib/elastictastic/properties.rb +236 -0
  24. data/lib/elastictastic/railtie.rb +35 -0
  25. data/lib/elastictastic/resource.rb +4 -0
  26. data/lib/elastictastic/scope.rb +283 -0
  27. data/lib/elastictastic/scope_builder.rb +32 -0
  28. data/lib/elastictastic/scoped.rb +20 -0
  29. data/lib/elastictastic/search.rb +180 -0
  30. data/lib/elastictastic/server_error.rb +15 -0
  31. data/lib/elastictastic/test_helpers.rb +172 -0
  32. data/lib/elastictastic/util.rb +63 -0
  33. data/lib/elastictastic/validations.rb +45 -0
  34. data/lib/elastictastic/version.rb +3 -0
  35. data/lib/elastictastic.rb +82 -0
  36. data/spec/environment.rb +6 -0
  37. data/spec/examples/active_model_lint_spec.rb +20 -0
  38. data/spec/examples/bulk_persistence_strategy_spec.rb +233 -0
  39. data/spec/examples/callbacks_spec.rb +96 -0
  40. data/spec/examples/dirty_spec.rb +238 -0
  41. data/spec/examples/document_spec.rb +600 -0
  42. data/spec/examples/mass_assignment_security_spec.rb +13 -0
  43. data/spec/examples/middleware_spec.rb +92 -0
  44. data/spec/examples/observing_spec.rb +141 -0
  45. data/spec/examples/parent_child_spec.rb +308 -0
  46. data/spec/examples/properties_spec.rb +92 -0
  47. data/spec/examples/scope_spec.rb +491 -0
  48. data/spec/examples/search_spec.rb +382 -0
  49. data/spec/examples/spec_helper.rb +15 -0
  50. data/spec/examples/validation_spec.rb +65 -0
  51. data/spec/models/author.rb +9 -0
  52. data/spec/models/blog.rb +5 -0
  53. data/spec/models/comment.rb +5 -0
  54. data/spec/models/post.rb +41 -0
  55. data/spec/models/post_observer.rb +11 -0
  56. data/spec/support/fakeweb_request_history.rb +13 -0
  57. metadata +227 -0
@@ -0,0 +1,141 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Elastictastic::Observing do
4
+ include Elastictastic::TestHelpers
5
+
6
+ let(:id) { '123' }
7
+ let(:post) { Post.new }
8
+ let(:persisted_post) do
9
+ Post.new.tap do |post|
10
+ post.elasticsearch_hit = { '_id' => id, '_index' => 'default' }
11
+ end
12
+ end
13
+
14
+ before do
15
+ stub_elasticsearch_create('default', 'post')
16
+ stub_elasticsearch_update('default', 'post', id)
17
+ stub_elasticsearch_destroy('default', 'post', id)
18
+ Elastictastic.config.observers = [:post_observer]
19
+ Elastictastic.config.instantiate_observers
20
+ end
21
+
22
+ context 'on create' do
23
+ let(:observers) { post.observers_that_ran }
24
+
25
+ before do
26
+ post.save
27
+ end
28
+
29
+ it 'should run before_create' do
30
+ observers.should include(:before_create)
31
+ end
32
+
33
+ it 'should run after_create' do
34
+ observers.should include(:after_create)
35
+ end
36
+
37
+ it 'should run before_save' do
38
+ observers.should include(:before_save)
39
+ end
40
+
41
+ it 'should run after_save' do
42
+ observers.should include(:after_save)
43
+ end
44
+
45
+ it 'should not run before_update' do
46
+ observers.should_not include(:before_update)
47
+ end
48
+
49
+ it 'should not run after_update' do
50
+ observers.should_not include(:after_update)
51
+ end
52
+
53
+ it 'should not run before_destroy' do
54
+ observers.should_not include(:before_destroy)
55
+ end
56
+
57
+ it 'should not run after_destroy' do
58
+ observers.should_not include(:after_destroy)
59
+ end
60
+ end
61
+
62
+ context 'on update' do
63
+ let(:observers) { persisted_post.observers_that_ran }
64
+
65
+ before do
66
+ persisted_post.save
67
+ end
68
+
69
+ it 'should not run before_create' do
70
+ observers.should_not include(:before_create)
71
+ end
72
+
73
+ it 'should not run after_create' do
74
+ observers.should_not include(:after_create)
75
+ end
76
+
77
+ it 'should run before_update' do
78
+ observers.should include(:before_update)
79
+ end
80
+
81
+ it 'should run after_update' do
82
+ observers.should include(:after_update)
83
+ end
84
+
85
+ it 'should run before_save' do
86
+ observers.should include(:before_save)
87
+ end
88
+
89
+ it 'should run after_save' do
90
+ observers.should include(:after_save)
91
+ end
92
+
93
+ it 'should not run before_destroy' do
94
+ observers.should_not include(:before_destroy)
95
+ end
96
+
97
+ it 'should not run after_destroy' do
98
+ observers.should_not include(:after_destroy)
99
+ end
100
+ end
101
+
102
+ context 'on destroy' do
103
+ let(:observers) { persisted_post.observers_that_ran }
104
+
105
+ before do
106
+ persisted_post.destroy
107
+ end
108
+
109
+ it 'should not run before_create' do
110
+ observers.should_not include(:before_create)
111
+ end
112
+
113
+ it 'should not run after_create' do
114
+ observers.should_not include(:after_create)
115
+ end
116
+
117
+ it 'should not run before_update' do
118
+ observers.should_not include(:before_update)
119
+ end
120
+
121
+ it 'should not run after_update' do
122
+ observers.should_not include(:after_update)
123
+ end
124
+
125
+ it 'should not run before_save' do
126
+ observers.should_not include(:before_save)
127
+ end
128
+
129
+ it 'should not run after_save' do
130
+ observers.should_not include(:after_save)
131
+ end
132
+
133
+ it 'should run before_destroy' do
134
+ observers.should include(:before_destroy)
135
+ end
136
+
137
+ it 'should run after_destroy' do
138
+ observers.should include(:after_destroy)
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,308 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Elastictastic::ParentChild do
4
+ include Elastictastic::TestHelpers
5
+
6
+ describe 'mappings' do
7
+ it 'should put _parent in mapping' do
8
+ Post.mapping['post']['_parent'].should == { 'type' => 'blog' }
9
+ end
10
+ end
11
+
12
+ describe 'child instance' do
13
+ let(:post) { blog.posts.new }
14
+ let(:blog) do
15
+ stub_elasticsearch_create('default', 'blog')
16
+ Blog.new.tap { |blog| blog.save }
17
+ end
18
+
19
+ it 'should set parent' do
20
+ blog.posts.new.blog.should == blog
21
+ end
22
+
23
+ it 'should set parent when creating via class method' do
24
+ blog.posts.from_hash('title' => 'hey').blog.should == blog
25
+ end
26
+
27
+ describe 'discrete persistence' do
28
+ it 'should pass parent param on create' do
29
+ stub_elasticsearch_create('default', 'post')
30
+ post.save
31
+ URI.parse(FakeWeb.last_request.path).query.should == "parent=#{blog.id}"
32
+ end
33
+
34
+ it 'should pass parent param on update' do
35
+ stub_elasticsearch_create('default', 'post')
36
+ post.save
37
+ stub_elasticsearch_update('default', 'post', post.id)
38
+ post.save
39
+ URI.parse(FakeWeb.last_request.path).query.should == "parent=#{blog.id}"
40
+ end
41
+
42
+ it 'should pass parent on delete' do
43
+ stub_elasticsearch_create('default', 'post')
44
+ post = blog.posts.new
45
+ post.save
46
+ stub_elasticsearch_destroy('default', 'post', post.id)
47
+ post.destroy
48
+ URI.parse(FakeWeb.last_request.path).query.should == "parent=#{blog.id}"
49
+ end
50
+ end
51
+
52
+ describe 'bulk persistence' do
53
+ let(:bulk_requests) do
54
+ FakeWeb.last_request.body.split("\n").map do |line|
55
+ JSON.parse(line)
56
+ end
57
+ end
58
+
59
+ before do
60
+ stub_elasticsearch_bulk
61
+ end
62
+
63
+ it 'should pass parent param on create' do
64
+ post = blog.posts.new
65
+ Elastictastic.bulk { post.save }
66
+ bulk_requests.first.should == {
67
+ 'create' => {
68
+ '_index' => 'default', '_type' => 'post', 'parent' => blog.id
69
+ }
70
+ }
71
+ end
72
+
73
+ it 'should pass parent param on update' do
74
+ post = blog.posts.new
75
+ post.id = '1'
76
+ post.persisted!
77
+ Elastictastic.bulk { post.save }
78
+ bulk_requests.first.should == {
79
+ 'index' => {
80
+ '_index' => 'default',
81
+ '_type' => 'post',
82
+ '_id' => '1',
83
+ 'parent' => blog.id
84
+ }
85
+ }
86
+ end
87
+
88
+ it 'should pass parent param on delete' do
89
+ post = blog.posts.new
90
+ post.id = '1'
91
+ post.persisted!
92
+ Elastictastic.bulk { post.destroy }
93
+ bulk_requests.first.should == {
94
+ 'delete' => {
95
+ '_index' => 'default',
96
+ '_type' => 'post',
97
+ '_id' => '1',
98
+ 'parent' => blog.id
99
+ }
100
+ }
101
+ end
102
+ end
103
+
104
+ it 'should set index' do
105
+ stub_elasticsearch_create('my_index', 'blog')
106
+ blog = Blog.in_index('my_index').new
107
+ blog.save
108
+ post = blog.posts.new
109
+ post.index.name.should == 'my_index'
110
+ end
111
+ end
112
+
113
+ describe 'collection proxies' do
114
+ let(:blog) do
115
+ stub_elasticsearch_create('my_index', 'blog')
116
+ Blog.in_index('my_index').new.tap { |blog| blog.save }
117
+ end
118
+ let(:posts) { blog.posts }
119
+
120
+ it 'should by default scope query to the parent' do
121
+ posts.params.should == { 'query' => { 'constant_score' => { 'filter' => { 'term' => { '_parent' => blog.id }}}}}
122
+ end
123
+
124
+ it 'should filter existing query' do
125
+ posts.query { query_string(:query => 'bacon') }.params['query'].should ==
126
+ {
127
+ 'filtered' => {
128
+ 'query' => { 'query_string' => { 'query' => 'bacon' }},
129
+ 'filter' => { 'term' => { '_parent' => blog.id }}
130
+ }
131
+ }
132
+ end
133
+
134
+ it 'should retain other parts of scope' do
135
+ scope = posts.size(10)
136
+ scope.params['size'].should == 10
137
+ end
138
+
139
+ it 'should search correct index' do
140
+ stub_elasticsearch_scan('my_index', 'post', 100, { '_id' => '1' })
141
+ posts.to_a.first.id.should == '1'
142
+ end
143
+
144
+ it 'should set routing to parent ID on get' do
145
+ stub_elasticsearch_get('my_index', 'post', 1)
146
+ blog.posts.find(1)
147
+ URI.parse(FakeWeb.last_request.path).query.should == "routing=#{blog.id}"
148
+ end
149
+
150
+ it 'should set routing to parent ID on multiget' do
151
+ stub_elasticsearch_mget('my_index', 'post')
152
+ blog.posts.find(1, 2)
153
+ JSON.parse(FakeWeb.last_request.body).should == {
154
+ 'docs' => [
155
+ { '_id' => 1, 'routing' => blog.id },
156
+ { '_id' => 2, 'routing' => blog.id }
157
+ ]
158
+ }
159
+ end
160
+
161
+ it 'should save transient instances when parent is saved' do
162
+ post = posts.new
163
+ stub_elasticsearch_update('my_index', 'blog', blog.id)
164
+ stub_elasticsearch_create('my_index', 'post')
165
+ blog.save
166
+ post.should be_persisted
167
+ end
168
+
169
+ it 'should not attempt to save transient instances that are pending for save in a bulk block' do
170
+ stub_elasticsearch_bulk(
171
+ { 'create' => { '_index' => 'my_index', '_type' => 'post', '_id' => '123', '_version' => 1, 'ok' => true }},
172
+ { 'index' => { '_index' => 'my_index', '_type' => 'blog', '_id' => blog.id, '_version' => 2, 'ok' => true }}
173
+ )
174
+ post = blog.posts.new
175
+ Elastictastic.bulk do
176
+ post.save
177
+ blog.save
178
+ end
179
+ request_statements =
180
+ FakeWeb.last_request.body.each_line.map { |line| JSON.parse(line) }
181
+ request_statements.length.should == 4
182
+ end
183
+
184
+ it 'should not save transient instances again' do
185
+ post = posts.new
186
+ stub_elasticsearch_update('my_index', 'blog', blog.id)
187
+ stub_elasticsearch_create('my_index', 'post')
188
+ blog.save
189
+ FakeWeb.clean_registry
190
+ stub_elasticsearch_update('my_index', 'blog', blog.id)
191
+ expect { blog.save }.to_not raise_error
192
+ end
193
+
194
+ it 'should populate parent when finding one' do
195
+ stub_elasticsearch_get('my_index', 'post', '1')
196
+ blog.posts.find('1').blog.should == blog
197
+ end
198
+
199
+ it 'should populate parent when finding many' do
200
+ stub_elasticsearch_mget('my_index', 'post', '1', '2')
201
+ blog.posts.find('1', '2').each do |post|
202
+ post.blog.should == blog
203
+ end
204
+ end
205
+
206
+ it 'should populate parent when retrieving first' do
207
+ stub_elasticsearch_search(
208
+ 'my_index', 'post',
209
+ 'total' => 1,
210
+ 'hits' => { 'hits' => [{ '_id' => '2', 'index' => 'my_index', '_type' => 'post' }]}
211
+ )
212
+ blog.posts.first.blog.id.should == blog.id
213
+ end
214
+
215
+ it 'should populate parent when iterating over cursor' do
216
+ stub_elasticsearch_scan(
217
+ 'my_index', 'post', 100,
218
+ '_id' => '1'
219
+ )
220
+ blog.posts.to_a.first.blog.should == blog
221
+ end
222
+
223
+ it 'should populate parent when paginating' do
224
+ stub_elasticsearch_search(
225
+ 'my_index', 'post',
226
+ 'hits' => {
227
+ 'total' => 1,
228
+ 'hits' => [{ '_id' => '1', '_type' => 'post', '_index' => 'my_index' }]
229
+ }
230
+ )
231
+ blog.posts.from(0).to_a.first.blog.should == blog
232
+ end
233
+
234
+ it 'should iterate over transient instances along with retrieved results' do
235
+ stub_elasticsearch_scan('my_index', 'post', 100, '_id' => '1')
236
+ post = blog.posts.new
237
+ posts = blog.posts.to_a
238
+ posts[0].id.should == '1'
239
+ posts[1].should == post
240
+ end
241
+
242
+ it 'should return transient instance as #first if no persisted results' do
243
+ stub_elasticsearch_search(
244
+ 'my_index', 'post', 'hits' => { 'total' => 0, 'hits' => [] })
245
+ post = blog.posts.new
246
+ blog.posts.first.should == post
247
+ end
248
+
249
+ describe '#<<' do
250
+ let(:post) { Post.new }
251
+
252
+ before do
253
+ blog.posts << post
254
+ end
255
+
256
+ it 'should set parent of child object' do
257
+ post.blog.should == blog
258
+ end
259
+
260
+ it 'should not allow setting of a different parent' do
261
+ blog2 = Blog.new
262
+ expect { blog2.posts << post }.to raise_error(
263
+ Elastictastic::IllegalModificationError)
264
+ end
265
+
266
+ it 'should not allow setting of parent on already-persisted object' do
267
+ post = Post.new
268
+ post.persisted!
269
+ expect { blog.posts << post }.to raise_error(
270
+ Elastictastic::IllegalModificationError)
271
+ end
272
+ end
273
+ end
274
+
275
+ describe 'searching children directly' do
276
+ before do
277
+ stub_elasticsearch_search(
278
+ 'default', 'post',
279
+ 'hits' => {
280
+ 'hits' => [
281
+ {
282
+ '_id' => '1', '_index' => 'default', '_type' => 'post',
283
+ '_source' => { 'title' => 'hey' },
284
+ 'fields' => { '_parent' => '3' }
285
+ }
286
+ ]
287
+ }
288
+ )
289
+ end
290
+
291
+ let(:post) { Post.first }
292
+
293
+ it 'should provide access to parent' do
294
+ stub_elasticsearch_get('default', 'blog', '3')
295
+ post.blog.id.should == '3'
296
+ end
297
+
298
+ it 'should populate other fields from source' do
299
+ post.title.should == 'hey'
300
+ end
301
+
302
+ it 'should save post without dereferencing parent' do
303
+ stub_elasticsearch_update('default', 'post', post.id)
304
+ post.save
305
+ URI.parse(FakeWeb.last_request.path).query.should == 'parent=3'
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,92 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Elastictastic::Properties do
4
+ describe '::mapping' do
5
+ let(:mapping) { Post.mapping }
6
+ let(:properties) { mapping['post']['properties'] }
7
+
8
+ it 'should set basic field' do
9
+ properties.should have_key('title')
10
+ end
11
+
12
+ it 'should default type to string' do
13
+ properties['title']['type'].should == 'string'
14
+ end
15
+
16
+ it 'should force date format as date_time_no_millis' do
17
+ properties['published_at']['format'].should == 'date_time_no_millis'
18
+ end
19
+
20
+ it 'should accept options' do
21
+ properties['comments_count']['type'].should == 'integer'
22
+ end
23
+
24
+ it 'should set up multifield' do
25
+ properties['tags'].should == {
26
+ 'type' => 'multi_field',
27
+ 'fields' => {
28
+ 'tags' => { 'type' => 'string', 'index' => 'analyzed' },
29
+ 'non_analyzed' => { 'type' => 'string', 'index' => 'not_analyzed' }
30
+ }
31
+ }
32
+ end
33
+
34
+ it 'should map embedded object fields' do
35
+ properties['author']['properties']['id']['type'].should == 'integer'
36
+ end
37
+ end
38
+
39
+ describe '#elasticsearch_doc' do
40
+ let(:post) { Post.new }
41
+ let(:doc) { post.elasticsearch_doc }
42
+
43
+ it 'should return scalar properties' do
44
+ post.title = 'You know, for search.'
45
+ doc['title'].should == 'You know, for search.'
46
+ end
47
+
48
+ it 'should serialize dates to integers' do
49
+ time = Time.now
50
+ post.published_at = time
51
+ doc['published_at'].should == time.to_i * 1000 + time.usec / 1000
52
+ end
53
+
54
+ it 'should serialize an array of dates' do
55
+ time1, time2 = Time.now, Time.now + 60
56
+ post.published_at = [time1, time2]
57
+ doc['published_at'].should ==
58
+ [(time1.to_f * 1000).to_i, (time2.to_f * 1000).to_i]
59
+ end
60
+
61
+ it 'should not include unset values' do
62
+ doc.should_not have_key('title')
63
+ end
64
+
65
+ it 'should have embedded object properties' do
66
+ post.author = Author.new
67
+ post.author.name = 'Smedley Butler'
68
+ doc['author']['name'].should == 'Smedley Butler'
69
+ end
70
+
71
+ it 'should embed properties of arrays of embedded objects' do
72
+ authors = [Author.new, Author.new]
73
+ post.author = authors
74
+ authors[0].name = 'Smedley Butler'
75
+ authors[1].name = 'Harry S Truman'
76
+ doc['author'].should == [
77
+ { 'name' => 'Smedley Butler' },
78
+ { 'name' => 'Harry S Truman' }
79
+ ]
80
+ end
81
+
82
+ it 'should ignore missing embedded docs' do
83
+ doc.should_not have_key('author')
84
+ end
85
+ end
86
+
87
+ describe 'attributes' do
88
+ it 'should raise TypeError if improper type passed to embed setter' do
89
+ lambda { Post.new.author = Post.new }.should raise_error(TypeError)
90
+ end
91
+ end
92
+ end