caoutsearch 0.0.0 → 0.0.3

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +819 -6
  3. data/lib/caoutsearch/config/mappings.rb +1 -1
  4. data/lib/caoutsearch/filter/base.rb +11 -7
  5. data/lib/caoutsearch/filter/boolean.rb +1 -1
  6. data/lib/caoutsearch/filter/date.rb +93 -22
  7. data/lib/caoutsearch/filter/default.rb +10 -10
  8. data/lib/caoutsearch/filter/geo_point.rb +1 -1
  9. data/lib/caoutsearch/filter/match.rb +5 -5
  10. data/lib/caoutsearch/filter/none.rb +1 -1
  11. data/lib/caoutsearch/filter/range.rb +6 -6
  12. data/lib/caoutsearch/index/document.rb +11 -11
  13. data/lib/caoutsearch/index/indice_versions.rb +3 -3
  14. data/lib/caoutsearch/index/internal_dsl.rb +3 -3
  15. data/lib/caoutsearch/index/reindex.rb +11 -11
  16. data/lib/caoutsearch/index/scoping.rb +4 -4
  17. data/lib/caoutsearch/index/serialization.rb +13 -13
  18. data/lib/caoutsearch/instrumentation/base.rb +12 -12
  19. data/lib/caoutsearch/instrumentation/search.rb +11 -2
  20. data/lib/caoutsearch/mappings.rb +1 -1
  21. data/lib/caoutsearch/model/indexable.rb +57 -0
  22. data/lib/caoutsearch/model/searchable.rb +31 -0
  23. data/lib/caoutsearch/model.rb +12 -0
  24. data/lib/caoutsearch/response/aggregations.rb +50 -0
  25. data/lib/caoutsearch/response/response.rb +9 -0
  26. data/lib/caoutsearch/response/suggestions.rb +9 -0
  27. data/lib/caoutsearch/response.rb +6 -0
  28. data/lib/caoutsearch/search/adapter/active_record.rb +39 -0
  29. data/lib/caoutsearch/search/base.rb +16 -15
  30. data/lib/caoutsearch/search/batch/scroll.rb +93 -0
  31. data/lib/caoutsearch/search/batch/search_after.rb +70 -0
  32. data/lib/caoutsearch/search/batch_methods.rb +63 -0
  33. data/lib/caoutsearch/search/callbacks.rb +28 -0
  34. data/lib/caoutsearch/search/delete_methods.rb +19 -0
  35. data/lib/caoutsearch/search/dsl/item.rb +2 -2
  36. data/lib/caoutsearch/search/inspect.rb +34 -0
  37. data/lib/caoutsearch/search/instrumentation.rb +19 -0
  38. data/lib/caoutsearch/search/internal_dsl.rb +107 -0
  39. data/lib/caoutsearch/search/naming.rb +45 -0
  40. data/lib/caoutsearch/search/point_in_time.rb +28 -0
  41. data/lib/caoutsearch/search/query/boolean.rb +4 -4
  42. data/lib/caoutsearch/search/query/nested.rb +1 -1
  43. data/lib/caoutsearch/search/query/setters.rb +4 -4
  44. data/lib/caoutsearch/search/query_builder/aggregations.rb +49 -0
  45. data/lib/caoutsearch/search/query_builder.rb +89 -0
  46. data/lib/caoutsearch/search/query_methods.rb +157 -0
  47. data/lib/caoutsearch/search/records.rb +23 -0
  48. data/lib/caoutsearch/search/resettable.rb +38 -0
  49. data/lib/caoutsearch/search/response.rb +97 -0
  50. data/lib/caoutsearch/search/sanitizer.rb +2 -2
  51. data/lib/caoutsearch/search/search_methods.rb +239 -0
  52. data/lib/caoutsearch/search/type_cast.rb +14 -6
  53. data/lib/caoutsearch/search/value.rb +10 -10
  54. data/lib/caoutsearch/search/value_overflow.rb +1 -1
  55. data/lib/caoutsearch/settings.rb +1 -1
  56. data/lib/caoutsearch/testing/mock_requests.rb +105 -0
  57. data/lib/caoutsearch/testing.rb +3 -0
  58. data/lib/caoutsearch/version.rb +1 -1
  59. data/lib/caoutsearch.rb +10 -5
  60. metadata +44 -126
  61. data/lib/caoutsearch/search/search/delete_methods.rb +0 -21
  62. data/lib/caoutsearch/search/search/inspect.rb +0 -36
  63. data/lib/caoutsearch/search/search/instrumentation.rb +0 -21
  64. data/lib/caoutsearch/search/search/internal_dsl.rb +0 -77
  65. data/lib/caoutsearch/search/search/naming.rb +0 -47
  66. data/lib/caoutsearch/search/search/query_builder.rb +0 -94
  67. data/lib/caoutsearch/search/search/query_methods.rb +0 -180
  68. data/lib/caoutsearch/search/search/resettable.rb +0 -35
  69. data/lib/caoutsearch/search/search/response.rb +0 -88
  70. data/lib/caoutsearch/search/search/scroll_methods.rb +0 -113
  71. data/lib/caoutsearch/search/search/search_methods.rb +0 -230
