esse 0.2.0 → 0.2.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/lib/esse/cli/event_listener.rb +13 -0
  3. data/lib/esse/cli/generate.rb +53 -14
  4. data/lib/esse/cli/index/base_operation.rb +5 -13
  5. data/lib/esse/cli/index/close.rb +1 -1
  6. data/lib/esse/cli/index/create.rb +1 -1
  7. data/lib/esse/cli/index/delete.rb +1 -1
  8. data/lib/esse/cli/index/import.rb +6 -2
  9. data/lib/esse/cli/index/open.rb +1 -1
  10. data/lib/esse/cli/index/reset.rb +1 -1
  11. data/lib/esse/cli/index/update_aliases.rb +2 -2
  12. data/lib/esse/cli/index/update_mapping.rb +9 -5
  13. data/lib/esse/cli/index/update_settings.rb +1 -1
  14. data/lib/esse/cli/index.rb +11 -4
  15. data/lib/esse/cli/templates/collection.rb.erb +29 -0
  16. data/lib/esse/cli/templates/config.rb.erb +13 -3
  17. data/lib/esse/cli/templates/document.rb.erb +34 -0
  18. data/lib/esse/cli/templates/index.rb.erb +63 -114
  19. data/lib/esse/cli/templates/mappings.json +27 -0
  20. data/lib/esse/cli/templates/settings.json +62 -0
  21. data/lib/esse/cli.rb +5 -0
  22. data/lib/esse/cluster.rb +93 -12
  23. data/lib/esse/cluster_engine.rb +42 -0
  24. data/lib/esse/collection.rb +18 -0
  25. data/lib/esse/config.rb +14 -2
  26. data/lib/esse/core.rb +28 -7
  27. data/lib/esse/deprecations/cluster.rb +27 -0
  28. data/lib/esse/deprecations/deprecate.rb +29 -0
  29. data/lib/esse/deprecations/index.rb +37 -0
  30. data/lib/esse/deprecations/index_backend_delegator.rb +217 -0
  31. data/lib/esse/deprecations/repository.rb +34 -0
  32. data/lib/esse/deprecations/repository_backend_delegator.rb +110 -0
  33. data/lib/esse/deprecations/serializer.rb +14 -0
  34. data/lib/esse/deprecations.rb +7 -0
  35. data/lib/esse/document.rb +91 -0
  36. data/lib/esse/dynamic_template.rb +43 -0
  37. data/lib/esse/errors.rb +60 -2
  38. data/lib/esse/events/event.rb +4 -19
  39. data/lib/esse/events.rb +13 -2
  40. data/lib/esse/hash_document.rb +38 -0
  41. data/lib/esse/import/bulk.rb +106 -0
  42. data/lib/esse/import/request_body.rb +60 -0
  43. data/lib/esse/index/aliases.rb +50 -0
  44. data/lib/esse/index/attributes.rb +107 -0
  45. data/lib/esse/index/base.rb +17 -53
  46. data/lib/esse/index/documents.rb +236 -0
  47. data/lib/esse/index/indices.rb +171 -0
  48. data/lib/esse/index/inheritance.rb +30 -0
  49. data/lib/esse/index/mappings.rb +6 -19
  50. data/lib/esse/index/object_document_mapper.rb +36 -0
  51. data/lib/esse/index/plugins.rb +42 -0
  52. data/lib/esse/index/search.rb +27 -0
  53. data/lib/esse/index/settings.rb +2 -2
  54. data/lib/esse/index/type.rb +51 -11
  55. data/lib/esse/index.rb +14 -9
  56. data/lib/esse/index_mapping.rb +10 -2
  57. data/lib/esse/index_setting.rb +3 -1
  58. data/lib/esse/null_document.rb +35 -0
  59. data/lib/esse/plugins.rb +12 -0
  60. data/lib/esse/primitives/hstring.rb +1 -1
  61. data/lib/esse/{index_type → repository}/actions.rb +1 -1
  62. data/lib/esse/repository/documents.rb +13 -0
  63. data/lib/esse/repository/object_document_mapper.rb +157 -0
  64. data/lib/esse/repository.rb +17 -0
  65. data/lib/esse/search/query.rb +105 -0
  66. data/lib/esse/search/response.rb +46 -0
  67. data/lib/esse/template_loader.rb +1 -1
  68. data/lib/esse/transport/aliases.rb +36 -0
  69. data/lib/esse/transport/documents.rb +199 -0
  70. data/lib/esse/transport/health.rb +30 -0
  71. data/lib/esse/transport/indices.rb +192 -0
  72. data/lib/esse/transport/search.rb +48 -0
  73. data/lib/esse/transport.rb +44 -0
  74. data/lib/esse/version.rb +1 -1
  75. data/lib/esse.rb +20 -5
  76. metadata +55 -50
  77. data/lib/esse/backend/index/aliases.rb +0 -73
  78. data/lib/esse/backend/index/close.rb +0 -54
  79. data/lib/esse/backend/index/create.rb +0 -67
  80. data/lib/esse/backend/index/delete.rb +0 -39
  81. data/lib/esse/backend/index/documents.rb +0 -23
  82. data/lib/esse/backend/index/existance.rb +0 -22
  83. data/lib/esse/backend/index/open.rb +0 -54
  84. data/lib/esse/backend/index/refresh.rb +0 -43
  85. data/lib/esse/backend/index/reset.rb +0 -33
  86. data/lib/esse/backend/index/update.rb +0 -143
  87. data/lib/esse/backend/index.rb +0 -54
  88. data/lib/esse/backend/index_type/documents.rb +0 -214
  89. data/lib/esse/backend/index_type.rb +0 -37
  90. data/lib/esse/cli/templates/type_collection.rb.erb +0 -41
  91. data/lib/esse/cli/templates/type_mappings.json +0 -6
  92. data/lib/esse/cli/templates/type_serializer.rb.erb +0 -23
  93. data/lib/esse/index/backend.rb +0 -14
  94. data/lib/esse/index/naming.rb +0 -64
  95. data/lib/esse/index_type/backend.rb +0 -14
  96. data/lib/esse/index_type/mappings.rb +0 -42
  97. data/lib/esse/index_type.rb +0 -15
  98. data/lib/esse/object_document_mapper.rb +0 -110
