search_flip 3.0.0.beta2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ module SearchFlip
2
+ # The SearchFlip::Sortable mixin provides the chainable #custom method to
3
+ # add arbitrary sections to the elasticsearch request
4
+
5
+ module Customable
6
+ def self.included(base)
7
+ base.class_eval do
8
+ attr_accessor :custom_value
9
+ end
10
+ end
11
+
12
+ # Adds a fully custom field/section to the request, such that upcoming or
13
+ # minor Elasticsearch features as well as other custom requirements can be
14
+ # used without having yet specialized criteria methods.
15
+ #
16
+ # @note Use with caution, because using #custom will potentiall override
17
+ # other sections like +aggregations+, +query+, +sort+, etc if you use the
18
+ # the same section names.
19
+ #
20
+ # @example
21
+ # CommentIndex.custom(section: { argument: "value" }).request
22
+ # => {:section=>{:argument=>"value"},...}
23
+ #
24
+ # @param hash [Hash] The custom section that is added to the request
25
+ #
26
+ # @return [SearchFlip::Criteria] A newly created extended criteria
27
+
28
+ def custom(hash)
29
+ fresh.tap do |criteria|
30
+ criteria.custom_value = (custom_value || {}).merge(hash)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ module SearchFlip
2
+ # The SearchFlip::Sortable mixin provides the chainable #explain method to
3
+ # control elasticsearch query explanations
4
+
5
+ module Explainable
6
+ def self.included(base)
7
+ base.class_eval do
8
+ attr_accessor :explain_value
9
+ end
10
+ end
11
+
12
+ # Specifies whether or not to enable explanation for each hit on how
13
+ # its score was computed.
14
+ #
15
+ # @example
16
+ # CommentIndex.explain(true)
17
+ #
18
+ # @param value [Boolean] The value for explain
19
+ #
20
+ # @return [SearchFlip::Criteria] A newly created extended criteria
21
+
22
+ def explain(value)
23
+ fresh.tap do |criteria|
24
+ criteria.explain_value = value
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,49 @@
1
+ module SearchFlip
2
+ # The SearchFlip::Sortable mixin provides the chainable #highlight method to
3
+ # use elasticsearch highlighting
4
+
5
+ module Highlightable
6
+ def self.included(base)
7
+ base.class_eval do
8
+ attr_accessor :highlight_values
9
+ end
10
+ end
11
+
12
+ # Adds highlighting of the given fields to the request.
13
+ #
14
+ # @example
15
+ # CommentIndex.highlight([:title, :message])
16
+ # CommentIndex.highlight(:title).highlight(:description)
17
+ # CommentIndex.highlight(:title, require_field_match: false)
18
+ # CommentIndex.highlight(title: { type: "fvh" })
19
+ #
20
+ # @example
21
+ # query = CommentIndex.highlight(:title).search("hello")
22
+ # query.results[0].highlight.title # => "<em>hello</em> world"
23
+ #
24
+ # @param fields [Hash, Array, String, Symbol] The fields to highligt.
25
+ # Supports raw Elasticsearch values by passing a Hash.
26
+ #
27
+ # @param options [Hash] Extra highlighting options. Check out the Elasticsearch
28
+ # docs for further details.
29
+ #
30
+ # @return [SearchFlip::Criteria] A new criteria including the highlighting
31
+
32
+ def highlight(fields, options = {})
33
+ fresh.tap do |criteria|
34
+ criteria.highlight_values = (criteria.highlight_values || {}).merge(options)
35
+
36
+ hash =
37
+ if fields.is_a?(Hash)
38
+ fields
39
+ elsif fields.is_a?(Array)
40
+ fields.each_with_object({}) { |field, h| h[field] = {} }
41
+ else
42
+ { fields => {} }
43
+ end
44
+
45
+ criteria.highlight_values[:fields] = (criteria.highlight_values[:fields] || {}).merge(hash)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -25,17 +25,21 @@ module SearchFlip
25
25
  client.request = request.send(method, *args)
26
26
  end
27
27
  end
28
+
29
+ ruby2_keywords method
28
30
  end
29
31
 
30
32
  [:get, :post, :put, :delete, :head].each do |method|
31
- define_method method do |*args|
33
+ define_method(method) do |*args|
32
34
  execute(method, *args)
33
35
  end
36
+
37
+ ruby2_keywords method
34
38
  end
35
39
 
36
40
  private
37
41
 
38
- def execute(method, *args)
42
+ ruby2_keywords def execute(method, *args)
39
43
  response = request.send(method, *args)
40
44
 