@@ -8,10 +8,19 @@ module Caoutsearch
8
8
  log_response("Search", event)
9
9
  end
10
10
 
11
+ def search_after(event)
12
+ log_request("SearchAfter", event, format: "truncated")
13
+ log_response("SearchAfter", event) do |message|
14
+ payload = event.payload
15
+ message += ", progress: #{payload[:progress]} / #{payload[:total]}"
16
+ message
17
+ end
18
+ end
19
+
11
20
  def scroll_search(event)
12
21
  log_request("Search", event, format: log_request_format)
13
22
  log_response("Search", event) do |message|
14
- payload = event.payload
23
+ payload = event.payload
15
24
  message += ", progress: #{payload[:progress]} / #{payload[:total]}"
16
25
  message
17
26
  end
@@ -20,7 +29,7 @@ module Caoutsearch
20
29
  def scroll(event)
21
30
  log_request("Scroll", event, format: "truncated")
22
31
  log_response("Scroll", event) do |message|
23
- payload = event.payload
32
+ payload = event.payload
24
33
  message += ", progress: #{payload[:progress]} / #{payload[:total]}"
25
34
  message
26
35
  end
@@ -18,7 +18,7 @@ module Caoutsearch
18
18
  alias_method :as_json, :to_hash
19
19
 
20
20
  def to_json(*)
21
- as_json.to_json
21
+ MultiJson.dump(as_json)
22
22
  end
23
23
 
24
24
  def include?(*args)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Model
5
+ module Indexable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :index_engine_class
10
+ end
11
+
12
+ class_methods do
13
+ def index_with(index_class)
14
+ self.index_engine_class = index_class
15
+
16
+ extend ClassMethods
17
+ include InstanceMethods
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+ def reindex(*properties, **options, &block)
23
+ index_engine_class.reindex(all, *properties, **options, &block)
24
+ end
25
+
26
+ def delete_indexes
27
+ find_in_batches do |batch|
28
+ ids = batch.map(&:id)
29
+ index_engine_class.delete_documents(ids)
30
+ end
31
+ end
32
+
33
+ def delete_index(id)
34
+ index_engine_class.delete_document(id)
35
+ end
36
+ end
37
+
38
+ module InstanceMethods
39
+ def as_indexed_json(*properties)
40
+ index_engine_class.new(self).as_json(*properties)
41
+ end
42
+
43
+ def indexed_document
44
+ index_engine_class.new(self).indexed_document
45
+ end
46
+
47
+ def update_index(*properties)
48
+ index_engine_class.new(self).update_document(*properties)
49
+ end
50
+
51
+ def delete_index
52
+ index_engine_class.new(self).delete_document
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Model
5
+ module Searchable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :search_engine_class
10
+ end
11
+
12
+ class_methods do
13
+ def search_with(search_class)
14
+ self.search_engine_class = search_class
15
+
16
+ extend ClassMethods
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ def search_engine
22
+ search_engine_class.new
23
+ end
24
+
25
+ def search(...)
26
+ search_engine_class.new.search(...)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Model
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include Indexable
9
+ include Searchable
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Response
5
+ class Aggregations < Caoutsearch::Response::Response
6
+ disable_warnings
7
+
8
+ def initialize(aggs, search = nil, &block)
9
+ @source_search = search
10
+ super(aggs, &block)
11
+ end
12
+
13
+ def key?(key)
14
+ super || has_transformation?(key)
15
+ end
16
+
17
+ def custom_reader(key)
18
+ if has_transformation?(key)
19
+ transformation_cache(key) do
20
+ call_transformation(key)
21
+ end
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ alias_method :[], :custom_reader
28
+
29
+ def has_transformation?(key)
30
+ @source_search&.class&.transformations&.include?(key.to_s)
31
+ end
32
+
33
+ private
34
+
35
+ def call_transformation(key)
36
+ item = @source_search.class.transformations.fetch(key.to_s)
37
+ aggs = @source_search.response.aggregations
38
+
39
+ @source_search.instance_exec(aggs, &item.block)
40
+ end
41
+
42
+ def transformation_cache(name)
43
+ @cached_transformations ||= {}
44
+ return @cached_transformations[name] if @cached_transformations.include?(name)
45
+
46
+ @cached_transformations[name] = yield
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Response
5
+ class Response < ::Hashie::Mash
6
+ disable_warnings
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Response
5
+ class Suggestions < Caoutsearch::Response::Response
6
+ disable_warnings
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Response
5
+ end
6
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Adapter
6
+ module ActiveRecord
7
+ def self.call(model, hits, skip_query_cache: false)
8
+ ids = hits.map { |hit| hit["_id"] }
9
+
10
+ relation = model.where(model.primary_key => ids).extending(Relation)
11
+ relation.skip_query_cache! if skip_query_cache
12
+ relation.hits = hits
13
+ relation
14
+ end
15
+
16
+ module Relation
17
+ attr_reader :hits
18
+
19
+ def hits=(values)
20
+ @hits = values
21
+ end
22
+
23
+ # Re-order records based on hits order
24
+ #
25
+ def records
26
+ return super if order_values.present? || @_reordered_records
27
+
28
+ load
29
+ indexes = @hits.each_with_index.to_h { |hit, index| [hit["_id"].to_s, index] }
30
+ @records = @records.sort_by { |record| indexes[record.id.to_s] }.freeze
31
+ @_reordered_records = true
32
+
33
+ @records
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -7,21 +7,22 @@ module Caoutsearch
7
7
  include Caoutsearch::Config::Mappings
