esse 0.2.0 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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