41
45
  raise SearchFlip::ResponseError.new(code: response.code, body: response.body.to_s) unless response.status.success?
@@ -498,7 +498,7 @@ module SearchFlip
498
498
  #
499
499
  # @see #index See #index for more details
500
500
 
501
- def import(*args)
501
+ ruby2_keywords def import(*args)
502
502
  index(*args)
503
503
  end
504
504
 
@@ -0,0 +1,21 @@
1
+ module SearchFlip
2
+ class NullInstrumenter
3
+ def instrument(name, payload = {})
4
+ start(name, payload)
5
+
6
+ begin
7
+ yield(payload) if block_given?
8
+ ensure
9
+ finish(name, payload)
10
+ end
11
+ end
12
+
13
+ def start(_name, _payload)
14
+ true
15
+ end
16
+
17
+ def finish(_name, _payload)
18
+ true
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,93 @@
1
+ module SearchFlip
2
+ # The SearchFlip::Paginatable mixin provides chainable methods to allow
3
+ # paginating the search results
4
+
5
+ module Paginatable
6
+ def self.included(base)
7
+ base.class_eval do
8
+ attr_accessor :offset_value, :limit_value
9
+ end
10
+ end
11
+
12
+ # Sets the request offset, ie SearchFlip's from parameter that is used
13
+ # to skip results in the result set from being returned.
14
+ #
15
+ # @example
16
+ # CommentIndex.offset(100)
17
+ #
18
+ # @param value [Fixnum] The offset value, ie the number of results that are
19
+ # skipped in the result set
20
+ #
21
+ # @return [SearchFlip::Criteria] A newly created extended criteria
22
+
23
+ def offset(value)
24
+ fresh.tap do |criteria|
25
+ criteria.offset_value = value.to_i
26
+ end
27
+ end
28
+
29
+ # @api private
30
+ #
31
+ # Returns the offset value or, if not yet set, the default limit value (0).
32
+ #
33
+ # @return [Fixnum] The offset value
34
+
35
+ def offset_value_with_default
36
+ (offset_value || 0).to_i
37
+ end
38
+
39
+ # Sets the request limit, ie Elasticsearch's size parameter that is used
40
+ # to restrict the results that get returned.
41
+ #
42
+ # @example
43
+ # CommentIndex.limit(100)
44
+ #
45
+ # @param value [Fixnum] The limit value, ie the max number of results that
46
+ # should be returned
47
+ #
48
+ # @return [SearchFlip::Criteria] A newly created extended criteria
49
+
50
+ def limit(value)
51
+ fresh.tap do |criteria|
52
+ criteria.limit_value = value.to_i
53
+ end
54
+ end
55
+
56
+ # @api private
57
+ #
58
+ # Returns the limit value or, if not yet set, the default limit value (30).
59
+ #
60
+ # @return [Fixnum] The limit value
61
+
62
+ def limit_value_with_default
63
+ (limit_value || 30).to_i
64
+ end
65
+
66
+ # Sets pagination parameters for the criteria by using offset and limit,
67
+ # ie Elasticsearch's from and size parameters.
68
+ #
69
+ # @example
70
+ # CommentIndex.paginate(page: 3)
71
+ # CommentIndex.paginate(page: 5, per_page: 60)
72
+ #
73
+ # @param page [#to_i] The current page
74
+ # @param per_page [#to_i] The number of results per page
75
+ #
76
+ # @return [SearchFlip::Criteria] A newly created extended criteria
77
+
78
+ def paginate(page: 1, per_page: 30)
79
+ page = [page.to_i, 1].max
80
+ per_page = per_page.to_i
81
+
82
+ offset((page - 1) * per_page).limit(per_page)
83
+ end
84
+
85
+ def page(value)
86
+ paginate(page: value, per_page: limit_value_with_default)
87
+ end
88
+
89
+ def per(value)
90
+ paginate(page: 1 + (offset_value_with_default / limit_value_with_default), per_page: value)
91
+ end
92
+ end
93
+ end
@@ -222,7 +222,7 @@ module SearchFlip
222
222
  #
223
223
  # @return [Array] An array of database records
224
224
 
225
- def records(options = {})
225
+ def records
226
226
  @records ||= begin
227
227
  sort_map = ids.each_with_index.each_with_object({}) { |(id, index), hash| hash[id.to_s] = index }
228
228
 