8
8
  include Caoutsearch::Config::Settings
9
9
 
10
- include Caoutsearch::Search::Search::DeleteMethods
11
- include Caoutsearch::Search::Search::Inspect
12
- include Caoutsearch::Search::Search::Instrumentation
13
- include Caoutsearch::Search::Search::InternalDSL
14
- include Caoutsearch::Search::Search::Naming
15
- include Caoutsearch::Search::Search::QueryBuilder
16
- include Caoutsearch::Search::Search::QueryMethods
17
- include Caoutsearch::Search::Search::Resettable
18
- include Caoutsearch::Search::Search::Response
19
- include Caoutsearch::Search::Search::ScrollMethods
20
- include Caoutsearch::Search::Search::SearchMethods
21
-
22
- def self.search(*args)
23
- new.search(*args)
24
- end
10
+ include Caoutsearch::Search::BatchMethods
11
+ include Caoutsearch::Search::Batch::Scroll
12
+ include Caoutsearch::Search::Batch::SearchAfter
13
+ include Caoutsearch::Search::Callbacks
14
+ include Caoutsearch::Search::DeleteMethods
15
+ include Caoutsearch::Search::Inspect
16
+ include Caoutsearch::Search::Instrumentation
17
+ include Caoutsearch::Search::InternalDSL
18
+ include Caoutsearch::Search::Naming
19
+ include Caoutsearch::Search::PointInTime
20
+ include Caoutsearch::Search::QueryBuilder
21
+ include Caoutsearch::Search::QueryMethods
22
+ include Caoutsearch::Search::Records
23
+ include Caoutsearch::Search::Resettable
24
+ include Caoutsearch::Search::Response
25
+ include Caoutsearch::Search::SearchMethods
25
26
  end
26
27
  end