@@ -0,0 +1,106 @@
1
+ module Esse
2
+ module Import
3
+ class Bulk
4
+ def initialize(type: nil, index: nil, delete: nil, create: nil)
5
+ @index = Array(index).select(&method(:valid_doc?)).reject(&:ignore_on_index?).map do |doc|
6
+ value = doc.to_bulk
7
+ value[:_type] ||= type if type
8
+ { index: value }
9
+ end
10
+ @create = Array(create).select(&method(:valid_doc?)).reject(&:ignore_on_index?).map do |doc|
11
+ value = doc.to_bulk
12
+ value[:_type] ||= type if type
13
+ { create: value }
14
+ end
15
+ @delete = Array(delete).select(&method(:valid_doc?)).reject(&:ignore_on_delete?).map do |doc|
16
+ value = doc.to_bulk(data: false)
17
+ value[:_type] ||= type if type
18
+ { delete: value }
19
+ end
20
+ end
21
+
22
+ # Return an array of RequestBody instances
23
+ #
24
+ # In case of timeout error, will retry with an exponential backoff using the following formula:
25
+ # wait_interval = (retry_count**4) + 15 + (rand(10) * (retry_count + 1)) seconds. It will retry up to max_retries times that is default 3.
26
+ #
27
+ # Too large bulk requests will be split into multiple requests with only one attempt.
28
+ #
29
+ # @yield [RequestBody] A request body instance
30
+ def each_request(max_retries: 3)
31
+ # @TODO create indexes when by checking all the index suffixes (if mapping is not empty)
32
+ requests = [optimistic_request]
33
+ retry_count = 0
34
+
35
+ begin
36
+ requests.each do |request|
37
+ next unless request.body?
38
+ resp = yield request
39
+ if resp&.[]('errors')
40
+ raise resp&.fetch('items', [])&.select { |item| item.values.first['error'] }&.join("\n")
41
+ end
42
+ end
43
+ rescue Faraday::TimeoutError, Esse::Transport::RequestTimeoutError => e
44
+ retry_count += 1
45
+ raise Esse::Transport::RequestTimeoutError.new(e.message) if retry_count >= max_retries
46
+ wait_interval = (retry_count**4) + 15 + (rand(10) * (retry_count + 1))
47
+ Esse.logger.warn "Timeout error, retrying in #{wait_interval} seconds"
48
+ sleep(wait_interval)
49
+ retry
50
+ rescue Esse::Transport::RequestEntityTooLargeError => e
51
+ retry_count += 1
52
+ raise e if retry_count > 1 # only retry once on this error
53
+ requests = balance_requests_size(e)
54
+ Esse.logger.warn <<~MSG
55
+ Request entity too large, retrying with a bulk with: #{requests.map(&:bytesize).join(' + ')}.
56
+ Note that this cause performance degradation, consider adjusting the batch_size of the index or increasing the bulk size.
57
+ MSG
58
+ retry
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def valid_doc?(doc)
65
+ Esse.document?(doc)
66
+ end
67
+
68
+ def optimistic_request
69
+ request = Import::RequestBodyAsJson.new
70
+ request.delete = @delete
71
+ request.create = @create
72
+ request.index = @index
73
+ request
74
+ end
75
+
76
+ # @return [Array<RequestBody>]
77
+ def balance_requests_size(err)
78
+ if (bulk_size = err.message.scan(/exceeded.(\d+).bytes/).dig(0, 0).to_i) > 0
79
+ requests = (@delete + @create + @index).each_with_object([Import::RequestBodyRaw.new]) do |as_json, result|
80
+ operation, meta = as_json.to_a.first
81
+ meta = meta.dup
82
+ data = meta.delete(:data)
83
+ piece = MultiJson.dump(operation => meta)
84
+ piece << "\n" << MultiJson.dump(data) if data
85
+ if piece.bytesize > bulk_size
86
+ Esse.logger.warn <<~MSG
87
+ The document #{meta.inspect} size is #{piece.bytesize} bytes, which exceeds the maximum bulk size of #{bulk_size} bytes.
88
+ Consider increasing the bulk size or reducing the document size. The document will be ignored during this import.
89
+ MSG
90
+ next
91
+ end
92
+
93
+ if result.last.body.bytesize + piece.bytesize > bulk_size
94
+ result.push(Import::RequestBodyRaw.new.tap { |r| r.add(operation, piece) })
95
+ else
96
+ result[-1].add(operation, piece)
97
+ end
98
+ end
99
+ requests.each(&:finalize)
100
+ else
101
+ raise err
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,60 @@
1
+ module Esse
2
+ module Import
3
+ class RequestBody
4
+ attr_reader :body, :stats
5
+
6
+ def initialize(body:)
7
+ @body = body # body may be String or Array<Hash>
8
+ @stats = { index: 0, create: 0, delete: 0 }
9
+ end
10
+
11
+ def body?
12
+ !body.empty?
13
+ end
14
+ end
15
+
16
+ class RequestBodyRaw < RequestBody
17
+ def initialize
18
+ super(body: '')
19
+ end
20
+
21
+ def bytesize
22
+ body.bytesize
23
+ end
24
+
25
+ def add(operation, payload)
26
+ stats[operation] += 1
27
+ if @body.empty?
28
+ @body = payload
29
+ else
30
+ @body << "\n" << payload
31
+ end
32
+ end
33
+
34
+ def finalize
35
+ @body << "\n"
36
+ end
37
+ end
38
+
39
+ class RequestBodyAsJson < RequestBody
40
+ def initialize
41
+ super(body: [])
42
+ end
43
+
44
+ def index=(docs)
45
+ @body += docs
46
+ @stats[:index] += docs.size
47
+ end
48
+
49
+ def create=(docs)
50
+ @body += docs
51
+ @stats[:create] += docs.size
52
+ end
53
+
54
+ def delete=(docs)
55
+ @body += docs
56
+ @stats[:delete] += docs.size
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ module ClassMethods
6
+ # Get the aliases for the index.
7
+ def aliases(**options)
8
+ response = cluster.api.aliases(**options, index: index_name, name: '*')
9
+ idx_name = response.keys.find { |idx| idx.start_with?(index_name) }
10
+ return [] unless idx_name
11
+
12
+ response.dig(idx_name, 'aliases')&.keys || []
13
+ rescue Esse::Transport::NotFoundError
14
+ []
15
+ end
16
+
17
+ # Return list of real index names for the virtual index name(alias)
18
+ def indices_pointing_to_alias(**options)
19
+ cluster.api.aliases(**options, name: index_name).keys
20
+ rescue Esse::Transport::NotFoundError
21
+ []
22
+ end
23
+
24
+ # Replaces all existing aliases by the respective suffixed index from argument.
25
+ #
26
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
27
+ # @option [Array<String>] :suffix One or more index suffixes to point the alias to.
28
+ # @raise [Esse::Transport::ServerError] in case of failure
29
+ # @return [Hash] the elasticsearch response
30
+ def update_aliases(suffix:, **options)
31
+ cluster.throw_error_when_readonly!
32
+ raise(ArgumentError, 'index suffix cannot be nil') if suffix.nil?
33
+
34
+ options[:body] = {
35
+ actions: [
36
+ *indices_pointing_to_alias.map do |index|
37
+ { remove: { index: index, alias: index_name } }
38
+ end,
39
+ *Array(suffix).map do |value|
40
+ { add: { index: build_real_index_name(value), alias: index_name } }
41
+ end,
42
+ ],
43
+ }
44
+ cluster.api.update_aliases(**options)
45
+ end
46
+ end
47
+
48
+ extend ClassMethods
49
+ end
50
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ module ClassMethods
6
+ TEMPLATE_DIRS = [
7
+ '%<dirname>s/templates',
8
+ '%<dirname>s'
9
+ ].freeze
10
+
11
+ def index_name=(value)
12
+ @index_name = Hstring.new(value.to_s).underscore.presence
13
+ end
14
+
15
+ def index_name(suffix: nil)
16
+ iname = index_prefixed_name(@index_name || normalized_name)
17
+ suffix = Hstring.new(suffix).underscore.presence
18
+ return iname if !iname || !suffix
19
+
20
+ [iname, suffix].join('_')
21
+ end
22
+
23
+ def index_name?
24
+ !index_name.nil?
25
+ end
26
+
27
+ def index_prefix
28
+ return @index_prefix if defined? @index_prefix
29
+
30
+ cluster.index_prefix
31
+ end
32
+
33
+ def index_prefix=(value)
34
+ if value == false
35
+ @index_prefix = nil
36
+ return
37
+ end
38
+
39
+ @index_prefix = Hstring.new(value.to_s).underscore.presence
40
+ end
41
+
42
+ def index_suffix=(value)
43
+ @index_suffix = Hstring.new(value.to_s).underscore.presence
44
+ end
45
+
46
+ def index_suffix
47
+ @index_suffix
48
+ end
49
+
50
+ def uname
51
+ Hstring.new(name).underscore.presence
52
+ end
53
+
54
+ def index_directory
55
+ return unless uname
56
+ return if uname == 'Esse::Index'
57
+
58
+ Esse.config.indices_directory.join(uname).to_s
59
+ end
60
+
61
+ def template_dirs
62
+ return [] unless index_directory
63
+
64
+ TEMPLATE_DIRS.map { |term| format(term, dirname: index_directory) }
65
+ end
66
+
67
+ def bulk_wait_interval
68
+ @bulk_wait_interval || Esse.config.bulk_wait_interval
69
+ end
70
+
71
+ def bulk_wait_interval=(value)
72
+ @bulk_wait_interval = value.to_f
73
+ end
74
+
75
+ def mapping_single_type=(value)
76
+ @mapping_single_type = !!value
77
+ end
78
+
79
+ def mapping_single_type?
80
+ return @mapping_single_type if defined? @mapping_single_type
81
+
82
+ @mapping_single_type = cluster.engine.mapping_single_type?
83
+ end
84
+
85
+ protected
86
+
87
+ def index_prefixed_name(value)
88
+ return if value == '' || value.nil?
89
+ return value.to_s unless index_prefix
90
+
91
+ [index_prefix, value].join('_')
92
+ end
93
+
94
+ def normalized_name
95
+ Hstring.new(name).underscore.tr('/', '_').sub(/_(index)$/, '')
96
+ end
97
+
98
+ def build_real_index_name(suffix = nil)
99
+ suffix = Hstring.new(suffix).underscore.presence || index_suffix || Esse.timestamp
100
+
101
+ index_name(suffix: suffix)
102
+ end
103
+ end
104
+
105
+ extend ClassMethods
106
+ end
107
+ end
@@ -3,48 +3,23 @@
3
3
  module Esse