@@ -0,0 +1,69 @@
1
+ module SearchFlip
2
+ # The SearchFlip::Sortable mixin provides the chainable methods #sort as
3
+ # well as #resort
4
+
5
+ module Sortable
6
+ def self.included(base)
7
+ base.class_eval do
8
+ attr_accessor :sort_values
9
+
10
+ alias_method :order, :sort
11
+ end
12
+ end
13
+
14
+ # Specify the sort order you want Elasticsearch to use for sorting the
15
+ # results. When you call this multiple times, the sort orders are appended
16
+ # to the already existing ones. The sort arguments get passed to
17
+ # Elasticsearch without modifications, such that you can use sort by
18
+ # script, etc here as well.
19
+ #
20
+ # @example Default usage
21
+ # CommentIndex.sort(:user_id, :id)
22
+ #
23
+ # # Same as
24
+ #
25
+ # CommentIndex.sort(:user_id).sort(:id)
26
+ #
27
+ # @example Default hash usage
28
+ # CommentIndex.sort(user_id: "asc").sort(id: "desc")
29
+ #
30
+ # # Same as
31
+ #
32
+ # CommentIndex.sort({ user_id: "asc" }, { id: "desc" })
33
+ #
34
+ # @example Sort by native script
35
+ # CommentIndex.sort("_script" => "sort_script", lang: "native", order: "asc", type: "number")
36
+ #
37
+ # @param args The sort values that get passed to Elasticsearch
38
+ #
39
+ # @return [SearchFlip::Criteria] A newly created extended criteria
40
+
41
+ def sort(*args)
42
+ fresh.tap do |criteria|
43
+ criteria.sort_values = (sort_values || []) + args
44
+ end
45
+ end
46
+
47
+ # Specify the sort order you want Elasticsearch to use for sorting the
48
+ # results with already existing sort orders being removed.
49
+ #
50
+ # @example
51
+ # CommentIndex.sort(user_id: "asc").resort(id: "desc")
52
+ #
53
+ # # Same as
54
+ #
55
+ # CommentIndex.sort(id: "desc")
56
+ #
57
+ # @return [SearchFlip::Criteria] A newly created extended criteria
58
+ #
59
+ # @see #sort See #sort for more details
60
+
61
+ def resort(*args)
62
+ fresh.tap do |criteria|
63
+ criteria.sort_values = args
64
+ end
65
+ end
66
+
67
+ alias_method :reorder, :resort
68
+ end
69
+ end
@@ -0,0 +1,30 @@
1
+ module SearchFlip
2
+ # The SearchFlip::Sortable mixin provides the chainable #source method to
3
+ # use elasticsearch source filtering
4
+
5
+ module Sourceable
6
+ def self.included(base)
7
+ base.class_eval do
8
+ attr_accessor :source_value
9
+ end
10
+ end
11
+
12
+ # Use to specify which fields of the source document you want Elasticsearch
13
+ # to return for each matching result.
14
+ #
15
+ # @example
16
+ # CommentIndex.source([:id, :message]).search("hello world")
17
+ # CommentIndex.source(exclude: "description")
18
+ # CommentIndex.source(false)
19
+ #
20
+ # @param value Pass any allowed value to restrict the returned source
21
+ #
22
+ # @return [SearchFlip::Criteria] A newly created extended criteria
23
+
24
+ def source(value)
25
+ fresh.tap do |criteria|
26
+ criteria.source_value = value
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module SearchFlip
2
- VERSION = "3.0.0.beta2"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.post_install_message = <<~MESSAGE
22
22
  Thanks for using search_flip!
23
- When upgrading from 1.x to 2.x, please check out
23
+ When upgrading to 3.x, please check out
24
24
  https://github.com/mrkamel/search_flip/blob/master/UPDATING.md
25
25
  MESSAGE
26
26
 
@@ -37,4 +37,5 @@ Gem::Specification.new do |spec|
37
37
  spec.add_dependency "hashie"
38
38
  spec.add_dependency "http"
39
39
  spec.add_dependency "oj"
40
+ spec.add_dependency "ruby2_keywords"
40
41
  end
@@ -271,8 +271,8 @@ RSpec.describe SearchFlip::Aggregation do
271
271
 
272
272
  ProductIndex.import [product1, product2, product3, product4]
273
273
 
274
- query = ProductIndex.aggregate(categories: {}) do |agg|
275
- agg.merge(ProductIndex.where(price: 100..200)).aggregate(:category)
274
+ query = ProductIndex.aggregate(categories: {}) do |aggregation|
275
+ aggregation.merge(ProductIndex.where(price: 100..200)).aggregate(:category)
276
276
  end
277
277
 
278
278
  result = query.aggregations(:categories).category.buckets.each_with_object({}) do |bucket, hash|