27
28
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Batch
6
+ module Scroll
7
+ def scroll(scroll: "1m", batch_size: 1000, &block)
8
+ search = per(batch_size).track_total_hits
9
+
10
+ request_payload = {
11
+ index: index_name,
12
+ scroll: scroll,
13
+ body: search.build.to_h
14
+ }
15
+
16
+ total = 0
17
+ progress = 0
18
+ requested_at = nil
19
+ last_response_time = nil
20
+
21
+ results = instrument(:scroll_search) do |event_payload|
22
+ response = client.search(request_payload)
23
+ last_response_time = Time.current
24
+
25
+ total = response["hits"]["total"]["value"]
26
+ progress += response["hits"]["hits"].size
27
+
28
+ event_payload[:request] = request_payload
29
+ event_payload[:response] = response
30
+ event_payload[:total] = total
31
+ event_payload[:progress] = progress
32
+
33
+ response
34
+ end
35
+
36
+ hits = results["hits"]["hits"]
37
+ return if hits.empty?
38
+
39
+ yield hits
40
+ return if progress >= total
41
+
42
+ scroll_id = results["_scroll_id"]
43
+ request_payload = {
44
+ scroll_id: scroll_id,
45
+ scroll: scroll
46
+ }
47
+
48
+ loop do
49
+ requested_at = Time.current
50
+
51
+ results = instrument(:scroll, scroll: scroll_id) do |event_payload|
52
+ response = client.scroll(request_payload)
53
+ last_response_time = Time.current
54
+
55
+ progress += response["hits"]["hits"].size
56
+
57
+ event_payload[:request] = request_payload
58
+ event_payload[:response] = response
59
+ event_payload[:total] = total
60
+ event_payload[:progress] = progress
61
+
62
+ response
63
+ rescue Elastic::Transport::Transport::Errors::NotFound => e
64
+ raise_enhance_message_when_scroll_failed(e, scroll, requested_at, last_response_time)
65
+ end
66
+
67
+ hits = results["hits"]["hits"]
68
+ break if hits.empty?
69
+
70
+ yield hits
71
+ break if progress >= total
72
+ end
73
+
74
+ total
75
+ ensure
76
+ clear_scroll(scroll_id) if scroll_id
77
+ end
78
+
79
+ def clear_scroll(scroll_id)
80
+ client.clear_scroll(scroll_id: scroll_id)
81
+ rescue ::Elastic::Transport::Transport::Errors::NotFound
82
+ # We dont care if the scroll ID is already expired
83
+ end
84
+
85
+ def raise_enhance_message_when_scroll_failed(error, scroll, requested_at, last_response_time)
86
+ elapsed = (requested_at - last_response_time).round(1).seconds
87
+
88
+ raise error.exception("Scroll registered for #{scroll}, #{elapsed.inspect} elapsed between. #{error.message}")
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Batch
6
+ module SearchAfter
7
+ def search_after(keep_alive: "1m", batch_size: 1000, &block)
8
+ pit_id = open_point_in_time(keep_alive: keep_alive)
9
+ search = per(batch_size).track_total_hits
10
+
11
+ request_payload = {
12
+ body: search.build.to_h.merge(
13
+ pit: {
14
+ id: pit_id,
15
+ keep_alive: keep_alive
16
+ }
17
+ )
18
+ }
19
+
20
+ total = nil
21
+ progress = 0
22
+ requested_at = nil
23
+ last_response_time = Time.current
24
+
25
+ loop do
26
+ requested_at = Time.current
27
+
28
+ results = instrument(:search_after, pit: pit_id) do |event_payload|
29
+ response = client.search(request_payload)
30
+ last_response_time = Time.current
31
+
32
+ total ||= response["hits"]["total"]["value"]
33
+ progress += response["hits"]["hits"].size
34
+
35
+ event_payload[:request] = request_payload
36
+ event_payload[:response] = response
37
+ event_payload[:total] = total
38
+ event_payload[:progress] = progress
39
+
40
+ response
41
+ rescue Elastic::Transport::Transport::Errors::NotFound => e
42
+ raise_enhance_message_when_pit_failed(e, keep_alive, requested_at, last_response_time)
43
+ end
44
+
45
+ hits = results["hits"]["hits"]
46
+ pit_id = results["pit_id"]
47
+ break if hits.empty?
48
+
49
+ yield hits
50
+ break if progress >= total
51
+
52
+ request_payload[:body].tap do |body|
53
+ body[:pit][:id] = pit_id
54
+ body[:search_after] = hits.last["sort"]
55
+ body.delete(:track_total_hits)
56
+ end
57
+ end
58
+ ensure
59
+ close_point_in_time(pit_id) if pit_id
60
+ end
61
+
62
+ def raise_enhance_message_when_pit_failed(error, keep_alive, requested_at, last_response_time)
63
+ elapsed = (requested_at - last_response_time).round(1).seconds
64
+
65
+ raise error.exception("PIT registered for #{keep_alive}, #{elapsed.inspect} elapsed between. #{error.message}")
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module BatchMethods
6
+ def find_each_hit(**options, &block)
7
+ return to_enum(:find_each_hit, **options) { total_count } unless block
8
+
9
+ find_hits_in_batches(**options) do |hits|
10
+ hits.each(&block)
11
+ end
12
+ end
13
+
14
+ def find_each_record(**options, &block)
15
+ return to_enum(:find_each_record, **options) { total_count } unless block
16
+
17
+ find_records_in_batches(**options) do |relation|
18
+ relation.each(&block)
19
+ end
20
+ end
21
+
22
+ def find_records_in_batches(**options)
23
+ unless block_given?
24
+ return to_enum(:find_records_in_batches, **options) do
25
+ find_hits_in_batches(**options).size
26
+ end
27
+ end
28
+
29
+ find_hits_in_batches(**options) do |hits|
30
+ yield records_adapter.call(model, hits, skip_query_cache: true)
31
+ end
32
+ end
33
+
34
+ def find_hits_in_batches(implementation: :search_after, **options)
35
+ batch_size = options[:batch_size]&.to_i || @current_limit&.to_i || 1000
36
+
37
+ unless block_given?
38
+ return to_enum(:find_hits_in_batches, **options) do
39
+ total_count.div(batch_size) + 1
40
+ end
41
+ end
42
+
43
+ unless %i[search_after scroll].include?(implementation)
44
+ raise ArgumentError, "unexpected implementation argument: #{implementation.inspect}"
45
+ end
46
+
47
+ method(implementation).call(batch_size: batch_size, **options) do |hits|
48
+ yield hits
49
+ end
50
+ end
51
+
52
+ def scroll_records_in_batches(**options)
53
+ ActiveSupport::Deprecation.warn("scroll_records_in_batches is deprecated, use find_records_in_batches instead")
54
+ find_records_in_batches(implementation: :scroll, **options)
55
+ end
56
+
57
+ def scroll_records(**options)
58
+ ActiveSupport::Deprecation.warn("scroll_records is deprecated, use find_each_record instead")
59
+ find_each_record(implementation: :scroll, **options)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Callbacks
6
+ extend ActiveSupport::Concern
7
+ include ActiveSupport::Callbacks
8
+
9
+ included do
10
+ define_callbacks :build
11
+ end
12
+
13
+ class_methods do
14
+ def before_build(*filters, &blk)
15
+ set_callback(:build, :before, *filters, &blk)
16
+ end
17
+
18
+ def after_build(*filters, &blk)
19
+ set_callback(:build, :after, *filters, &blk)
20
+ end
21
+
22
+ def around_build(*filters, &blk)
23
+ set_callback(:build, :around, *filters, &blk)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module DeleteMethods
6
+ def delete_documents
7
+ request_payload = {
8
+ index: index_name,
9
+ body: build.to_h
10
+ }
11
+
12
+ instrument(:delete) do |event_payload|
13
+ event_payload[:request] = request_payload
14
+ event_payload[:response] = client.delete_by_query(request_payload)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -9,9 +9,9 @@ module Caoutsearch
9
9
  PROPERTIES_TO_INSPECT = %i[name options block].freeze
