esse 0.3.4 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0d63306d9028972cd58998169513b15c761f1ecd6fe67d2d6d04a6eb995cdae
4
- data.tar.gz: f4877f14109d22c40dbec51eef17edc86d60231d488929cc773749deeb8cb6b4
3
+ metadata.gz: b6c9d974dbe345e36fc5b8f286185735377309176016d9662900cec7510dd788
4
+ data.tar.gz: 01ad7d8d5ee1d849e0dc67cdf1c812c7c44aa686e8b71af5f85137c75e911842
5
5
  SHA512:
6
- metadata.gz: ee0b45292911de3db131af146d212e36f94ad8782021c89e20da74813135ebff6c586996bd33e355bb6af0e190db6a3be34eba1aebb4fa51f75abb4adf4cf1a1
7
- data.tar.gz: 70c69e8941c07ec18f3d14164ef211308ea72752bc22e1fa6e20dc0578d6c43aa549e681cca1177b148c6e37b9d2420f21f5a9ba9da4414a70a6814196a3d2e4
6
+ metadata.gz: b67855b65130b69443a88fe5f033c5437066f84b3ee377169c3ecb8f10c336b1f58e02751aa7fda5393eab308fbd022cc440465cfaa759a29fc943cdc85dc2b3
7
+ data.tar.gz: c509f2960292c11144a0092befd0fd8c074faa341b0d4ffb028eb9819561fbf28d5af07c388b7712dcc7e576214fa679c009cd54757d364ca967674de643dedd
@@ -95,6 +95,13 @@ module Esse
95
95
  end
96
96
  print_message(stats.join(', ') + '.')
97
97
  end
98
+
99
+ def elasticsearch_reindex(event)
100
+ print_message '[%<runtime>s] Reindex from %<from>s to %<to>s successfuly completed',
101
+ from: colorize(event[:request].dig(:body, :source, :index), :bold),
102
+ to: colorize(event[:request].dig(:body, :dest, :index), :bold),
103
+ runtime: formatted_runtime(event[:runtime])
104
+ end
98
105
  end
99
106
  end
100
107
  end
@@ -16,14 +16,18 @@ module Esse
16
16
  DESC
17
17
  option :suffix, type: :string, default: nil, aliases: '-s', desc: 'Suffix to append to index name'
18
18
  option :import, type: :boolean, default: true, desc: 'Import documents before point alias to the new index'
19
+ option :reindex, type: :boolean, default: false, desc: 'Use _reindex API to import documents from the old index to the new index'
19
20
  option :optimize, type: :boolean, default: true, desc: 'Optimize index before import documents by disabling refresh_interval and setting number_of_replicas to 0'
21
+ option :settings, type: :hash, default: nil, desc: 'List of settings to pass to the index class. Example: --settings=refresh_interval:1s,number_of_replicas:0'
20
22
  def reset(*index_classes)
21
23
  require_relative 'index/reset'
22
- Reset.new(indices: index_classes, **options.to_h.transform_keys(&:to_sym)).run
24
+ opts = HashUtils.deep_transform_keys(options.to_h, &:to_sym)
25
+ if opts[:import] && opts[:reindex]
26
+ raise ArgumentError, 'You cannot use --import and --reindex together'
27
+ end
28
+ Reset.new(indices: index_classes, **opts).run
23
29
  end
24
30
 
25
- # @TODO Add reindex task to create a new index and import documents from the old index using _reindex API
26
-
27
31
  desc 'create *INDEX_CLASSES', 'Creates indices for the given classes'
28
32
  long_desc <<-DESC
29
33
  Creates index and applies mapping and settings for the given classes.
@@ -33,9 +37,11 @@ module Esse
33
37
  DESC
34
38
  option :suffix, type: :string, default: nil, aliases: '-s', desc: 'Suffix to append to index name'
35
39
  option :alias, type: :boolean, default: false, aliases: '-a', desc: 'Update alias after create index'
40
+ option :settings, type: :hash, default: nil, desc: 'List of settings to pass to the index class. Example: --settings=index.refresh_interval:-1,index.number_of_replicas:0'
36
41
  def create(*index_classes)