@@ -282,33 +282,24 @@ RSpec.describe SearchFlip::Aggregation do
282
282
  expect(result).to eq("category1" => 2, "category2" => 1)
283
283
  end
284
284
 
285
- describe "unsupported methods" do
286
- unsupported_methods = [
287
- :profile_value, :failsafe_value, :terminate_after_value, :timeout_value, :offset_value, :limit_value,
288
- :scroll_args, :highlight_values, :suggest_values, :custom_value, :source_value, :sort_values,
289
- :includes_values, :preload_values, :eager_load_values, :post_must_values,
290
- :post_must_not_values, :post_filter_values, :preference_value,
291
- :search_type_value, :routing_value
292
- ]
285
+ describe "assignments" do
286
+ methods = [:offset_value, :limit_value, :source_value, :explain_value]
293
287
 
294
- unsupported_methods.each do |unsupported_method|
295
- it "raises a NotSupportedError #{unsupported_method}" do
296
- block = lambda do
297
- TestIndex.aggregate(field: {}) do |agg|
298
- criteria = SearchFlip::Criteria.new(target: TestIndex)
299
- criteria.send("#{unsupported_method}=", "value")
288
+ methods.each do |method|
289
+ it "replaces the values" do
290
+ aggregation = SearchFlip::Aggregation.new(target: TestIndex)
291
+ aggregation.send("#{method}=", "value1")
300
292
 
301
- agg.merge(criteria)
302
- end
303
- end
293
+ criteria = SearchFlip::Criteria.new(target: TestIndex)
294
+ criteria.send("#{method}=", "value2")
304
295
 
305
- expect(&block).to raise_error(SearchFlip::NotSupportedError)
296
+ expect(aggregation.merge(criteria).send(method)).to eq("value2")
306
297
  end
307
298
  end
308
299
  end
309
300
 
310
301
  describe "array concatenations" do
311
- methods = [:must_values, :must_not_values, :filter_values]
302
+ methods = [:sort_values, :must_values, :must_not_values, :filter_values]
312
303
 
313
304
  methods.each do |method|
314
305
  it "concatenates the values for #{method}" do
@@ -326,7 +317,7 @@ RSpec.describe SearchFlip::Aggregation do
326
317
  end
327
318
 
328
319
  describe "hash merges" do
329
- methods = [:aggregation_values]
320
+ methods = [:highlight_values, :custom_value, :aggregation_values]
330
321
 
331
322
  methods.each do |method|
332
323
  it "merges the values for #{method}" do
@@ -342,6 +333,29 @@ RSpec.describe SearchFlip::Aggregation do
342
333
  end
343
334
  end
344
335
  end
336
+
337
+ describe "unsupported methods" do
338
+ unsupported_methods = [
339
+ :profile_value, :failsafe_value, :terminate_after_value, :timeout_value, :scroll_args,
340
+ :suggest_values, :includes_values, :preload_values, :eager_load_values, :post_must_values,
341
+ :post_must_not_values, :post_filter_values, :preference_value, :search_type_value, :routing_value
342
+ ]
343
+
344
+ unsupported_methods.each do |unsupported_method|
345
+ it "raises a NotSupportedError #{unsupported_method}" do
346
+ block = lambda do
347
+ aggregation = SearchFlip::Aggregation.new(target: TestIndex)
348
+
349
+ criteria = SearchFlip::Criteria.new(target: TestIndex)
350
+ criteria.send("#{unsupported_method}=", "value")
351
+
352
+ aggregation.merge(criteria)
353
+ end
354
+
355
+ expect(&block).to raise_error(SearchFlip::NotSupportedError)
356
+ end
357
+ end
358
+ end
345
359
  end
346
360
 
347
361
  describe "#respond_to?" do
@@ -369,8 +383,8 @@ RSpec.describe SearchFlip::Aggregation do
369
383
 
370
384
  temp_index.import [product1, product2, product3, product4]
371
385
 
372
- query = temp_index.aggregate(categories: {}) do |agg|
373
- agg.merge(temp_index.with_price_range(100..200)).aggregate(:category)
386
+ query = temp_index.aggregate(categories: {}) do |aggregation|
387
+ aggregation.merge(temp_index.with_price_range(100..200)).aggregate(:category)
374
388
  end
375
389
 
376
390
  result = query.aggregations(:categories).category.buckets.each_with_object({}) do |bucket, hash|
@@ -380,4 +394,144 @@ RSpec.describe SearchFlip::Aggregation do
380
394
  expect(result).to eq("category1" => 2, "category2" => 1)
381
395
  end
382
396
  end