4
4
  class Index
5
5
  module ClassMethods
6
- # Define a Index method on the given module that calls the Index
7
- # method on the receiver. This is how the Esse::Index() method is
8
- # defined, and allows you to define Index() methods on other modules,
9
- # making it easier to have custom index settings for all indices under
10
- # a namespace. Example:
11
- #
12
- # module V1
13
- # EsIndex = Class.new(Esse::Index)
14
- # EsIndex.def_Index(self)
15
- #
16
- # class Bar < EsIndex
17
- # # Uses :default elasticsearch client connection
18
- # end
19
- #
20
- # class Baz < EsIndex(:v1)
21
- # # Uses :v1 elasticsearch client connection
22
- # end
23
- # end
24
- def def_Index(index_module) # rubocop:disable Naming/MethodName
25
- tap do |model|
26
- index_module.define_singleton_method(:Index) do |source|
27
- model.Index(source)
28
- end
6
+ # Sets the client_id associated with the Index class.
7
+ # This can be used directly on Esse::Index to set the :default es cluster
8
+ # to be used by subclasses, or to override the es client used for specific indices:
9
+ # Esse::Index.cluster_id = :v1
10
+ # ArtistIndex = Class.new(Esse::Index)
11
+ # ArtistIndex.cluster_id = :v2
12
+ # @param [Symbol, Esse::Cluster, NilClass] source the cluster id or the cluster instance
13
+ # @return [Symbol] the cluster id
14
+ # @raise [ArgumentError] if the cluster id is not defined in the Esse.config
15
+ def cluster_id=(source)
16
+ if source.nil?
17
+ @cluster_id = nil
18
+ return
29
19
  end