37
42
  require_relative 'index/create'
38
- Create.new(indices: index_classes, **options.to_h.transform_keys(&:to_sym)).run
43
+ opts = HashUtils.deep_transform_keys(options.to_h, &:to_sym)
44
+ Create.new(indices: index_classes, **opts).run
39
45
  end
40
46
 
41
47
  desc 'delete *INDEX_CLASSES', 'Deletes indices for the given classes'
@@ -58,9 +64,11 @@ module Esse
58
64
  desc 'update_settings *INDEX_CLASS', 'Closes the index for read/write operations, updates the index settings, and open it again'
59
65
  option :suffix, type: :string, default: nil, aliases: '-s', desc: 'Suffix to append to index name'
60
66
  option :type, type: :string, default: nil, aliases: '-t', desc: 'Document Type to update mapping for'
67
+ option :settings, type: :hash, default: nil, desc: 'List of settings to pass to the index class. Example: --settings=index.refresh_interval:-1,index.number_of_replicas:0'
61
68
  def update_settings(*index_classes)
62
69
  require_relative 'index/update_settings'
63
- UpdateSettings.new(indices: index_classes, **options.to_h.transform_keys(&:to_sym)).run
70
+ opts = HashUtils.deep_transform_keys(options.to_h, &:to_sym)
71
+ UpdateSettings.new(indices: index_classes, **opts).run
64
72
  end
65
73
 
66
74
  desc 'update_mapping *INDEX_CLASS', 'Create or update a mapping'
@@ -89,8 +97,8 @@ module Esse
89
97
  option :suffix, type: :string, default: nil, aliases: '-s', desc: 'Suffix to append to index name'
90
98
  option :context, type: :hash, default: {}, required: true, desc: 'List of options to pass to the index class'
91
99
  option :repo, type: :string, default: nil, alias: '-r', desc: 'Repository to use for import'
92
- option :eager_include_document_attributes, type: :string, default: nil, desc: 'Comma separated list of lazy document attributes to include to the bulk index request'
93
- option :lazy_update_document_attributes, type: :string, default: nil, desc: 'Comma separated list of lazy document attributes to bulk update after the bulk index request'
100
+ option :eager_include_document_attributes, type: :string, default: nil, desc: 'Comma separated list of lazy document attributes to include to the bulk index request. Or pass `true` to include all lazy attributes'
101
+ option :lazy_update_document_attributes, type: :string, default: nil, desc: 'Comma separated list of lazy document attributes to bulk update after the bulk index request Or pass `true` to include all lazy attributes'
94
102
  def import(*index_classes)
95
103
  require_relative 'index/import'
96
104
  opts = HashUtils.deep_transform_keys(options.to_h, &:to_sym)
@@ -13,8 +13,10 @@ Esse.configure do |config|
13
13
 
14
14
  # Global index settings
15
15
  # cluster.settings = {
16
- # number_of_shards: 5,
17
- # number_of_replicas: 0,
16
+ # index: {
17
+ # number_of_shards: 5,
18
+ # number_of_replicas: 0,
19
+ # }
18
20
  # }
19
21
 
20
22
  # Global index mappings
data/lib/esse/config.rb CHANGED
@@ -10,8 +10,11 @@ module Esse
10
10
  # cluster.index_prefix = 'backend'
11
11
  # cluster.client = Elasticsearch::Client.new
12
12
  # cluster.settings = {
13
- # number_of_shards: 2,
14
- # number_of_replicas: 0
13
+ # index: {
14
+ # number_of_shards: 2,
15
+ # number_of_replicas: 1
16
+ # },
17
+ # analysis: { ... }
15
18
  # }
16
19
  # cluster.mappings = {
17
20
  # dynamic_templates: [...]
data/lib/esse/document.rb CHANGED
@@ -100,7 +100,10 @@ module Esse
100
100
  def inspect
101
101
  attributes = %i[id routing source].map do |attr|
102
102
  value = send(attr)
