search_flip 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,737 @@
1
+
2
+ module SearchFlip
3
+ # The SearchFlip::Criteria class serves the purpose of chaining various
4
+ # filtering and aggregation methods. Each chainable method creates a new
5
+ # criteria object until a method is called that finally sends the respective
6
+ # request to ElasticSearch and returns the result.
7
+ #
8
+ # @example
9
+ # CommentIndex.where(public: true).sort(id: "desc").limit(1_000).records
10
+ # CommentIndex.range(:created_at, lt: Time.parse("2014-01-01").delete
11
+ # CommentIndex.search("hello world").total_entries
12
+ # CommentIndex.query(more_like_this: { "...", fields: ["description"] })]
13
+ # CommentIndex.exists(:user_id).paginate(page: 1, per_page: 100)
14
+ # CommentIndex.sort("_doc").find_each { |comment| "..." }
15
+
16
+ class Criteria
17
+ include SearchFlip::Filterable
18
+ include SearchFlip::PostFilterable
19
+ include SearchFlip::Aggregatable
20
+ extend Forwardable
21
+
22
+ attr_accessor :target, :profile_value, :source_value, :sort_values, :highlight_values, :suggest_values, :offset_value, :limit_value,
23
+ :includes_values, :eager_load_values, :preload_values, :failsafe_value, :scroll_args, :custom_value, :terminate_after_value, :timeout_value
24
+
25
+ # Creates a new criteria while merging the attributes (constraints,
26
+ # settings, etc) of the current criteria with the attributes of another one
27
+ # passed as argument. For multi-value contstraints the resulting criteria
28
+ # will include constraints of both criterias. For single-value constraints,
29
+ # the values of the criteria passed as an argument are used.
30
+ #
31
+ # @example
32
+ # CommentIndex.where(approved: true).merge(CommentIndex.range(:created_at, gt: Time.parse("2015-01-01")))
33
+ # CommentIndex.aggregate(:user_id).merge(CommentIndex.where(admin: true))
34
+ #
35
+ # @return [SearchFlip::Criteria] A newly created extended criteria
36
+
37
+ def merge(other)
38
+ other = other.criteria
39
+
40
+ fresh.tap do |criteria|
41
+ criteria.profile_value = other.profile_value if other.profile_value != nil
42
+ criteria.source_value = (criteria.source_value || []) + other.source_value if other.source_value
43
+ criteria.sort_values = (criteria.sort_values || []) + other.sort_values if other.sort_values
44
+ criteria.highlight_values = (criteria.highlight_values || {}).merge(other.highlight_values) if other.highlight_values
45
+ criteria.suggest_values = (criteria.suggest_values || {}).merge(other.suggest_values) if other.suggest_values
46
+ criteria.offset_value = other.offset_value if other.offset_value
47
+ criteria.limit_value = other.limit_value if other.limit_value
48
+ criteria.includes_values = (criteria.includes_values || []) + other.includes_values if other.includes_values
49
+ criteria.preload_values = (criteria.preload_values || []) + other.preload_values if other.preload_values
50
+ criteria.eager_load_values = (criteria.eager_load_values || []) + other.eager_load_values if other.eager_load_values
51
+ criteria.failsafe_value = other.failsafe_value if other.failsafe_value != nil
52
+ criteria.scroll_args = other.scroll_args if other.scroll_args
53
+ criteria.custom_value = (criteria.custom_value || {}).merge(other.custom_value) if other.custom_value
54
+ criteria.search_values = (criteria.search_values || []) + other.search_values if other.search_values
55
+ criteria.must_values = (criteria.must_values || []) + other.must_values if other.must_values
56
+ criteria.must_not_values = (criteria.must_not_values || []) + other.must_not_values if other.must_not_values
57
+ criteria.should_values = (criteria.should_values || []) + other.should_values if other.should_values
58
+ criteria.filter_values = (criteria.filter_values || []) + other.filter_values if other.filter_values
59
+ criteria.post_search_values = (criteria.post_search_values || []) + other.post_search_values if other.post_search_values
60
+ criteria.post_must_values = (criteria.post_must_values || []) + other.post_must_values if other.post_must_values
61
+ criteria.post_must_not_values = (criteria.post_must_not_values || []) + other.post_must_not_values if other.post_must_not_values
62
+ criteria.post_should_values = (criteria.post_should_values || []) + other.post_should_values if other.post_should_values
63
+ criteria.post_filter_values = (criteria.post_filter_vales || []) + other.post_filter_values if other.post_filter_values
64
+ criteria.aggregation_values = (criteria.aggregation_values || {}).merge(other.aggregation_values) if other.aggregation_values
65
+ criteria.terminate_after_value = other.terminate_after_value if other.terminate_after_value != nil
66
+ criteria.timeout_value = other.timeout_value if other.timeout_value != nil
67
+ end
68
+ end
69
+
70
+ # Specifies a query timeout, such that the processing will be stopped after
71
+ # that timeout and only the results calculated up to that point will be
72
+ # processed and returned.
73
+ #
74
+ # @example
75
+ # ProductIndex.timeout("3s").search("hello world")
76
+ #
77
+ # @return [SearchFlip::Criteria] A newly created extended criteria
78
+
79
+ def timeout(n)
80
+ fresh.tap do |criteria|
81
+ criteria.timeout_value = n
82
+ end
83
+ end
84
+
85
+ # Specifies early query termination, such that the processing will be
86
+ # stopped after the specified number of results has been accumulated.
87
+ #
88
+ # @example
89
+ # ProductIndex.terminate_after(10_000).search("hello world")
90
+ #
91
+ # @return [SearchFlip::Criteria] A newly created extended criteria
92
+
93
+ def terminate_after(n)
94
+ fresh.tap do |criteria|
95
+ criteria.terminate_after_value = n
96
+ end
97
+ end
98
+
99
+ # Creates a new criteria while removing all specified scopes. Currently,
100
+ # you can unscope :search, :post_search, :sort, :highlight, :suggest, :custom
101
+ # and :aggregate.
102
+ #
103
+ # @example
104
+ # CommentIndex.search("hello world").aggregate(:username).unscope(:search, :aggregate)
105
+ #
106
+ # @param scopes [Symbol] All scopes that you want to remove
107
+ #
108
+ # @return [SearchFlip::Criteria] A newly created extended criteria
109
+
110
+ def unscope(*scopes)
111
+ unknown = scopes - [:search, :post_search, :sort, :highlight, :suggest, :custom, :aggregate]
112
+
113
+ raise(ArgumentError, "Can't unscope #{unknown.join(", ")}") if unknown.size > 0
114
+
115
+ scopes = scopes.to_set
116
+
117
+ fresh.tap do |criteria|
118
+ criteria.search_values = nil if scopes.include?(:search)
119
+ criteria.post_search_values = nil if scopes.include?(:search)
120
+ criteria.sort_values = nil if scopes.include?(:sort)
121
+ criteria.hightlight_values = nil if scopes.include?(:highlight)
122
+ criteria.suggest_values = nil if scopes.include?(:suggest)
123
+ criteria.custom_values = nil if scopes.include?(:custom)
124
+ criteria.aggregation_values = nil if scopes.include?(:aggregate)
125
+ end
126
+ end
127
+
128
+ # @api private
129
+ #
130
+ # Convenience method to have a unified conversion api.
131
+ #
132
+ # @return [SearchFlip::Criteria] Simply returns self
133
+
134
+ def criteria
135
+ self
136
+ end
137
+
138
+ # Creates a new SearchFlip::Criteria.
139
+ #
140
+ # @param attributes [Hash] Attributes to initialize the Criteria with
141
+
142
+ def initialize(attributes = {})
143
+ attributes.each do |key, value|
144
+ self.send "#{key}=", value
145
+ end
146
+ end
147
+
148
+ # Generates the request object from the attributes specified via chaining,
149
+ # like eg offset, limit, query, filters, aggregations, etc and returns a
150
+ # Hash that later gets serialized as JSON.
151
+ #
152
+ # @return [Hash] The generated request object
153
+
154
+ def request
155
+ res = {}
156
+
157
+ if must_values || search_values || must_not_values || should_values || filter_values
158
+ if SearchFlip.version.to_i >= 2
159
+ res[:query] = {
160
+ bool: {}.
161
+ merge(must_values || search_values ? { must: (must_values || []) + (search_values || [])} : {}).
162
+ merge(must_not_values ? { must_not: must_not_values } : {}).
163
+ merge(should_values ? { should: should_values } : {}).
164
+ merge(filter_values ? { filter: filter_values } : {})
165
+ }
166
+ else
167
+ filters = (filter_values || []) + (must_not_values || []).map { |must_not_value| { not: must_not_value } }
168
+
169
+ queries = {}.
170
+ merge(must_values || search_values ? { must: (must_values || []) + (search_values || []) } : {}).
171
+ merge(should_values ? { should: should_values } : {})
172
+
173
+ if filters.size > 0
174
+ res[:query] = {
175
+ filtered: {}.
176
+ merge(queries.size > 0 ? { query: { bool: queries } } : {}).
177
+ merge(filter: filters.size > 1 ? { and: filters } : filters.first)
178
+ }
179
+ else
180
+ res[:query] = { bool: queries }
181
+ end
182
+ end
183
+ end
184
+
185
+ res.update from: offset_value_with_default, size: limit_value_with_default
186
+
187
+ res[:timeout] = timeout_value if timeout_value
188
+ res[:terminate_after] = terminate_after_value if terminate_after_value
189
+ res[:highlight] = highlight_values if highlight_values
190
+ res[:suggest] = suggest_values if suggest_values
191
+ res[:sort] = sort_values if sort_values
192
+ res[:aggregations] = aggregation_values if aggregation_values
193
+
194
+ if post_must_values || post_search_values || post_must_not_values || post_should_values || post_filter_values
195
+ if SearchFlip.version.to_i >= 2
196
+ res[:post_filter] = {
197
+ bool: {}.
198
+ merge(post_must_values || post_search_values ? { must: (post_must_values || []) + (post_search_values || []) } : {}).
199
+ merge(post_must_not_values ? { must_not: post_must_not_values } : {}).
200
+ merge(post_should_values ? { should: post_should_values } : {}).
201
+ merge(post_filter_values ? { filter: post_filter_values } : {})
202
+ }
203
+ else
204
+ post_filters = (post_filter_values || []) + (post_must_not_values || []).map { |post_must_not_value| { not: post_must_not_value } }
205
+
206
+ post_queries = {}.
207
+ merge(post_must_values || post_search_values ? { must: (post_must_values || []) + (post_search_values || []) } : {}).
208
+ merge(post_should_values ? { should: post_should_values } : {})
209
+
210
+ post_filters_and_queries = post_filters + (post_queries.size > 0 ? [bool: post_queries] : [])
211
+
212
+ res[:post_filter] = post_filters_and_queries.size > 1 ? { and: post_filters_and_queries } : post_filters_and_queries.first
213
+ end
214
+ end
215
+
216
+ res[:_source] = source_value unless source_value.nil?
217
+ res[:profile] = true if profile_value
218
+
219
+ res.update(custom_value) if custom_value
220
+
221
+ res
222
+ end
223
+
224
+ # Adds highlighting of the given fields to the request.
225
+ #
226
+ # @example
227
+ # CommentIndex.highlight([:title, :message])
228
+ # CommentIndex.highlight(:title).highlight(:description)
229
+ # CommentIndex.highlight(:title, require_field_match: false)
230
+ # CommentIndex.highlight(title: { type: "fvh" })
231
+ #
232
+ # @example
233
+ # query = CommentIndex.highlight(:title).search("hello")
234
+ # query.results[0].highlight.title # => "<em>hello</em> world"
235
+ #
236
+ # @param fields [Hash, Array, String, Symbol] The fields to highligt.
237
+ # Supports raw ElasticSearch values by passing a Hash.
238
+ #
239
+ # @param options [Hash] Extra highlighting options. Check out the ElasticSearch
240
+ # docs for further details.
241
+ #
242
+ # @return [SearchFlip::Criteria] A new criteria including the highlighting
243
+
244
+ def highlight(fields, options = {})
245
+ fresh.tap do |criteria|
246
+ criteria.highlight_values = (criteria.highlight_values || {}).merge(options)
247
+
248
+ hash = if fields.is_a?(Hash)
249
+ fields
250
+ elsif fields.is_a?(Array)
251
+ fields.each_with_object({}) { |field, h| h[field] = {} }
252
+ else
253
+ { fields => {} }
254
+ end
255
+
256
+ criteria.highlight_values[:fields] = (criteria.highlight_values[:fields] || {}).merge(hash)
257
+ end
258
+ end
259
+
260
+ # Adds a suggestion section with the given name to the request.
261
+ #
262
+ # @example
263
+ # query = CommentIndex.suggest(:suggestion, text: "helo", term: { field: "message" })
264
+ # query.suggestions(:suggestion).first["text"] # => "hello"
265
+ #
266
+ # @param name [String, Symbol] The name of the suggestion section
267
+ #
268
+ # @param options [Hash] Additional suggestion options. Check out the ElasticSearch
269
+ # docs for further details.
270
+ #
271
+ # @return [SearchFlip::Criteria] A new criteria including the suggestion section
272
+
273
+ def suggest(name, options = {})
274
+ fresh.tap do |criteria|
275
+ criteria.suggest_values = (criteria.suggest_values || {}).merge(name => options)
276
+ end
277
+ end
278
+
279
+ # Sets whether or not query profiling should be enabled.
280
+ #
281
+ # @example
282
+ # query = CommentIndex.profile(true)
283
+ # query.raw_response["profile"] # => { "shards" => ... }
284
+ #
285
+ # @param value [Boolean] Whether query profiling should be enabled or not
286
+ #
287
+ # @return [SearchFlip::Criteria] A newly created extended criteria
288
+
289
+ def profile(value)
290
+ fresh.tap do |criteria|
291
+ criteria.profile_value = value
292
+ end
293
+ end
294
+
295
+ # Adds scrolling to the request with or without an already existing scroll
296
+ # id and using the specified timeout.
297
+ #
298
+ # @example
299
+ # query = CommentIndex.scroll(timeout: "5m")
300
+ #
301
+ # until query.records.empty?
302
+ # # ...
303
+ #
304
+ # query = query.scroll(id: query.scroll_id, timeout: "5m")
305
+ # end
306
+ #
307
+ # @param id [String, nil] The scroll id of the last request returned by
308
+ # SearchFlip or nil
309
+ #
310
+ # @param timeout [String] The timeout of the scroll request, ie. how long
311
+ # SearchFlip should keep the scroll handle open
312
+ #
313
+ # @return [SearchFlip::Criteria] A newly created extended criteria
314
+
315
+ def scroll(id: nil, timeout: "1m")
316
+ fresh.tap do |criteria|
317
+ criteria.scroll_args = { id: id, timeout: timeout }
318
+ end
319
+ end
320
+
321
+ # Sends a delete by query request to ElasticSearch, such that all documents
322
+ # matching the query get deleted. Please note, for certain ElasticSearch
323
+ # versions you need to install the delete-by-query plugin to get support
324
+ # for this feature. Refreshes the index if the auto_refresh is enabled.
325
+ # Raises SearchFlip::ResponseError in case any errors occur.
326
+ #
327
+ # @see SearchFlip::Config See SearchFlip::Config for auto_refresh
328
+ #
329
+ # @example
330
+ # CommentIndex.range(lt: Time.parse("2014-01-01")).delete
331
+ # CommentIndex.where(public: false).delete
332
+
333
+ def delete
334
+ _request = request.dup
335
+ _request.delete(:from)
336
+ _request.delete(:size)
337
+
338
+ if SearchFlip.version.to_i >= 5
339
+ SearchFlip::HTTPClient.post("#{target.type_url}/_delete_by_query", json: _request)
340
+ else
341
+ SearchFlip::HTTPClient.delete("#{target.type_url}/_query", json: _request)
342
+ end
343
+
344
+ target.refresh if SearchFlip::Config[:auto_refresh]
345
+
346
+ true
347
+ end
348
+
349
+ # Use to specify which fields of the source document you want ElasticSearch
350
+ # to return for each matching result.
351
+ #
352
+ # @example
353
+ # CommentIndex.source([:id, :message]).search("hello world")
354
+ #
355
+ # @param value [Array] Array listing the field names of the source document
356
+ #
357
+ # @return [SearchFlip::Criteria] A newly created extended criteria
358
+
359
+ def source(value)
360
+ fresh.tap do |criteria|
361
+ criteria.source_value = value
362
+ end
363
+ end
364
+
365
+ # Specify associations of the target model you want to include via
366
+ # ActiveRecord's or other ORM's mechanisms when records get fetched from
367
+ # the database.
368
+ #
369
+ # @example
370
+ # CommentIndex.includes(:user, :post).records
371
+ # PostIndex.includes(:comments => :user).records
372
+ #
373
+ # @param args The args that get passed to the includes method of
374
+ # ActiveRecord or other ORMs
375
+ #
376
+ # @return [SearchFlip::Criteria] A newly created extended criteria
377
+
378
+ def includes(*args)
379
+ fresh.tap do |criteria|
380
+ criteria.includes_values = (includes_values || []) + args
381
+ end
382
+ end
383
+
384
+ # Specify associations of the target model you want to eager load via
385
+ # ActiveRecord's or other ORM's mechanisms when records get fetched from
386
+ # the database.
387
+ #
388
+ # @example
389
+ # CommentIndex.eager_load(:user, :post).records
390
+ # PostIndex.eager_load(:comments => :user).records
391
+ #
392
+ # @param args The args that get passed to the eager load method of
393
+ # ActiveRecord or other ORMs
394
+ #
395
+ # @return [SearchFlip::Criteria] A newly created extended criteria
396
+
397
+ def eager_load(*args)
398
+ fresh.tap do |criteria|
399
+ criteria.eager_load_values = (eager_load_values || []) + args
400
+ end
401
+ end
402
+
403
+ # Specify associations of the target model you want to preload via
404
+ # ActiveRecord's or other ORM's mechanisms when records get fetched from
405
+ # the database.
406
+ #
407
+ # @example
408
+ # CommentIndex.preload(:user, :post).records
409
+ # PostIndex.includes(:comments => :user).records
410
+ #
411
+ # @param args The args that get passed to the preload method of
412
+ # ActiveRecord or other ORMs
413
+ #
414
+ # @return [SearchFlip::Criteria] A newly created extended criteria
415
+
416
+ def preload(*args)
417
+ fresh.tap do |criteria|
418
+ criteria.preload_values = (preload_values || []) + args
419
+ end
420
+ end
421
+
422
+ # Specify the sort order you want ElasticSearch to use for sorting the
423
+ # results. When you call this multiple times, the sort orders are appended
424
+ # to the already existing ones. The sort arguments get passed to
425
+ # ElasticSearch without modifications, such that you can use sort by
426
+ # script, etc here as well.
427
+ #
428
+ # @example Default usage
429
+ # CommentIndex.sort(:user_id, :id)
430
+ #
431
+ # # Same as
432
+ #
433
+ # CommentIndex.sort(:user_id).sort(:id)
434
+ #
435
+ # @example Default hash usage
436
+ # CommentIndex.sort(user_id: "asc").sort(id: "desc")
437
+ #
438
+ # # Same as
439
+ #
440
+ # CommentIndex.sort({ user_id: "asc" }, { id: "desc" })
441
+ #
442
+ # @example Sort by native script
443
+ # CommentIndex.sort("_script" => "sort_script", lang: "native", order: "asc", type: "number")
444
+ #
445
+ # @param args The sort values that get passed to ElasticSearch
446
+ #
447
+ # @return [SearchFlip::Criteria] A newly created extended criteria
448
+
449
+ def sort(*args)
450
+ fresh.tap do |criteria|
451
+ criteria.sort_values = (sort_values || []) + args
452
+ end
453
+ end
454
+
455
+ alias_method :order, :sort
456
+
457
+ # Specify the sort order you want ElasticSearch to use for sorting the
458
+ # results with already existing sort orders being removed.
459
+ #
460
+ # @example
461
+ # CommentIndex.sort(user_id: "asc").resort(id: "desc")
462
+ #
463
+ # # Same as
464
+ #
465
+ # CommentIndex.sort(id: "desc")
466
+ #
467
+ # @return [SearchFlip::Criteria] A newly created extended criteria
468
+ #
469
+ # @see #sort See #sort for more details
470
+
471
+ def resort(*args)
472
+ fresh.tap do |criteria|
473
+ criteria.sort_values = args
474
+ end
475
+ end
476
+
477
+ alias_method :reorder, :resort
478
+
479
+ # Adds a fully custom field/section to the request, such that upcoming or
480
+ # minor ElasticSearch features as well as other custom requirements can be
481
+ # used without having yet specialized criteria methods.
482
+ #
483
+ # @note Use with caution, because using #custom will potentiall override
484
+ # other sections like +aggregations+, +query+, +sort+, etc if you use the
485
+ # the same section names.
486
+ #
487
+ # @example
488
+ # CommentIndex.custom(section: { argument: "value" }).request
489
+ # => {:section=>{:argument=>"value"},...}
490
+ #
491
+ # @param hash [Hash] The custom section that is added to the request
492
+ #
493
+ # @return [SearchFlip::Criteria] A newly created extended criteria
494
+
495
+ def custom(hash)
496
+ fresh.tap do |criteria|
497
+ criteria.custom_value = (custom_value || {}).merge(hash)
498
+ end
499
+ end
500
+
501
+ # Sets the request offset, ie SearchFlip's from parameter that is used
502
+ # to skip results in the result set from being returned.
503
+ #
504
+ # @example
505
+ # CommentIndex.offset(100)
506
+ #
507
+ # @param n [Fixnum] The offset value, ie the number of results that are
508
+ # skipped in the result set
509
+ #
510
+ # @return [SearchFlip::Criteria] A newly created extended criteria
511
+
512
+ def offset(n)
513
+ fresh.tap do |criteria|
514
+ criteria.offset_value = n.to_i
515
+ end
516
+ end
517
+
518
+ # Returns the offset value or, if not yet set, the default limit value (0).
519
+ #
520
+ # @return [Fixnum] The offset value
521
+
522
+ def offset_value_with_default
523
+ (offset_value || 0).to_i
524
+ end
525
+
526
+ # Sets the request limit, ie ElasticSearch's size parameter that is used
527
+ # to restrict the results that get returned.
528
+ #
529
+ # @example
530
+ # CommentIndex.limit(100)
531
+ #
532
+ # @param n [Fixnum] The limit value, ie the max number of results that
533
+ # should be returned
534
+ #
535
+ # @return [SearchFlip::Criteria] A newly created extended criteria
536
+
537
+ def limit(n)
538
+ fresh.tap do |criteria|
539
+ criteria.limit_value = n.to_i
540
+ end
541
+ end
542
+
543
+ # Returns the limit value or, if not yet set, the default limit value (30).
544
+ #
545
+ # @return [Fixnum] The limit value
546
+
547
+ def limit_value_with_default
548
+ (limit_value || 30).to_i
549
+ end
550
+
551
+ # Sets pagination parameters for the criteria by using offset and limit,
552
+ # ie ElasticSearch's from and size parameters.
553
+ #
554
+ # @example
555
+ # CommentIndex.paginate(page: 3)
556
+ # CommentIndex.paginate(page: 5, per_page: 60)
557
+ #
558
+ # @param page [#to_i] The current page
559
+ # @param per_page [#to_i] The number of results per page
560
+ #
561
+ # @return [SearchFlip::Criteria] A newly created extended criteria
562
+
563
+ def paginate(page:, per_page: limit_value_with_default)
564
+ page = [page.to_i, 1].max
565
+ per_page = per_page.to_i
566
+
567
+ offset((page - 1) * per_page).limit(per_page)
568
+ end
569
+
570
+ def page(n)
571
+ paginate(page: n)
572
+ end
573
+
574
+ def per(n)
575
+ paginate(page: offset_value_with_default / limit_value_with_default + 1, per_page: n)
576
+ end
577
+
578
+ # Fetches the records specified by the criteria in batches using the
579
+ # ElasicSearch scroll API and yields each batch. The batch size and scroll
580
+ # API timeout can be specified. Check out the ElasticSearch docs for
581
+ # further details.
582
+ #
583
+ # @example
584
+ # CommentIndex.search("hello world").find_in_batches(batch_size: 100) do |batch|
585
+ # # ...
586
+ # end
587
+ #
588
+ # @param options [Hash] The options to control the fetching of batches
589
+ # @option options batch_size [Fixnum] The number of records to fetch per
590
+ # batch. Uses #limit to control the batch size.
591
+ # @option options timeout [String] The timeout per scroll request, ie how
592
+ # long ElasticSearch will keep the request handle open.
593
+ #
594
+ # @return [SearchFlip::Criteria] A newly created extended criteria
595
+
596
+ def find_in_batches(options = {})
597
+ return enum_for(:find_in_batches, options) unless block_given?
598
+
599
+ batch_size = options[:batch_size] || 1_000
600
+ timeout = options[:timeout] || "1m"
601
+
602
+ criteria = limit(batch_size).scroll(timeout: timeout)
603
+
604
+ until criteria.records.empty?
605
+ yield criteria.records
606
+
607
+ criteria = criteria.scroll(id: criteria.scroll_id, timeout: timeout)
608
+ end
609
+ end
610
+
611
+ # Fetches the records specified by the relatin in batches using the
612
+ # ElasticSearch scroll API and yields each record. The batch size and
613
+ # scroll API timeout can be specified. Check out the ElasticSearch docs for
614
+ # further details.
615
+ #
616
+ # @example
617
+ # CommentIndex.search("hello world").find_each(batch_size: 100) do |record|
618
+ # # ...
619
+ # end
620
+ #
621
+ # @param options [Hash] The options to control the fetching of batches
622
+ # @option options batch_size [Fixnum] The number of records to fetch per
623
+ # batch. Uses #limit to control the batch size.
624
+ # @option options timeout [String] The timeout per scroll request, ie how
625
+ # long ElasticSearch will keep the request handle open.
626
+ #
627
+ # @return [SearchFlip::Criteria] A newly created extended criteria
628
+
629
+ def find_each(options = {})
630
+ return enum_for(:find_each, options) unless block_given?
631
+
632
+ find_in_batches options do |batch|
633
+ batch.each do |record|
634
+ yield record
635
+ end
636
+ end
637
+ end
638
+
639
+ alias_method :each, :find_each
640
+
641
+ # Executes the search request for the current criteria, ie sends the
642
+ # request to ElasticSearch and returns the response. Connection and
643
+ # response errors will be rescued if you specify the criteria to be
644
+ # #failsafe, such that an empty response is returned instead.
645
+ #
646
+ # @param base_url An optional alternative base_url to send the request
647
+ # to for e.g. proxying
648
+ #
649
+ # @example
650
+ # response = CommentIndex.search("hello world").execute
651
+ #
652
+ # @return [SearchFlip::Response] The response object
653
+
654
+ def execute(base_url: target.base_url)
655
+ @response ||= begin
656
+ http_request = SearchFlip::HTTPClient.headers(accept: "application/json")
657
+
658
+ http_response =
659
+ if scroll_args && scroll_args[:id]
660
+ if SearchFlip.version.to_i >= 2
661
+ http_request.post("#{base_url}/_search/scroll", json: { scroll: scroll_args[:timeout], scroll_id: scroll_args[:id] })
662
+ else
663
+ http_request.headers(content_type: "text/plain").post("#{base_url}/_search/scroll", params: { scroll: scroll_args[:timeout] }, body: scroll_args[:id])
664
+ end
665
+ elsif scroll_args
666
+ http_request.post("#{target.type_url(base_url: base_url)}/_search", params: { scroll: scroll_args[:timeout] }, json: request)
667
+ else
668
+ http_request.post("#{target.type_url(base_url: base_url)}/_search", json: request)
669
+ end
670
+
671
+ SearchFlip::Response.new(self, http_response.parse)
672
+ rescue SearchFlip::ConnectionError, SearchFlip::ResponseError => e
673
+ raise e unless failsafe_value
674
+
675
+ SearchFlip::Response.new(self, "took" => 0, "hits" => { "total" => 0, "hits" => [] })
676
+ end
677
+ end
678
+
679
+ alias_method :response, :execute
680
+
681
+ # Marks the criteria to be failsafe, ie certain exceptions raised due to
682
+ # invalid queries, inavailability of ElasticSearch, etc get rescued and an
683
+ # empty criteria is returned instead.
684
+ #
685
+ # @see #execute See #execute for further details
686
+ #
687
+ # @example
688
+ # CommentIndex.search("invalid/request").execute
689
+ # # raises SearchFlip::ResponseError
690
+ #
691
+ # # ...
692
+ #
693
+ # CommentIndex.search("invalid/request").failsafe(true).execute
694
+ # # => #<SearchFlip::Response ...>
695
+ #
696
+ # @param value [Boolean] Whether or not the criteria should be failsafe
697
+ #
698
+ # @return [SearchFlip::Response] A newly created extended criteria
699
+
700
+ def failsafe(value)
701
+ fresh.tap do |criteria|
702
+ criteria.failsafe_value = value
703
+ end
704
+ end
705
+
706
+ # Returns a fresh, ie dupped, criteria with the response cache being
707
+ # cleared.
708
+ #
709
+ # @example
710
+ # CommentIndex.search("hello world").fresh
711
+ #
712
+ # @return [SearchFlip::Response] A dupped criteria with the response
713
+ # cache being cleared
714
+
715
+ def fresh
716
+ dup.tap do |criteria|
717
+ criteria.instance_variable_set(:@response, nil)
718
+ end
719
+ end
720
+
721
+ def respond_to?(name, *args)
722
+ super || target.respond_to?(name, *args)
723
+ end
724
+
725
+ def method_missing(name, *args, &block)
726
+ if target.respond_to?(name)
727
+ merge(target.send(name, *args, &block))
728
+ else
729
+ super
730
+ end
731
+ end
732
+
733
+ def_delegators :response, :total_entries, :total_count, :current_page, :previous_page, :prev_page, :next_page, :first_page?, :last_page?, :out_of_range?, :total_pages,
734
+ :hits, :ids, :count, :size, :length, :took, :aggregations, :suggestions, :scope, :results, :records, :scroll_id, :raw_response
735
+ end
736
+ end
737
+