30
- end
31
-
32
- # Lets you create a Index subclass with its elasticsearch cluster
33
- #
34
- # Example:
35
- # # Using a custom cluster
36
- # Esse.config.cluster(:v1).client = Elasticsearch::Client.new
37
- # class UsersIndex < Esse::Index(:v1)
38
- # end
39
- #
40
- # # Using :default cluster
41
- # class UsersIndex < Esse::Index
42
- # end
43
- def Index(source) # rubocop:disable Naming/MethodName
44
- klass = Class.new(self)
45
20
 
46
21
  valid_ids = Esse.config.cluster_ids
47
- klass.cluster_id = \
22
+ new_id = \
48
23
  case source
49
24
  when Esse::Cluster
50
25
  source.id
@@ -55,7 +30,7 @@ module Esse
55
30
 
56
31
  msg = <<~MSG
57
32
  We could not resolve the index cluster using the argument %<arg>p. \n
58
- It must be previously defined in the `Esse.config' settings. \n
33
+ It must be previously defined in the `Esse.config.cluster(%<arg>p) { ... }' settings. \n
59
34
  Here is the list of cluster ids we have configured: %<ids>s\n
60
35
 
61
36
  You can ignore this cluster id entirely. That way the :default id will be used.\n
@@ -63,22 +38,11 @@ module Esse
63
38
  class UsersIndex < Esse::Index\n