103
- "#{attr}: #{value.inspect}" if value
103
+ next unless value
104
+ "#{attr}: #{value.inspect}"
105
+ rescue
106
+ nil
104
107
  end.compact.join(', ')
105
108
  attributes << " mutations: #{@__mutations__.inspect}" if @__mutations__
106
109
  "#<#{self.class.name || 'Esse::Document'} #{attributes}>"
data/lib/esse/events.rb CHANGED
@@ -56,5 +56,7 @@ module Esse
56
56
  register_event 'elasticsearch.exist'
57
57
  register_event 'elasticsearch.count'
58
58
  register_event 'elasticsearch.get'
59
+ register_event 'elasticsearch.reindex'
60
+ register_event 'elasticsearch.update_by_query'
59
61
  end
60
62
  end
@@ -236,6 +236,20 @@ module Esse
236
236
  count
237
237
  end
238
238
 
239
+ # Update documents by query
240
+ #
241
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
242
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
243
+ #
244
+ # @return [Hash] The elasticsearch response hash
245
+ def update_by_query(suffix: nil, **options)
246
+ definition = {
247
+ index: index_name(suffix: suffix),
248
+ }.merge(options)
249
+ cluster.may_update_type!(definition)
250
+ cluster.api.update_by_query(**definition)
251
+ end
252
+
239
253
  protected
240
254
 
241
255
  def document?(doc)
@@ -26,10 +26,10 @@ module Esse
26
26
  #
27
27
  # @see http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
28
28
  # @see Esse::Transport#create_index
29
- def create_index(suffix: nil, body: nil, **options)
29
+ def create_index(suffix: nil, body: nil, settings: nil, **options)
30
30
  options = CREATE_INDEX_RESERVED_KEYWORDS.merge(options)
31
31
  name = build_real_index_name(suffix)
32
- definition = body || [settings_hash, mappings_hash].reduce(&:merge)
32
+ definition = body || [settings_hash(settings: settings), mappings_hash].reduce(&:merge)
33
33
 
34
34
  if options.delete(:alias) && name != index_name
35
35
  definition[:aliases] = { index_name => {} }
@@ -48,34 +48,38 @@ module Esse
48
48
  # @return [Hash] the elasticsearch response
49
49
  #
50
50
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-open-close.html
51
- def reset_index(suffix: index_suffix, optimize: true, import: true, reindex: false, **options)
51
+ def reset_index(suffix: index_suffix, settings: nil, optimize: true, import: true, reindex: false, refresh: nil, **options)
52
52
  cluster.throw_error_when_readonly!
53
53
 
54
54
  suffix ||= Esse.timestamp
55
55
  suffix = Esse.timestamp while index_exist?(suffix: suffix)
56
56
 
57
- if optimize
58
- definition = [settings_hash, mappings_hash].reduce(&:merge)
57
+ if optimize && import
58
+ definition = [settings_hash(settings: settings), mappings_hash].reduce(&:merge)
59
59
  number_of_replicas = definition.dig(Esse::SETTING_ROOT_KEY, :index, :number_of_replicas)
60
60
  refresh_interval = definition.dig(Esse::SETTING_ROOT_KEY, :index, :refresh_interval)
61
61
  new_number_of_replicas = ((definition[Esse::SETTING_ROOT_KEY] ||= {})[:index] ||= {})[:number_of_replicas] = 0
62
62
  new_refresh_interval = ((definition[Esse::SETTING_ROOT_KEY] ||= {})[:index] ||= {})[:refresh_interval] = '-1'
63
63
  create_index(**options, suffix: suffix, alias: false, body: definition)
64
64
  else
65
- create_index(**options, suffix: suffix, alias: false)
65
+ create_index(**options, suffix: suffix, alias: false, settings: settings)
66
66
  end
67
67
 
68
68
  if index_exist? && aliases.none?
69
69
  cluster.api.delete_index(index: index_name)
70
70
  end
71
71
  if import
