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.
- data/LICENSE +19 -0
- data/README.md +326 -0
- data/lib/elastictastic/association.rb +21 -0
- data/lib/elastictastic/bulk_persistence_strategy.rb +70 -0
- data/lib/elastictastic/callbacks.rb +30 -0
- data/lib/elastictastic/child_collection_proxy.rb +56 -0
- data/lib/elastictastic/client.rb +101 -0
- data/lib/elastictastic/configuration.rb +35 -0
- data/lib/elastictastic/dirty.rb +130 -0
- data/lib/elastictastic/discrete_persistence_strategy.rb +52 -0
- data/lib/elastictastic/document.rb +98 -0
- data/lib/elastictastic/errors.rb +7 -0
- data/lib/elastictastic/field.rb +38 -0
- data/lib/elastictastic/index.rb +19 -0
- data/lib/elastictastic/mass_assignment_security.rb +15 -0
- data/lib/elastictastic/middleware.rb +119 -0
- data/lib/elastictastic/nested_document.rb +29 -0
- data/lib/elastictastic/new_relic_instrumentation.rb +26 -0
- data/lib/elastictastic/observer.rb +3 -0
- data/lib/elastictastic/observing.rb +21 -0
- data/lib/elastictastic/parent_child.rb +115 -0
- data/lib/elastictastic/persistence.rb +67 -0
- data/lib/elastictastic/properties.rb +236 -0
- data/lib/elastictastic/railtie.rb +35 -0
- data/lib/elastictastic/resource.rb +4 -0
- data/lib/elastictastic/scope.rb +283 -0
- data/lib/elastictastic/scope_builder.rb +32 -0
- data/lib/elastictastic/scoped.rb +20 -0
- data/lib/elastictastic/search.rb +180 -0
- data/lib/elastictastic/server_error.rb +15 -0
- data/lib/elastictastic/test_helpers.rb +172 -0
- data/lib/elastictastic/util.rb +63 -0
- data/lib/elastictastic/validations.rb +45 -0
- data/lib/elastictastic/version.rb +3 -0
- data/lib/elastictastic.rb +82 -0
- data/spec/environment.rb +6 -0
- data/spec/examples/active_model_lint_spec.rb +20 -0
- data/spec/examples/bulk_persistence_strategy_spec.rb +233 -0
- data/spec/examples/callbacks_spec.rb +96 -0
- data/spec/examples/dirty_spec.rb +238 -0
- data/spec/examples/document_spec.rb +600 -0
- data/spec/examples/mass_assignment_security_spec.rb +13 -0
- data/spec/examples/middleware_spec.rb +92 -0
- data/spec/examples/observing_spec.rb +141 -0
- data/spec/examples/parent_child_spec.rb +308 -0
- data/spec/examples/properties_spec.rb +92 -0
- data/spec/examples/scope_spec.rb +491 -0
- data/spec/examples/search_spec.rb +382 -0
- data/spec/examples/spec_helper.rb +15 -0
- data/spec/examples/validation_spec.rb +65 -0
- data/spec/models/author.rb +9 -0
- data/spec/models/blog.rb +5 -0
- data/spec/models/comment.rb +5 -0
- data/spec/models/post.rb +41 -0
- data/spec/models/post_observer.rb +11 -0
- data/spec/support/fakeweb_request_history.rb +13 -0
- metadata +227 -0
@@ -0,0 +1,491 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
# This spec describes the behavior of Elastictastic when interacting with
|
4
|
+
# ElasticSearch to perform searches and related lookups. For behavior relating
|
5
|
+
# to the construction of search scopes, see spec/examples/search_spec
|
6
|
+
describe Elastictastic::Scope do
|
7
|
+
include Elastictastic::TestHelpers
|
8
|
+
|
9
|
+
let(:last_request) { FakeWeb.last_request }
|
10
|
+
let(:last_request_body) { JSON.parse(last_request.body) }
|
11
|
+
let(:last_request_path) { last_request.path.split('?', 2)[0] }
|
12
|
+
let(:last_request_params) { last_request.path.split('?', 2)[1].try(:split, '&') }
|
13
|
+
|
14
|
+
describe '#each' do
|
15
|
+
let(:noop) { proc { |arg| } }
|
16
|
+
|
17
|
+
context 'with query only' do
|
18
|
+
let(:scope) { Post.all.fields('title') }
|
19
|
+
let(:scan_request) { FakeWeb.requests[0] }
|
20
|
+
let(:scroll_requests) { FakeWeb.requests[1..-1] }
|
21
|
+
|
22
|
+
before do
|
23
|
+
@scroll_ids = stub_elasticsearch_scan(
|
24
|
+
'default', 'post', 2, *make_hits(3)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should return all contact documents' do
|
29
|
+
scope.map { |doc| doc.id }.should == %w(1 2 3)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should mark contact documents persisted' do
|
33
|
+
scope.each { |doc| doc.should be_persisted }
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'initiating scan search' do
|
37
|
+
before { scope.to_a }
|
38
|
+
|
39
|
+
it 'should make request to index/type search endpoint' do
|
40
|
+
scan_request.path.split('?').first.should == '/default/post/_search'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should send query in data for initial search' do
|
44
|
+
scan_request.body.should == scope.params.to_json
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should send POST request initially' do
|
48
|
+
scan_request.method.should == 'POST'
|
49
|
+
end
|
50
|
+
end # describe 'initiating scan search'
|
51
|
+
|
52
|
+
context 'with options specified' do
|
53
|
+
before { scope.find_each(:batch_size => 20, :ttl => 30, &noop) }
|
54
|
+
|
55
|
+
it 'should make request to index/type search endpoint with batch size and TTL' do
|
56
|
+
scan_request.path.split('?').last.split('&').should =~
|
57
|
+
%w(search_type=scan scroll=30s size=20)
|
58
|
+
end
|
59
|
+
end # context 'with options specified'
|
60
|
+
|
61
|
+
describe 'paging through cursor' do
|
62
|
+
before { scope.to_a }
|
63
|
+
|
64
|
+
it 'should make request to scan search endpoint' do
|
65
|
+
scroll_requests.each { |request| request.path.split('?').first.should == '/_search/scroll' }
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should send id in body' do
|
69
|
+
scroll_requests.map { |request| request.body }.should == @scroll_ids
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should include scroll param in each request' do
|
73
|
+
scroll_requests.each do |request|
|
74
|
+
request.path.split('?')[1].split('&').should include('scroll=60s')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end # describe 'paging through cursor'
|
78
|
+
end # context 'with query only'
|
79
|
+
|
80
|
+
context 'with from/size' do
|
81
|
+
let(:scope) { Post.from(10).size(10) }
|
82
|
+
|
83
|
+
before do
|
84
|
+
stub_elasticsearch_search(
|
85
|
+
'default', 'post', 'hits' => {
|
86
|
+
'total' => 2,
|
87
|
+
'hits' => make_hits(2)
|
88
|
+
}
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should send request to search endpoint' do
|
93
|
+
scope.to_a
|
94
|
+
last_request_path.should == '/default/post/_search'
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should send query in body' do
|
98
|
+
scope.to_a
|
99
|
+
last_request_body.should == scope.params
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should perform query_then_fetch search' do
|
103
|
+
scope.to_a
|
104
|
+
last_request_params.should include('search_type=query_then_fetch')
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should return documents' do
|
108
|
+
scope.map { |post| post.title }.should == ['Post 1', 'Post 2']
|
109
|
+
end
|
110
|
+
end # context 'with from/size'
|
111
|
+
|
112
|
+
context 'with sort but no from/size' do
|
113
|
+
let(:scope) { Post.sort(:title => 'asc') }
|
114
|
+
let(:requests) { FakeWeb.requests }
|
115
|
+
let(:request_bodies) do
|
116
|
+
requests.map do |request|
|
117
|
+
JSON.parse(request.body)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
before do
|
122
|
+
Elastictastic.config.default_batch_size = 2
|
123
|
+
stub_elasticsearch_search(
|
124
|
+
'default', 'post',
|
125
|
+
make_hits(3).each_slice(2).map { |batch| { 'hits' => { 'hits' => batch, 'total' => 3 }} }
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
after { Elastictastic.config.default_batch_size = nil }
|
130
|
+
|
131
|
+
it 'should send two requests' do
|
132
|
+
scope.to_a
|
133
|
+
requests.length.should == 2
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should send requests to search endpoint' do
|
137
|
+
scope.to_a
|
138
|
+
requests.each do |request|
|
139
|
+
request.path.split('?').first.should == '/default/post/_search'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should send from' do
|
144
|
+
scope.to_a
|
145
|
+
request_bodies.each_with_index do |body, i|
|
146
|
+
body['from'].should == i * 2
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should send size' do
|
151
|
+
scope.to_a
|
152
|
+
request_bodies.each do |body|
|
153
|
+
body['size'].should == 2
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should perform query_then_fetch search' do
|
158
|
+
scope.to_a
|
159
|
+
requests.each do |request|
|
160
|
+
request.path.should =~ /[\?&]search_type=query_then_fetch(&|$)/
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'should return all results' do
|
165
|
+
scope.map { |post| post.id }.should == (1..3).map(&:to_s)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should mark documents persisent' do
|
169
|
+
scope.each { |post| post.should be_persisted }
|
170
|
+
end
|
171
|
+
end # context 'with sort but no from/size'
|
172
|
+
end # describe '#each'
|
173
|
+
|
174
|
+
describe 'hit metadata' do
|
175
|
+
before { Elastictastic.config.default_batch_size = 2 }
|
176
|
+
after { Elastictastic.config.default_batch_size = nil }
|
177
|
+
|
178
|
+
let(:hits) do
|
179
|
+
make_hits(3) do |hit, i|
|
180
|
+
hit.merge('highlight' => { 'title' => ["pizza #{i}"] })
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
shared_examples_for 'enumerator with hit metadata' do
|
185
|
+
it 'should yield from each batch #find_in_batches' do
|
186
|
+
i = -1
|
187
|
+
scope.find_in_batches do |batch|
|
188
|
+
batch.each do |post, hit|
|
189
|
+
hit.highlight['title'].first.should == "pizza #{i += 1}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'should yield from #find_each' do
|
195
|
+
i = -1
|
196
|
+
scope.find_each do |post, hit|
|
197
|
+
hit.highlight['title'].first.should == "pizza #{i += 1}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context 'in scan search' do
|
203
|
+
let(:scope) { Post }
|
204
|
+
|
205
|
+
before do
|
206
|
+
stub_elasticsearch_scan('default', 'post', 2, *hits)
|
207
|
+
end
|
208
|
+
|
209
|
+
it_should_behave_like 'enumerator with hit metadata'
|
210
|
+
end
|
211
|
+
|
212
|
+
context 'in paginated search' do
|
213
|
+
let(:scope) { Post.sort('title' => 'desc') }
|
214
|
+
|
215
|
+
before do
|
216
|
+
batches = hits.each_slice(2).map { |batch| { 'hits' => { 'hits' => batch, 'total' => 3 }} }
|
217
|
+
stub_elasticsearch_search('default', 'post', batches)
|
218
|
+
end
|
219
|
+
|
220
|
+
it_should_behave_like 'enumerator with hit metadata'
|
221
|
+
end
|
222
|
+
|
223
|
+
context 'in single-page search' do
|
224
|
+
let(:scope) { Post.size(3) }
|
225
|
+
|
226
|
+
before do
|
227
|
+
stub_elasticsearch_search('default', 'post', 'hits' => { 'hits' => hits, 'total' => 3 })
|
228
|
+
end
|
229
|
+
|
230
|
+
it_should_behave_like 'enumerator with hit metadata'
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
describe '#count' do
|
235
|
+
context 'with no operations performed yet' do
|
236
|
+
let!(:count) do
|
237
|
+
stub_elasticsearch_search('default', 'post', 'hits' => { 'total' => 3 })
|
238
|
+
Post.all.count
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'should send search_type as count' do
|
242
|
+
last_request_params.should include('search_type=count')
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'should get count' do
|
246
|
+
count.should == 3
|
247
|
+
end
|
248
|
+
end # context 'with no operations performed yet'
|
249
|
+
|
250
|
+
context 'with scan search performed' do
|
251
|
+
let!(:count) do
|
252
|
+
stub_elasticsearch_scan(
|
253
|
+
'default', 'post', 2, *make_hits(3)
|
254
|
+
)
|
255
|
+
scope = Post.all
|
256
|
+
scope.to_a
|
257
|
+
scope.count
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'should get count from scan request' do
|
261
|
+
count.should == 3
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'should not send count request' do
|
265
|
+
FakeWeb.should have(4).requests
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
context 'with paginated search performed' do
|
270
|
+
let!(:count) do
|
271
|
+
stub_elasticsearch_search(
|
272
|
+
'default', 'post', 'hits' => {
|
273
|
+
'hits' => make_hits(3),
|
274
|
+
'total' => 3
|
275
|
+
}
|
276
|
+
)
|
277
|
+
scope = Post.size(10)
|
278
|
+
scope.to_a
|
279
|
+
scope.count
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'should get count from query_then_fetch search' do
|
283
|
+
count.should == 3
|
284
|
+
end
|
285
|
+
|
286
|
+
it 'should not perform extra request' do
|
287
|
+
FakeWeb.should have(1).request
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
context 'with paginated scan performed' do
|
292
|
+
let!(:count) do
|
293
|
+
stub_elasticsearch_search(
|
294
|
+
'default', 'post', 'hits' => {
|
295
|
+
'hits' => make_hits(2),
|
296
|
+
'total' => 2
|
297
|
+
}
|
298
|
+
)
|
299
|
+
scope = Post.sort('title' => 'asc')
|
300
|
+
scope.to_a
|
301
|
+
scope.count
|
302
|
+
end
|
303
|
+
|
304
|
+
it 'should return count from internal paginated request' do
|
305
|
+
count.should == 2
|
306
|
+
end
|
307
|
+
|
308
|
+
it 'should not perform extra request' do
|
309
|
+
FakeWeb.should have(1).request
|
310
|
+
end
|
311
|
+
end # context 'with paginated scan performed'
|
312
|
+
end # describe '#count'
|
313
|
+
|
314
|
+
describe '#all_facets' do
|
315
|
+
let(:facet_response) do
|
316
|
+
{
|
317
|
+
'comments_count' => {
|
318
|
+
'_type' => 'terms', 'total' => 2,
|
319
|
+
'terms' => [
|
320
|
+
{ 'term' => 4, 'count' => 1 },
|
321
|
+
{ 'term' => 2, 'count' => 1 }
|
322
|
+
]
|
323
|
+
}
|
324
|
+
}
|
325
|
+
end
|
326
|
+
let(:base_scope) { Post.facets(:comments_count => { :terms => { :field => :comments_count }}) }
|
327
|
+
|
328
|
+
context 'with no requests performed' do
|
329
|
+
let!(:facets) do
|
330
|
+
stub_elasticsearch_search(
|
331
|
+
'default', 'post',
|
332
|
+
'hits' => { 'hits' => [], 'total' => 2 },
|
333
|
+
'facets' => facet_response
|
334
|
+
)
|
335
|
+
base_scope.all_facets
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'should make count search_type' do
|
339
|
+
last_request_params.should include("search_type=count")
|
340
|
+
end
|
341
|
+
|
342
|
+
it 'should expose facets with object traversal' do
|
343
|
+
facets.comments_count.terms.first.term.should == 4
|
344
|
+
end
|
345
|
+
end # context 'with no requests performed'
|
346
|
+
|
347
|
+
context 'with count request performed' do
|
348
|
+
let!(:facets) do
|
349
|
+
stub_elasticsearch_search(
|
350
|
+
'default', 'post',
|
351
|
+
'hits' => { 'hits' => [], 'total' => 2 },
|
352
|
+
'facets' => facet_response
|
353
|
+
)
|
354
|
+
base_scope.count
|
355
|
+
base_scope.all_facets
|
356
|
+
end
|
357
|
+
|
358
|
+
it 'should only perform one request' do
|
359
|
+
FakeWeb.should have(1).request
|
360
|
+
end
|
361
|
+
|
362
|
+
it 'should set facets' do
|
363
|
+
facets.comments_count.should be
|
364
|
+
end
|
365
|
+
end # context 'with count request performed'
|
366
|
+
|
367
|
+
context 'with single-page search performed' do
|
368
|
+
let!(:facets) do
|
369
|
+
stub_elasticsearch_search(
|
370
|
+
'default', 'post',
|
371
|
+
'hits' => { 'hits' => make_hits(2), 'total' => 2 },
|
372
|
+
'facets' => facet_response
|
373
|
+
)
|
374
|
+
scope = base_scope.size(10)
|
375
|
+
scope.to_a
|
376
|
+
scope.all_facets
|
377
|
+
end
|
378
|
+
|
379
|
+
it 'should only perform one request' do
|
380
|
+
FakeWeb.should have(1).request
|
381
|
+
end
|
382
|
+
|
383
|
+
it 'should get facets' do
|
384
|
+
facets.comments_count.should be
|
385
|
+
end
|
386
|
+
end # context 'with single-page search performed'
|
387
|
+
|
388
|
+
context 'with multi-page search performed' do
|
389
|
+
let!(:facets) do
|
390
|
+
stub_elasticsearch_search(
|
391
|
+
'default', 'post',
|
392
|
+
'hits' => { 'hits' => make_hits(2), 'total' => 2 },
|
393
|
+
'facets' => facet_response
|
394
|
+
)
|
395
|
+
scope = base_scope.sort(:comments_count => :asc)
|
396
|
+
scope.to_a
|
397
|
+
scope.all_facets
|
398
|
+
end
|
399
|
+
|
400
|
+
it 'should only peform one request' do
|
401
|
+
FakeWeb.should have(1).request
|
402
|
+
end
|
403
|
+
|
404
|
+
it 'should populate facets' do
|
405
|
+
facets.comments_count.should be
|
406
|
+
end
|
407
|
+
end # context 'with multi-page search performed'
|
408
|
+
end # describe '#all_facets'
|
409
|
+
|
410
|
+
describe '#first' do
|
411
|
+
shared_examples_for 'first method' do
|
412
|
+
before do
|
413
|
+
stub_elasticsearch_search(
|
414
|
+
index, 'post', 'hits' => {
|
415
|
+
'total' => 12,
|
416
|
+
'hits' => make_hits(1)
|
417
|
+
}
|
418
|
+
)
|
419
|
+
end
|
420
|
+
|
421
|
+
it 'should retrieve first document' do
|
422
|
+
scope.first.id.should == '1'
|
423
|
+
end
|
424
|
+
|
425
|
+
it 'should mark document persisted' do
|
426
|
+
scope.first.should be_persisted
|
427
|
+
end
|
428
|
+
|
429
|
+
it 'should send size param' do
|
430
|
+
scope.first
|
431
|
+
last_request_body['size'].should == 1
|
432
|
+
end
|
433
|
+
|
434
|
+
it 'should send scope params' do
|
435
|
+
scope.first
|
436
|
+
last_request_body['query'].should == scope.all.params['query']
|
437
|
+
end
|
438
|
+
|
439
|
+
it 'should send from param' do
|
440
|
+
scope.first
|
441
|
+
last_request_body['from'].should == 0
|
442
|
+
end
|
443
|
+
|
444
|
+
it 'should override from and size param in scope' do
|
445
|
+
scope.from(10).size(10).first
|
446
|
+
last_request_body['from'].should == 0
|
447
|
+
last_request_body['size'].should == 1
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
describe 'called on class singleton' do
|
452
|
+
let(:scope) { Post }
|
453
|
+
let(:index) { 'default' }
|
454
|
+
|
455
|
+
it_should_behave_like 'first method'
|
456
|
+
end
|
457
|
+
|
458
|
+
describe 'called on index proxy' do
|
459
|
+
let(:scope) { Post.in_index('my_index') }
|
460
|
+
let(:index) { 'my_index' }
|
461
|
+
|
462
|
+
it_should_behave_like 'first method'
|
463
|
+
end
|
464
|
+
|
465
|
+
describe 'called on scope' do
|
466
|
+
let(:scope) { Post.query { match_all }}
|
467
|
+
let(:index) { 'default' }
|
468
|
+
|
469
|
+
it_should_behave_like 'first method'
|
470
|
+
end
|
471
|
+
|
472
|
+
describe 'called on scope with index proxy' do
|
473
|
+
let(:scope) { Post.in_index('my_index').query { match_all }}
|
474
|
+
let(:index) { 'my_index' }
|
475
|
+
|
476
|
+
it_should_behave_like 'first method'
|
477
|
+
end
|
478
|
+
end # describe '#first'
|
479
|
+
|
480
|
+
def make_hits(count)
|
481
|
+
Array.new(count) do |i|
|
482
|
+
hit = {
|
483
|
+
'_id' => (i + 1).to_s,
|
484
|
+
'_type' => 'post',
|
485
|
+
'_index' => 'default',
|
486
|
+
'_source' => { 'title' => "Post #{i + 1}" }
|
487
|
+
}
|
488
|
+
block_given? ? yield(hit, i) : hit
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|