64
39
  end\n
65
40
  MSG
66
- unless klass.cluster_id
41
+ unless new_id
67
42
  raise ArgumentError.new, format(msg, arg: source, ids: valid_ids.map(&:inspect).join(', '))
68
43
  end
69
44
 
70
- klass.type_hash = {}
71
- klass
72
- end
73
-
74
- # Sets the client_id associated with the Index class.
75
- # This can be used directly on Esse::Index to set the :default es cluster
76
- # to be used by subclasses, or to override the es client used for specific indices:
77
- # Esse::Index.cluster_id = :v1
78
- # ArtistIndex = Class.new(Esse::Index)
79
- # ArtistIndex.cluster_id = :v2
80
- def cluster_id=(cluster_id)
81
- @cluster_id = cluster_id
45
+ @cluster_id = new_id
82
46
  end
83
47
 
84
48
  # @return [Symbol] reads the @cluster_id instance variable or :default
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ module ClassMethods
6
+ # Retrieves the specified JSON document from an index.
7
+ #
8
+ # UsersIndex.get(id: 1) # { '_id' => 1, ... }
9
+ # UsersIndex.get(id: 'missing') # raise Esse::Transport::NotFoundError
10
+ #
11
+ # @param doc [Esse::Document] the document to retrieve
12
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
13
+ # @option [String, Integer] :id The `_id` of the elasticsearch document
14
+ # @option [String, NilClass] :type The type of the document (Optional for elasticsearch >= 7)
15
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
16
+ # @raise [Esse::Transport::NotFoundError] when the doc does not exist
17
+ # @return [Hash] The elasticsearch document.
18
+ #
19
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/7.5/docs-get.html
20
+ def get(doc = nil, suffix: nil, **options)
21
+ if document?(doc)
22
+ options[:id] = doc.id
23
+ options[:type] = doc.type if doc.type?
24
+ options[:routing] = doc.routing if doc.routing?
25
+ end
26
+ require_kwargs!(options, :id)
27
+ options[:index] = index_name(suffix: suffix)
28
+ cluster.may_update_type!(options)
29
+ cluster.api.get(**options)
30
+ end
31
+
32
+ # Check if a JSON document exists
33
+ #
34
+ # UsersIndex.exist?(id: 1) # true
35
+ # UsersIndex.exist?(id: 'missing') # false
36
+ #
37
+ # @param doc [Esse::Document] the document to retrieve
38
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
39
+ # @option [String, Integer] :id The `_id` of the elasticsearch document
40
+ # @option [String, NilClass] :type The type of the document (Optional for elasticsearch >= 7)
41
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
42
+ # @return [Boolean] true if the document exists
43
+ def exist?(doc = nil, suffix: nil, **options)
44
+ if document?(doc)
45
+ options[:id] = doc.id
46
+ options[:type] = doc.type if doc.type?
47
+ options[:routing] = doc.routing if doc.routing?
48
+ end
49
+ require_kwargs!(options, :id)
50
+ options[:index] = index_name(suffix: suffix)
51
+ cluster.may_update_type!(options)
52
+ cluster.api.exist?(**options)
53
+ end
54
+
55
+ # Gets the number of matches for a search query.
56
+ #
57
+ # UsersIndex.count # 999
58
+ # UsersIndex.count(body: { ... }) # 32
59
+ #
60
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
61
+ # @option [Hash] :body A query to restrict the results specified with the Query DSL (optional)
62
+ # @option [String, NilClass] :type The type of the document (Optional for elasticsearch >= 7)
63
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
64
+ # @return [Integer] amount of documents found
65
+ #
66
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/7.5/search-count.html
67
+ def count(type: nil, suffix: nil, **options)
68
+ params = {
69
+ index: index_name(suffix: suffix),
70
+ type: type,
71
+ }
72
+ cluster.may_update_type!(params)
73
+ cluster.api.count(**options, **params)['count']
74
+ end
75
+
76
+ # Removes a JSON document from the specified index.
77
+ #
78
+ # UsersIndex.delete(id: 1) # true
79
+ # UsersIndex.delete(id: 'missing') # false
80
+ #
81
+ # @param doc [Esse::Document] the document to retrieve
82
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
83
+ # @option [String, Integer] :id The `_id` of the elasticsearch document
84
+ # @option [String, NilClass] :type The type of the document (Optional for elasticsearch >= 7)
85
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
86
+ # @raise [Esse::Transport::NotFoundError] when the doc does not exist
87
+ #
88
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/7.5/docs-delete.html
89
+ def delete(doc = nil, suffix: nil, **options)
90
+ if document?(doc)
91
+ options[:id] = doc.id
92
+ options[:type] = doc.type if doc.type?
93
+ options[:routing] = doc.routing if doc.routing?
94
+ end
95
+ require_kwargs!(options, :id)
96
+ options[:index] = index_name(suffix: suffix)
97
+ cluster.may_update_type!(options)
98
+ cluster.api.delete(**options)
99
+ end
100
+
101
+ # Updates a document using the specified script.
102
+ #
103
+ # UsersIndex.update(id: 1, body: { doc: { ... } }) # { '_id' => 1, ...}
104
+ #
105
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
106
+ # @option [String, Integer] :id The `_id` of the elasticsearch document
107
+ # @option [Hash] :body the body of the request
108
+ # @option [String, NilClass] :type The type of the document (Optional for elasticsearch >= 7)
109
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
110
+ # @raise [Esse::Transport::NotFoundError] when the doc does not exist
111
+ # @return [Hash] elasticsearch response hash
112
+ #
113
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/7.5/docs-update.html
114
+ def update(doc = nil, suffix: nil, **options)
115
+ if document?(doc)
116
+ options[:id] = doc.id
117
+ options[:body] = { doc: doc.source }
118
+ options[:type] = doc.type if doc.type?
119
+ options[:routing] = doc.routing if doc.routing?
120
+ end
121
+ require_kwargs!(options, :id, :body)
122
+ options[:index] = index_name(suffix: suffix)
123
+ cluster.may_update_type!(options)
124
+ cluster.api.update(**options)
125
+ end
126
+
127
+ # Adds a JSON document to the specified index and makes it searchable. If the document
128
+ # already exists, updates the document and increments its version.
129
+ #
130
+ # UsersIndex::User.index(id: 1, body: { name: 'name' }) # { '_id' => 1, ...}
131
+ #
132
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
133
+ # @option [String, Integer] :id The `_id` of the elasticsearch document
134
+ # @option [Hash] :body The JSON document that will be indexed (Required)
135
+ # @option [String, NilClass] :type The type of the document (Optional for elasticsearch >= 7)
136
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
137
+ # @return [Hash] the elasticsearch response Hash
138
+ #
139
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/7.5/docs-index_.html
140
+ def index(doc = nil, suffix: nil, **options)
141
+ if document?(doc)
142
+ options[:id] = doc.id
143
+ options[:body] = doc.source
144
+ options[:type] = doc.type if doc.type?
145
+ options[:routing] = doc.routing if doc.routing?
146
+ end
147
+ require_kwargs!(options, :id, :body)
148
+ options[:index] = index_name(suffix: suffix)
149
+ cluster.may_update_type!(options)
150
+ cluster.api.index(**options)
151
+ end
152
+
153
+ # Performs multiple indexing or delete operations in a single API call.
154
+ # This reduces overhead and can greatly increase indexing speed.
155
+ #
156
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
157
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
158
+ # @option [Array<Esse::Document>] :index list of documents to be indexed(Optional)
159
+ # @option [Array<Esse::Document>] :delete list of documents to be deleted(Optional)
160
+ # @option [Array<Esse::Document>] :create list of documents to be created(Optional)
161
+ # @option [String, NilClass] :type The type of the document (Optional for elasticsearch >= 7)
162
+ # @return [Array<Esse::Import::RequestBody>] The list of request bodies. @TODO Change this to a Stats object
163
+ #
164
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/7.5/docs-bulk.html
165
+ # @see https://github.com/elastic/elasticsearch-ruby/blob/main/elasticsearch-api/lib/elasticsearch/api/utils.rb
166
+ # @see https://github.com/elastic/elasticsearch-ruby/blob/main/elasticsearch-api/lib/elasticsearch/api/actions/bulk.rb
167
+ def bulk(index: nil, delete: nil, create: nil, type: nil, suffix: nil, **options)
168
+ definition = {
169
+ index: index_name(suffix: suffix),
170
+ type: type,
171
+ }.merge(options)
172
+ cluster.may_update_type!(definition)
173
+
174
+ # @TODO Wrap the return in a some other Stats object with more information
175
+ Esse::Import::Bulk.new(
176
+ **definition.slice(:type),
177
+ index: index,
178
+ delete: delete,
179
+ create: create,
180
+ ).each_request do |request_body|
181
+ cluster.api.bulk(**definition, body: request_body.body) do |event_payload|
182
+ event_payload[:body_stats] = request_body.stats
183
+ if bulk_wait_interval > 0
184
+ event_payload[:wait_interval] = bulk_wait_interval
185
+ sleep(bulk_wait_interval)
186
+ else
187
+ event_payload[:wait_interval] = 0.0
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ # Resolve collection and index data
194
+ #
195
+ # @param repos [Array<String>] List of repo types. Defaults to all types.
196
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
197
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
198
+ # @option [Hash] :context The collection context. This value will be passed as argument to the collection
199
+ # May be SQL condition or any other filter you have defined on the collection.
200
+ # @return [Numeric] The number of documents imported
201
+ def import(*repo_types, context: {}, suffix: nil, **options)
202
+ repo_types = repo_hash.keys if repo_types.empty?
203
+ count = 0
204
+ repo_hash.slice(*repo_types).each do |repo_name, repo|
205
+ repo.each_serialized_batch(**(context || {})) do |batch|
206
+ # Elasticsearch 6.x and older have multiple types per index.
207
+ # This gem supports multiple types per index for backward compatibility, but we recommend to update
208
+ # your elasticsearch to a at least 7.x version and use a single type per index.
209
+ #
210
+ # Note that the repository name will be used as the document type.
211
+ # mapping_default_type
212
+ kwargs = { index: batch, suffix: suffix, type: repo_name, **options }
213
+ cluster.may_update_type!(kwargs)
214
+ bulk(**kwargs)
215
+ count += batch.size
216
+ end
217
+ end
218
+ count
219
+ end
220
+
221
+ protected
222
+
223
+ def document?(doc)
224
+ Esse.document?(doc)
225
+ end
226
+
227
+ def require_kwargs!(options, *keys)
228
+ keys.each do |key|
229
+ raise ArgumentError, "missing keyword: #{key}" unless options.key?(key)
230
+ end
231
+ end
232
+ end
233
+
234
+ extend ClassMethods
235
+ end
236
+ end