10
10
 
11
11
  def initialize(name, options = {}, &block)
12
- @name = name
12
+ @name = name
13
13
  @options = options
14
- @block = block if block
14
+ @block = block if block
15
15
  end
16
16
 
17
17
  def has_block?
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Inspect
6
+ PROPERTIES_TO_INSPECT = %i[
7
+ search_criteria
8
+ current_context
9
+ current_order
10
+ current_page
11
+ current_limit
12
+ current_offset
13
+ current_aggregations
14
+ current_suggestions
15
+ current_returns
16
+ ].freeze
17
+
18
+ def inspect
19
+ properties = properties_to_inspect.map { |k, v| " #{k}: #{v}" }
20
+
21
+ "#<#{self.class}#{properties.join(",")}>"
22
+ end
23
+
24
+ private
25
+
26
+ def properties_to_inspect
27
+ PROPERTIES_TO_INSPECT.each_with_object({}) do |name, properties|
28
+ value = instance_variable_get("@#{name}")
29
+ properties[name] = value.inspect if value
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caoutsearch
4
+ module Search
5
+ module Instrumentation
6
+ extend ActiveSupport::Concern
7
+
8
+ def instrument(action, **options, &block)
9
+ ActiveSupport::Notifications.instrument("#{action}.caoutsearch_search", **options, klass: self.class.to_s, &block)
10
+ end
11
+
12
+ class_methods do
13
+ def instrument(action, **options, &block)
14
+ ActiveSupport::Notifications.instrument("#{action}.caoutsearch_search", **options, klass: to_s, &block)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end