72
- import(**options, suffix: suffix)
73
- elsif reindex && (_from = indices_pointing_to_alias).any?
74
- # @TODO: Reindex using the reindex API
72
+ import(**options, suffix: suffix, refresh: refresh)
73
+ elsif reindex && (source_indexes = indices_pointing_to_alias).any?
74
+ reindex_kwargs = reindex.is_a?(Hash) ? reindex : {}
75
+ reindex_kwargs[:wait_for_completion] = true unless reindex_kwargs.key?(:wait_for_completion)
76
+ source_indexes.each do |from|
77
+ cluster.api.reindex(**options, body: { source: { index: from }, dest: { index: index_name(suffix: suffix) } }, refresh: refresh)
78
+ end
75
79
  end
76
80
 
77
- if optimize && number_of_replicas != new_number_of_replicas || refresh_interval != new_refresh_interval
78
- update_settings(suffix: suffix)
81
+ if optimize && import && number_of_replicas != new_number_of_replicas || refresh_interval != new_refresh_interval
82
+ update_settings(suffix: suffix, settings: settings)
79
83
  refresh(suffix: suffix)
80
84
  end
81
85
 
@@ -152,16 +156,17 @@ module Esse
152
156
  #
153
157
  # @param :suffix [String, nil] :suffix The index suffix
154
158
  # @see Esse::Transport#update_settings
155
- def update_settings(suffix: nil, **options)
159
+ def update_settings(suffix: nil, settings: nil, **options)
156
160
  response = nil
157
161
 
158
- settings = HashUtils.deep_transform_keys(settings_hash.fetch(Esse::SETTING_ROOT_KEY), &:to_s)
162
+ settings = HashUtils.deep_transform_keys(settings_hash(settings: settings).fetch(Esse::SETTING_ROOT_KEY), &:to_sym)
159
163
  if options[:body]
160
- settings = settings.merge(HashUtils.deep_transform_keys(options.delete(:body), &:to_s))
164
+ body = HashUtils.deep_transform_keys(options.delete(:body), &:to_sym)
165
+ settings = HashUtils.deep_merge(settings, body)
161
166
  end
162
- settings.delete('number_of_shards') # Can't change number of shards for an index
163
- settings['index']&.delete('number_of_shards')
164
- analysis = settings.delete('analysis')
167
+ settings.delete(:number_of_shards) # Can't change number of shards for an index
168
+ settings[:index]&.delete(:number_of_shards)
169
+ analysis = settings.delete(:analysis)
165
170
 
166
171
  if settings.any?
167
172
  response = cluster.api.update_settings(index: index_name(suffix: suffix), body: settings, **options)
@@ -4,9 +4,28 @@ module Esse
4
4
  # https://github.com/elastic/elasticsearch-ruby/blob/master/elasticsearch-api/lib/elasticsearch/api/actions/indices/put_settings.rb
5
5
  class Index
6
6
  module ClassMethods
7
- def settings_hash
7
+ # Elasticsearch supports passing index.* related settings directly in the body of the request.
8
+ # We are moving it to the index key to make it more explicit and to be the source-of-truth when merging settings.
9
+ # So the settings `{ number_of_shards: 1 }` will be transformed to `{ index: { number_of_shards: 1 } }`
10
+ INDEX_SIMPLIFIED_SETTINGS = %i[
11
+ number_of_shards
12
+ number_of_replicas
13
+ refresh_interval
14
+ ].freeze
15
+
16
+ def settings_hash(settings: nil)
8
17
  hash = setting.body
9
- { Esse::SETTING_ROOT_KEY => (hash.key?(Esse::SETTING_ROOT_KEY) ? hash[Esse::SETTING_ROOT_KEY] : hash) }
18
+ values = (hash.key?(Esse::SETTING_ROOT_KEY) ? hash[Esse::SETTING_ROOT_KEY] : hash)
19
+ values = HashUtils.explode_keys(values)
20
+ if settings.is_a?(Hash)
21
+ values = HashUtils.deep_merge(values, HashUtils.explode_keys(settings))
22
+ end
23
+ INDEX_SIMPLIFIED_SETTINGS.each do |key|
24
+ next unless values.key?(key)
25
+
26
+ (values[:index] ||= {}).merge!(key => values.delete(key))
27
+ end
28
+ { Esse::SETTING_ROOT_KEY => values }
10
29
  end