397
+
398
+ describe "#explain" do
399
+ it "returns the explaination" do
400
+ ProductIndex.import create(:product)
401
+
402
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
403
+ aggregation.explain(true)
404
+ end
405
+
406
+ expect(query.aggregations("top_hits").hits.hits.first.key?("_explanation")).to eq(true)
407
+ end
408
+ end
409
+
410
+ describe "#custom" do
411
+ it "adds a custom entry to the request" do
412
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
413
+ aggregation.custom(custom_key: "custom_value")
414
+ end
415
+
416
+ expect(query.request[:aggregations][:top_hits][:top_hits][:custom_key]).to eq("custom_value")
417
+ end
418
+ end
419
+
420
+ describe "#highlight" do
421
+ it "adds a custom entry to the request" do
422
+ ProductIndex.import create(:product, title: "Title highlight")
423
+
424
+ query = ProductIndex.search("title:highlight").aggregate(top_hits: { top_hits: {} }) do |aggregation|
425
+ aggregation.highlight([:title])
426
+ end
427
+
428
+ expect(query.aggregations("top_hits").hits.hits.first.highlight.title).to be_present
429
+ end
430
+ end
431
+
432
+ describe "#page" do
433
+ it "returns the respective result window" do
434
+ product1, product2 = create_list(:product, 2)
435
+
436
+ ProductIndex.import [product1, product2]
437
+
438
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
439
+ aggregation.sort(:id).per(1).page(2)
440
+ end
441
+
442
+ expect(query.aggregations("top_hits").hits.hits.first._id).to eq(product2.id.to_s)
443
+ end
444
+ end
445
+
446
+ describe "#per" do
447
+ it "returns the respective result window" do
448
+ ProductIndex.import create_list(:product, 2)
449
+
450
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
451
+ aggregation.per(1)
452
+ end
453
+
454
+ expect(query.aggregations("top_hits").hits.hits.size).to eq(1)
455
+ end
456
+ end
457
+
458
+ describe "#paginate" do
459
+ it "returns the respective result window" do
460
+ product1, product2 = create_list(:product, 2)
461
+
462
+ ProductIndex.import [product1, product2]
463
+
464
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
465
+ aggregation.sort(:id).paginate(page: 2, per_page: 1)
466
+ end
467
+
468
+ expect(query.aggregations("top_hits").hits.hits.first._id).to eq(product2.id.to_s)
469
+ end
470
+ end
471
+
472
+ describe "#limit" do
473
+ it "returns the respective result window" do
474
+ ProductIndex.import create_list(:product, 2)
475
+
476
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
477
+ aggregation.limit(1)
478
+ end
479
+
480
+ expect(query.aggregations("top_hits").hits.hits.size).to eq(1)
481
+ end
482
+ end
483
+
484
+ describe "#offset" do
485
+ it "returns the respective result window" do
486
+ product1, product2 = create_list(:product, 2)
487
+
488
+ ProductIndex.import [product1, product2]
489
+
490
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
491
+ aggregation.sort(:id).limit(1).offset(1)
492
+ end
493
+
494
+ expect(query.aggregations("top_hits").hits.hits.first._id).to eq(product2.id.to_s)
495
+ end
496
+ end
497
+
498
+ describe "#sort" do
499
+ it "returns the results in the specified order" do
500
+ product1, product2 = create_list(:product, 2)
501
+
502
+ ProductIndex.import [product1, product2]
503
+
504
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
505
+ aggregation.sort(id: "desc")
506
+ end
507
+
508
+ expect(query.aggregations("top_hits").hits.hits.map(&:_id)).to eq([product2, product1].map(&:id).map(&:to_s))
509
+ end
510
+ end
511
+
512
+ describe "#resort" do
513
+ it "overrides the previous sorting and returns the results in the specified order" do
514
+ product1, product2 = create_list(:product, 2)
515
+
516
+ ProductIndex.import [product1, product2]
517
+
518
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
519
+ aggregation.sort(id: "desc").resort(:id)
520
+ end
521
+
522
+ expect(query.aggregations("top_hits").hits.hits.map(&:_id)).to eq([product1, product2].map(&:id).map(&:to_s))
523
+ end
524
+ end
525
+
526
+ describe "#source" do
527
+ it "returns the specified fields only" do
528
+ ProductIndex.import create(:product)
529
+
530
+ query = ProductIndex.aggregate(top_hits: { top_hits: {} }) do |aggregation|
531
+ aggregation.source([:id, :title])
532
+ end
533
+
534
+ expect(query.aggregations("top_hits").hits.hits.first._source.keys).to eq(["id", "title"])
535
+ end
536
+ end
383
537
  end