11
30
 
12
31
  # Define /_settings definition by each index.
@@ -18,7 +37,7 @@ module Esse
18
37
  #
19
38
  # class UserIndex < Esse::Index
20
39
  # settings {
21
- # number_of_replicas: 4,
40
+ # index: { number_of_replicas: 4 }
22
41
  # }
23
42
  # end
24
43
  #
@@ -47,5 +47,15 @@ module Esse
47
47
  end
48
48
  end
49
49
  end
50
+
51
+ def explode_keys(hash, separator = '.')
52
+ hash.each_with_object({}) do |(key, value), result|
53
+ is_symbol = key.is_a?(Symbol)
54
+ keys = key.to_s.split(separator)
55
+ last_key = keys.pop
56
+ current = keys.reduce(result) { |memo, k| memo[is_symbol ? k.to_sym : k] ||= {} }
57
+ current[is_symbol ? last_key.to_sym : last_key] = value
58
+ end
59
+ end
50
60
  end
51
61
  end
@@ -185,6 +185,83 @@ module Esse
185
185
  payload[:response] = coerce_exception { client.indices.put_settings(**opts) }
186
186
  end
187
187
  end
188
+
189
+ # Allows to copy documents from one index to another, optionally filtering the source
190
+ # documents by a query, changing the destination index settings, or fetching the
191
+ # documents from a remote cluster.
192
+ #
193
+ # @option arguments [Boolean] :refresh Should the affected indexes be refreshed?
194
+ # @option arguments [Time] :timeout Time each individual bulk request should wait for shards that are unavailable.
195
+ # @option arguments [String] :wait_for_active_shards Sets the number of shard copies that must be active before proceeding with the reindex operation. Defaults to 1, meaning the primary shard only. Set to `all` for all shard copies, otherwise set to any non-negative value less than or equal to the total number of copies for the shard (number of replicas + 1)
196
+ # @option arguments [Boolean] :wait_for_completion Should the request should block until the reindex is complete.
197
+ # @option arguments [Number] :requests_per_second The throttle to set on this request in sub-requests per second. -1 means no throttle.
198
+ # @option arguments [Time] :scroll Control how long to keep the search context alive
199
+ # @option arguments [Number|string] :slices The number of slices this task should be divided into. Defaults to 1, meaning the task isn't sliced into subtasks. Can be set to `auto`.
200
+ # @option arguments [Number] :max_docs Maximum number of documents to process (default: all documents)
201
+ # @option arguments [Hash] :headers Custom HTTP headers
202
+ # @option arguments [Hash] :body The search definition using the Query DSL and the prototype for the index request. (*Required*)
203
+ #
204
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html
205
+ def reindex(body:, **options)
206
+ throw_error_when_readonly!
207
+
208
+ Esse::Events.instrument('elasticsearch.reindex') do |payload|
209
+ payload[:request] = opts = options.merge(body: body)
210
+ payload[:response] = coerce_exception { client.reindex(**opts) }
211
+ end
212
+ end
213
+
214
+ # Performs an update on every document in the index without changing the source,
215
+ # for example to pick up a mapping change.
216
+ #
217
+ # @option arguments [List] :index A comma-separated list of index names to search; use `_all` or empty string to perform the operation on all indices (*Required*)
218
+ # @option arguments [String] :analyzer The analyzer to use for the query string
219
+ # @option arguments [Boolean] :analyze_wildcard Specify whether wildcard and prefix queries should be analyzed (default: false)
220
+ # @option arguments [String] :default_operator The default operator for query string query (AND or OR) (options: AND, OR)
221
+ # @option arguments [String] :df The field to use as default where no field prefix is given in the query string
222
+ # @option arguments [Number] :from Starting offset (default: 0)
223
+ # @option arguments [Boolean] :ignore_unavailable Whether specified concrete indices should be ignored when unavailable (missing or closed)
224
+ # @option arguments [Boolean] :allow_no_indices Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified)
225
+ # @option arguments [String] :conflicts What to do when the update by query hits version conflicts? (options: abort, proceed)
226
+ # @option arguments [String] :expand_wildcards Whether to expand wildcard expression to concrete indices that are open, closed or both. (options: open, closed, hidden, none, all)
227
+ # @option arguments [Boolean] :lenient Specify whether format-based query failures (such as providing text to a numeric field) should be ignored
228
+ # @option arguments [String] :pipeline Ingest pipeline to set on index requests made by this action. (default: none)
229
+ # @option arguments [String] :preference Specify the node or shard the operation should be performed on (default: random)
230
+ # @option arguments [String] :q Query in the Lucene query string syntax
231
+ # @option arguments [List] :routing A comma-separated list of specific routing values
232
+ # @option arguments [Time] :scroll Specify how long a consistent view of the index should be maintained for scrolled search
233
+ # @option arguments [String] :search_type Search operation type (options: query_then_fetch, dfs_query_then_fetch)
234
+ # @option arguments [Time] :search_timeout Explicit timeout for each search request. Defaults to no timeout.
235
+ # @option arguments [Number] :size Deprecated, please use `max_docs` instead
236
+ # @option arguments [Number] :max_docs Maximum number of documents to process (default: all documents)
237
+ # @option arguments [List] :sort A comma-separated list of <field>:<direction> pairs
238
+ # @option arguments [List] :_source True or false to return the _source field or not, or a list of fields to return
239
+ # @option arguments [List] :_source_excludes A list of fields to exclude from the returned _source field
240
+ # @option arguments [List] :_source_includes A list of fields to extract and return from the _source field
241
+ # @option arguments [Number] :terminate_after The maximum number of documents to collect for each shard, upon reaching which the query execution will terminate early.
242
+ # @option arguments [List] :stats Specific 'tag' of the request for logging and statistical purposes
243
+ # @option arguments [Boolean] :version Specify whether to return document version as part of a hit
244
+ # @option arguments [Boolean] :version_type Should the document increment the version number (internal) on hit or not (reindex)
245
+ # @option arguments [Boolean] :request_cache Specify if request cache should be used for this request or not, defaults to index level setting
246
+ # @option arguments [Boolean] :refresh Should the affected indexes be refreshed?
247
+ # @option arguments [Time] :timeout Time each individual bulk request should wait for shards that are unavailable.
248
+ # @option arguments [String] :wait_for_active_shards Sets the number of shard copies that must be active before proceeding with the update by query operation. Defaults to 1, meaning the primary shard only. Set to `all` for all shard copies, otherwise set to any non-negative value less than or equal to the total number of copies for the shard (number of replicas + 1)
249
+ # @option arguments [Number] :scroll_size Size on the scroll request powering the update by query
250
+ # @option arguments [Boolean] :wait_for_completion Should the request should block until the update by query operation is complete.
251
+ # @option arguments [Number] :requests_per_second The throttle to set on this request in sub-requests per second. -1 means no throttle.
252
+ # @option arguments [Number|string] :slices The number of slices this task should be divided into. Defaults to 1, meaning the task isn't sliced into subtasks. Can be set to `auto`.
253
+ # @option arguments [Hash] :headers Custom HTTP headers
254
+ # @option arguments [Hash] :body The search definition using the Query DSL
255
+ #
256
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html
257
+ def update_by_query(index:, **options)
258
+ throw_error_when_readonly!
259
+
260
+ Esse::Events.instrument('elasticsearch.update_by_query') do |payload|
261
+ payload[:request] = opts = options.merge(index: index)
262
+ payload[:response] = coerce_exception { client.update_by_query(**opts) }
263
+ end
264
+ end
188
265
  end
189
266
 
190
267
  include InstanceMethods
data/lib/esse/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Esse
4
- VERSION = '0.3.4'
4
+ VERSION = '0.3.5'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: esse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcos G. Zimmermann
8
8
  autorequire:
9
9
  bindir: exec
10
10
  cert_chain: []
11
- date: 2024-07-18 00:00:00.000000000 Z
11
+ date: 2024-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json