@elastic/elasticsearch 7.15.0

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 (103) hide show
  1. package/.dockerignore +5 -0
  2. package/LICENSE +202 -0
  3. package/README.md +232 -0
  4. package/api/api/async_search.js +141 -0
  5. package/api/api/autoscaling.js +147 -0
  6. package/api/api/bulk.js +70 -0
  7. package/api/api/cat.js +648 -0
  8. package/api/api/ccr.js +403 -0
  9. package/api/api/clear_scroll.js +55 -0
  10. package/api/api/close_point_in_time.js +50 -0
  11. package/api/api/cluster.js +420 -0
  12. package/api/api/count.js +64 -0
  13. package/api/api/create.js +69 -0
  14. package/api/api/dangling_indices.js +115 -0
  15. package/api/api/delete.js +65 -0
  16. package/api/api/delete_by_query.js +71 -0
  17. package/api/api/delete_by_query_rethrottle.js +60 -0
  18. package/api/api/delete_script.js +56 -0
  19. package/api/api/enrich.js +173 -0
  20. package/api/api/eql.js +150 -0
  21. package/api/api/exists.js +65 -0
  22. package/api/api/exists_source.js +74 -0
  23. package/api/api/explain.js +65 -0
  24. package/api/api/features.js +81 -0
  25. package/api/api/field_caps.js +55 -0
  26. package/api/api/fleet.js +65 -0
  27. package/api/api/get.js +65 -0
  28. package/api/api/get_script.js +56 -0
  29. package/api/api/get_script_context.js +50 -0
  30. package/api/api/get_script_languages.js +50 -0
  31. package/api/api/get_source.js +65 -0
  32. package/api/api/graph.js +72 -0
  33. package/api/api/ilm.js +317 -0
  34. package/api/api/index.js +71 -0
  35. package/api/api/indices.js +1753 -0
  36. package/api/api/info.js +50 -0
  37. package/api/api/ingest.js +200 -0
  38. package/api/api/license.js +188 -0
  39. package/api/api/logstash.js +125 -0
  40. package/api/api/mget.js +70 -0
  41. package/api/api/migration.js +60 -0
  42. package/api/api/ml.js +2010 -0
  43. package/api/api/monitoring.js +66 -0
  44. package/api/api/msearch.js +70 -0
  45. package/api/api/msearch_template.js +70 -0
  46. package/api/api/mtermvectors.js +64 -0
  47. package/api/api/nodes.js +268 -0
  48. package/api/api/open_point_in_time.js +56 -0
  49. package/api/api/ping.js +50 -0
  50. package/api/api/put_script.js +71 -0
  51. package/api/api/rank_eval.js +61 -0
  52. package/api/api/reindex.js +56 -0
  53. package/api/api/reindex_rethrottle.js +60 -0
  54. package/api/api/render_search_template.js +55 -0
  55. package/api/api/rollup.js +319 -0
  56. package/api/api/scripts_painless_execute.js +50 -0
  57. package/api/api/scroll.js +55 -0
  58. package/api/api/search.js +64 -0
  59. package/api/api/search_mvt.js +87 -0
  60. package/api/api/search_shards.js +55 -0
  61. package/api/api/search_template.js +70 -0
  62. package/api/api/searchable_snapshots.js +186 -0
  63. package/api/api/security.js +1261 -0
  64. package/api/api/shutdown.js +124 -0
  65. package/api/api/slm.js +256 -0
  66. package/api/api/snapshot.js +439 -0
  67. package/api/api/sql.js +203 -0
  68. package/api/api/ssl.js +55 -0
  69. package/api/api/tasks.js +108 -0
  70. package/api/api/terms_enum.js +56 -0
  71. package/api/api/termvectors.js +67 -0
  72. package/api/api/text_structure.js +65 -0
  73. package/api/api/transform.js +268 -0
  74. package/api/api/update.js +69 -0
  75. package/api/api/update_by_query.js +67 -0
  76. package/api/api/update_by_query_rethrottle.js +60 -0
  77. package/api/api/watcher.js +333 -0
  78. package/api/api/xpack.js +76 -0
  79. package/api/index.js +508 -0
  80. package/api/new.d.ts +1585 -0
  81. package/api/requestParams.d.ts +2920 -0
  82. package/api/types.d.ts +15420 -0
  83. package/api/utils.js +58 -0
  84. package/codecov.yml +14 -0
  85. package/index.d.ts +2991 -0
  86. package/index.js +349 -0
  87. package/index.mjs +29 -0
  88. package/lib/Connection.d.ts +99 -0
  89. package/lib/Connection.js +392 -0
  90. package/lib/Helpers.d.ts +124 -0
  91. package/lib/Helpers.js +770 -0
  92. package/lib/Serializer.d.ts +30 -0
  93. package/lib/Serializer.js +94 -0
  94. package/lib/Transport.d.ts +162 -0
  95. package/lib/Transport.js +689 -0
  96. package/lib/errors.d.ts +90 -0
  97. package/lib/errors.js +159 -0
  98. package/lib/pool/BaseConnectionPool.js +262 -0
  99. package/lib/pool/CloudConnectionPool.js +64 -0
  100. package/lib/pool/ConnectionPool.js +246 -0
  101. package/lib/pool/index.d.ts +220 -0
  102. package/lib/pool/index.js +30 -0
  103. package/package.json +106 -0
package/lib/Helpers.js ADDED
@@ -0,0 +1,770 @@
1
+ /*
2
+ * Licensed to Elasticsearch B.V. under one or more contributor
3
+ * license agreements. See the NOTICE file distributed with
4
+ * this work for additional information regarding copyright
5
+ * ownership. Elasticsearch B.V. licenses this file to you under
6
+ * the Apache License, Version 2.0 (the "License"); you may
7
+ * not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+
20
+ 'use strict'
21
+
22
+ /* eslint camelcase: 0 */
23
+
24
+ const { Readable } = require('stream')
25
+ const { promisify } = require('util')
26
+ const { ResponseError, ConfigurationError } = require('./errors')
27
+
28
+ const pImmediate = promisify(setImmediate)
29
+ const sleep = promisify(setTimeout)
30
+ const kClient = Symbol('elasticsearch-client')
31
+ const kMetaHeader = Symbol('meta header')
32
+ /* istanbul ignore next */
33
+ const noop = () => {}
34
+
35
+ class Helpers {
36
+ constructor (opts) {
37
+ this[kClient] = opts.client
38
+ this[kMetaHeader] = opts.metaHeader
39
+ this.maxRetries = opts.maxRetries
40
+ }
41
+
42
+ /**
43
+ * Runs a search operation. The only difference between client.search and this utility,
44
+ * is that we are only returning the hits to the user and not the full ES response.
45
+ * This helper automatically adds `filter_path=hits.hits._source` to the querystring,
46
+ * as it will only need the documents source.
47
+ * @param {object} params - The Elasticsearch's search parameters.
48
+ * @param {object} options - The client optional configuration for this request.
49
+ * @return {array} The documents that matched the request.
50
+ */
51
+ async search (params, options) {
52
+ appendFilterPath('hits.hits._source', params, true)
53
+ const { body } = await this[kClient].search(params, options)
54
+ if (body.hits && body.hits.hits) {
55
+ return body.hits.hits.map(d => d._source)
56
+ }
57
+ return []
58
+ }
59
+
60
+ /**
61
+ * Runs a scroll search operation. This function returns an async iterator, allowing
62
+ * the user to use a for await loop to get all the results of a given search.
63
+ * ```js
64
+ * for await (const result of client.helpers.scrollSearch({ params })) {
65
+ * console.log(result)
66
+ * }
67
+ * ```
68
+ * Each result represents the entire body of a single scroll search request,
69
+ * if you just need to scroll the results, use scrollDocuments.
70
+ * This function handles automatically retries on 429 status code.
71
+ * @param {object} params - The Elasticsearch's search parameters.
72
+ * @param {object} options - The client optional configuration for this request.
73
+ * @return {iterator} the async iterator
74
+ */
75
+ async * scrollSearch (params, options = {}) {
76
+ if (this[kMetaHeader] !== null) {
77
+ options.headers = options.headers || {}
78
+ options.headers['x-elastic-client-meta'] = this[kMetaHeader] + ',h=s'
79
+ }
80
+ // TODO: study scroll search slices
81
+ const wait = options.wait || 5000
82
+ const maxRetries = options.maxRetries || this.maxRetries
83
+ if (Array.isArray(options.ignore)) {
84
+ options.ignore.push(429)
85
+ } else {
86
+ options.ignore = [429]
87
+ }
88
+ params.scroll = params.scroll || '1m'
89
+ appendFilterPath('_scroll_id', params, false)
90
+ const { method, body, index, ...querystring } = params
91
+
92
+ let response = null
93
+ for (let i = 0; i <= maxRetries; i++) {
94
+ response = await this[kClient].search(params, options)
95
+ if (response.statusCode !== 429) break
96
+ await sleep(wait)
97
+ }
98
+ if (response.statusCode === 429) {
99
+ throw new ResponseError(response)
100
+ }
101
+
102
+ let scroll_id = response.body._scroll_id
103
+ let stop = false
104
+ const clear = async () => {
105
+ stop = true
106
+ await this[kClient].clearScroll(
107
+ { body: { scroll_id } },
108
+ { ignore: [400], ...options }
109
+ )
110
+ }
111
+
112
+ while (response.body.hits && response.body.hits.hits.length > 0) {
113
+ // scroll id is always present in the response, but it might
114
+ // change over time based on the number of shards
115
+ scroll_id = response.body._scroll_id
116
+ response.clear = clear
117
+ addDocumentsGetter(response)
118
+
119
+ yield response
120
+
121
+ if (stop === true) {
122
+ break
123
+ }
124
+
125
+ for (let i = 0; i <= maxRetries; i++) {
126
+ response = await this[kClient].scroll({
127
+ scroll: querystring.scroll,
128
+ rest_total_hits_as_int: querystring.rest_total_hits_as_int || querystring.restTotalHitsAsInt,
129
+ body: { scroll_id }
130
+ }, options)
131
+ if (response.statusCode !== 429) break
132
+ await sleep(wait)
133
+ }
134
+ if (response.statusCode === 429) {
135
+ throw new ResponseError(response)
136
+ }
137
+ }
138
+
139
+ if (stop === false) {
140
+ await clear()
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Runs a scroll search operation. This function returns an async iterator, allowing
146
+ * the user to use a for await loop to get all the documents of a given search.
147
+ * ```js
148
+ * for await (const document of client.helpers.scrollSearch({ params })) {
149
+ * console.log(document)
150
+ * }
151
+ * ```
152
+ * Each document is what you will find by running a scrollSearch and iterating on the hits array.
153
+ * This helper automatically adds `filter_path=hits.hits._source` to the querystring,
154
+ * as it will only need the documents source.
155
+ * @param {object} params - The Elasticsearch's search parameters.
156
+ * @param {object} options - The client optional configuration for this request.
157
+ * @return {iterator} the async iterator
158
+ */
159
+ async * scrollDocuments (params, options) {
160
+ appendFilterPath('hits.hits._source', params, true)
161
+ for await (const { documents } of this.scrollSearch(params, options)) {
162
+ for (const document of documents) {
163
+ yield document
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Creates a msearch helper instance. Once you configure it, you can use the provided
170
+ * `search` method to add new searches in the queue.
171
+ * @param {object} options - The configuration of the msearch operations.
172
+ * @param {object} reqOptions - The client optional configuration for this request.
173
+ * @return {object} The possible operations to run.
174
+ */
175
+ msearch (options = {}, reqOptions = {}) {
176
+ const client = this[kClient]
177
+ const {
178
+ operations = 5,
179
+ concurrency = 5,
180
+ flushInterval = 500,
181
+ retries = this.maxRetries,
182
+ wait = 5000,
183
+ ...msearchOptions
184
+ } = options
185
+
186
+ let stopReading = false
187
+ let stopError = null
188
+ let timeoutRef = null
189
+ const operationsStream = new Readable({
190
+ objectMode: true,
191
+ read (size) {}
192
+ })
193
+
194
+ const p = iterate()
195
+ const helper = {
196
+ then (onFulfilled, onRejected) {
197
+ return p.then(onFulfilled, onRejected)
198
+ },
199
+ catch (onRejected) {
200
+ return p.catch(onRejected)
201
+ },
202
+ stop (error = null) {
203
+ if (stopReading === true) return
204
+ stopReading = true
205
+ stopError = error
206
+ operationsStream.push(null)
207
+ },
208
+ // TODO: support abort a single search?
209
+ // NOTE: the validation checks are synchronous and the callback/promise will
210
+ // be resolved in the same tick. We might want to fix this in the future.
211
+ search (header, body, callback) {
212
+ if (stopReading === true) {
213
+ const error = stopError === null
214
+ ? new ConfigurationError('The msearch processor has been stopped')
215
+ : stopError
216
+ return callback ? callback(error, {}) : Promise.reject(error)
217
+ }
218
+
219
+ if (!(typeof header === 'object' && header !== null && !Array.isArray(header))) {
220
+ const error = new ConfigurationError('The header should be an object')
221
+ return callback ? callback(error, {}) : Promise.reject(error)
222
+ }
223
+
224
+ if (!(typeof body === 'object' && body !== null && !Array.isArray(body))) {
225
+ const error = new ConfigurationError('The body should be an object')
226
+ return callback ? callback(error, {}) : Promise.reject(error)
227
+ }
228
+
229
+ let promise = null
230
+ if (callback === undefined) {
231
+ let onFulfilled = null
232
+ let onRejected = null
233
+ promise = new Promise((resolve, reject) => {
234
+ onFulfilled = resolve
235
+ onRejected = reject
236
+ })
237
+ callback = function callback (err, result) {
238
+ err ? onRejected(err) : onFulfilled(result)
239
+ }
240
+ }
241
+
242
+ operationsStream.push([header, body, callback])
243
+
244
+ if (promise !== null) {
245
+ return promise
246
+ }
247
+ }
248
+ }
249
+
250
+ return helper
251
+
252
+ async function iterate () {
253
+ const { semaphore, finish } = buildSemaphore()
254
+ const msearchBody = []
255
+ const callbacks = []
256
+ let loadedOperations = 0
257
+ timeoutRef = setTimeout(onFlushTimeout, flushInterval)
258
+
259
+ for await (const operation of operationsStream) {
260
+ timeoutRef.refresh()
261
+ loadedOperations += 1
262
+ msearchBody.push(operation[0], operation[1])
263
+ callbacks.push(operation[2])
264
+ if (loadedOperations >= operations) {
265
+ const send = await semaphore()
266
+ send(msearchBody.slice(), callbacks.slice())
267
+ msearchBody.length = 0
268
+ callbacks.length = 0
269
+ loadedOperations = 0
270
+ }
271
+ }
272
+
273
+ clearTimeout(timeoutRef)
274
+ // In some cases the previos http call does not have finished,
275
+ // or we didn't reach the flush bytes threshold, so we force one last operation.
276
+ if (loadedOperations > 0) {
277
+ const send = await semaphore()
278
+ send(msearchBody, callbacks)
279
+ }
280
+
281
+ await finish()
282
+
283
+ if (stopError !== null) {
284
+ throw stopError
285
+ }
286
+
287
+ async function onFlushTimeout () {
288
+ if (loadedOperations === 0) return
289
+ const msearchBodyCopy = msearchBody.slice()
290
+ const callbacksCopy = callbacks.slice()
291
+ msearchBody.length = 0
292
+ callbacks.length = 0
293
+ loadedOperations = 0
294
+ try {
295
+ const send = await semaphore()
296
+ send(msearchBodyCopy, callbacksCopy)
297
+ } catch (err) {
298
+ /* istanbul ignore next */
299
+ helper.stop(err)
300
+ }
301
+ }
302
+ }
303
+
304
+ // This function builds a semaphore using the concurrency
305
+ // options of the msearch helper. It is used inside the iterator
306
+ // to guarantee that no more than the number of operations
307
+ // allowed to run at the same time are executed.
308
+ // It returns a semaphore function which resolves in the next tick
309
+ // if we didn't reach the maximim concurrency yet, otherwise it returns
310
+ // a promise that resolves as soon as one of the running request has finshed.
311
+ // The semaphore function resolves a send function, which will be used
312
+ // to send the actual msearch request.
313
+ // It also returns a finish function, which returns a promise that is resolved
314
+ // when there are no longer request running.
315
+ function buildSemaphore () {
316
+ let resolveSemaphore = null
317
+ let resolveFinish = null
318
+ let running = 0
319
+
320
+ return { semaphore, finish }
321
+
322
+ function finish () {
323
+ return new Promise((resolve, reject) => {
324
+ if (running === 0) {
325
+ resolve()
326
+ } else {
327
+ resolveFinish = resolve
328
+ }
329
+ })
330
+ }
331
+
332
+ function semaphore () {
333
+ if (running < concurrency) {
334
+ running += 1
335
+ return pImmediate(send)
336
+ } else {
337
+ return new Promise((resolve, reject) => {
338
+ resolveSemaphore = resolve
339
+ })
340
+ }
341
+ }
342
+
343
+ function send (msearchBody, callbacks) {
344
+ /* istanbul ignore if */
345
+ if (running > concurrency) {
346
+ throw new Error('Max concurrency reached')
347
+ }
348
+ msearchOperation(msearchBody, callbacks, () => {
349
+ running -= 1
350
+ if (resolveSemaphore) {
351
+ running += 1
352
+ resolveSemaphore(send)
353
+ resolveSemaphore = null
354
+ } else if (resolveFinish && running === 0) {
355
+ resolveFinish()
356
+ }
357
+ })
358
+ }
359
+ }
360
+
361
+ function msearchOperation (msearchBody, callbacks, done) {
362
+ let retryCount = retries
363
+
364
+ // Instead of going full on async-await, which would make the code easier to read,
365
+ // we have decided to use callback style instead.
366
+ // This because every time we use async await, V8 will create multiple promises
367
+ // behind the scenes, making the code slightly slower.
368
+ tryMsearch(msearchBody, callbacks, retrySearch)
369
+ function retrySearch (msearchBody, callbacks) {
370
+ if (msearchBody.length > 0 && retryCount > 0) {
371
+ retryCount -= 1
372
+ setTimeout(tryMsearch, wait, msearchBody, callbacks, retrySearch)
373
+ return
374
+ }
375
+
376
+ done()
377
+ }
378
+
379
+ // This function never returns an error, if the msearch operation fails,
380
+ // the error is dispatched to all search executors.
381
+ function tryMsearch (msearchBody, callbacks, done) {
382
+ client.msearch(Object.assign({}, msearchOptions, { body: msearchBody }), reqOptions, (err, results) => {
383
+ const retryBody = []
384
+ const retryCallbacks = []
385
+ if (err) {
386
+ addDocumentsGetter(results)
387
+ for (const callback of callbacks) {
388
+ callback(err, results)
389
+ }
390
+ return done(retryBody, retryCallbacks)
391
+ }
392
+ const { responses } = results.body
393
+ for (let i = 0, len = responses.length; i < len; i++) {
394
+ const response = responses[i]
395
+ if (response.status === 429 && retryCount > 0) {
396
+ retryBody.push(msearchBody[i * 2])
397
+ retryBody.push(msearchBody[(i * 2) + 1])
398
+ retryCallbacks.push(callbacks[i])
399
+ continue
400
+ }
401
+ const result = { ...results, body: response }
402
+ addDocumentsGetter(result)
403
+ if (response.status >= 400) {
404
+ callbacks[i](new ResponseError(result), result)
405
+ } else {
406
+ callbacks[i](null, result)
407
+ }
408
+ }
409
+ done(retryBody, retryCallbacks)
410
+ })
411
+ }
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Creates a bulk helper instance. Once you configure it, you can pick which operation
417
+ * to execute with the given dataset, index, create, update, and delete.
418
+ * @param {object} options - The configuration of the bulk operation.
419
+ * @param {object} reqOptions - The client optional configuration for this request.
420
+ * @return {object} The possible operations to run with the datasource.
421
+ */
422
+ bulk (options, reqOptions = {}) {
423
+ const client = this[kClient]
424
+ const { serializer } = client
425
+ if (this[kMetaHeader] !== null) {
426
+ reqOptions.headers = reqOptions.headers || {}
427
+ reqOptions.headers['x-elastic-client-meta'] = this[kMetaHeader] + ',h=bp'
428
+ }
429
+ const {
430
+ datasource,
431
+ onDocument,
432
+ flushBytes = 5000000,
433
+ flushInterval = 30000,
434
+ concurrency = 5,
435
+ retries = this.maxRetries,
436
+ wait = 5000,
437
+ onDrop = noop,
438
+ refreshOnCompletion = false,
439
+ ...bulkOptions
440
+ } = options
441
+
442
+ if (datasource === undefined) {
443
+ return Promise.reject(new ConfigurationError('bulk helper: the datasource is required'))
444
+ }
445
+ if (!(Array.isArray(datasource) || Buffer.isBuffer(datasource) || typeof datasource.pipe === 'function' || datasource[Symbol.asyncIterator])) {
446
+ return Promise.reject(new ConfigurationError('bulk helper: the datasource must be an array or a buffer or a readable stream or an async generator'))
447
+ }
448
+ if (onDocument === undefined) {
449
+ return Promise.reject(new ConfigurationError('bulk helper: the onDocument callback is required'))
450
+ }
451
+
452
+ let shouldAbort = false
453
+ let timeoutRef = null
454
+ const stats = {
455
+ total: 0,
456
+ failed: 0,
457
+ retry: 0,
458
+ successful: 0,
459
+ noop: 0,
460
+ time: 0,
461
+ bytes: 0,
462
+ aborted: false
463
+ }
464
+
465
+ const p = iterate()
466
+ const helper = {
467
+ get stats () {
468
+ return stats
469
+ },
470
+ then (onFulfilled, onRejected) {
471
+ return p.then(onFulfilled, onRejected)
472
+ },
473
+ catch (onRejected) {
474
+ return p.catch(onRejected)
475
+ },
476
+ abort () {
477
+ clearTimeout(timeoutRef)
478
+ shouldAbort = true
479
+ stats.aborted = true
480
+ return this
481
+ }
482
+ }
483
+
484
+ return helper
485
+
486
+ /**
487
+ * Function that iterates over the given datasource and start a bulk operation as soon
488
+ * as it reaches the configured bulk size. It's designed to use the Node.js asynchronous
489
+ * model at this maximum capacity, as it will collect the next body to send while there is
490
+ * a running http call. In this way, the CPU time will be used carefully.
491
+ * The objects will be serialized right away, to approximate the byte length of the body.
492
+ * It creates an array of strings instead of a ndjson string because the bulkOperation
493
+ * will navigate the body for matching failed operations with the original document.
494
+ */
495
+ async function iterate () {
496
+ const { semaphore, finish } = buildSemaphore()
497
+ const startTime = Date.now()
498
+ const bulkBody = []
499
+ let actionBody = ''
500
+ let payloadBody = ''
501
+ let chunkBytes = 0
502
+ timeoutRef = setTimeout(onFlushTimeout, flushInterval)
503
+
504
+ for await (const chunk of datasource) {
505
+ if (shouldAbort === true) break
506
+ timeoutRef.refresh()
507
+ const action = onDocument(chunk)
508
+ const operation = Array.isArray(action)
509
+ ? Object.keys(action[0])[0]
510
+ : Object.keys(action)[0]
511
+ if (operation === 'index' || operation === 'create') {
512
+ actionBody = serializer.serialize(action)
513
+ payloadBody = typeof chunk === 'string' ? chunk : serializer.serialize(chunk)
514
+ chunkBytes += Buffer.byteLength(actionBody) + Buffer.byteLength(payloadBody)
515
+ bulkBody.push(actionBody, payloadBody)
516
+ } else if (operation === 'update') {
517
+ actionBody = serializer.serialize(action[0])
518
+ payloadBody = typeof chunk === 'string'
519
+ ? `{"doc":${chunk}}`
520
+ : serializer.serialize({ doc: chunk, ...action[1] })
521
+ chunkBytes += Buffer.byteLength(actionBody) + Buffer.byteLength(payloadBody)
522
+ bulkBody.push(actionBody, payloadBody)
523
+ } else if (operation === 'delete') {
524
+ actionBody = serializer.serialize(action)
525
+ chunkBytes += Buffer.byteLength(actionBody)
526
+ bulkBody.push(actionBody)
527
+ } else {
528
+ clearTimeout(timeoutRef)
529
+ throw new ConfigurationError(`Bulk helper invalid action: '${operation}'`)
530
+ }
531
+
532
+ if (chunkBytes >= flushBytes) {
533
+ stats.bytes += chunkBytes
534
+ const send = await semaphore()
535
+ send(bulkBody.slice())
536
+ bulkBody.length = 0
537
+ chunkBytes = 0
538
+ }
539
+ }
540
+
541
+ clearTimeout(timeoutRef)
542
+ // In some cases the previos http call does not have finished,
543
+ // or we didn't reach the flush bytes threshold, so we force one last operation.
544
+ if (shouldAbort === false && chunkBytes > 0) {
545
+ const send = await semaphore()
546
+ stats.bytes += chunkBytes
547
+ send(bulkBody)
548
+ }
549
+
550
+ await finish()
551
+
552
+ if (refreshOnCompletion) {
553
+ await client.indices.refresh({
554
+ index: typeof refreshOnCompletion === 'string'
555
+ ? refreshOnCompletion
556
+ : '_all'
557
+ }, reqOptions)
558
+ }
559
+
560
+ stats.time = Date.now() - startTime
561
+ stats.total = stats.successful + stats.failed
562
+
563
+ return stats
564
+
565
+ async function onFlushTimeout () {
566
+ if (chunkBytes === 0) return
567
+ stats.bytes += chunkBytes
568
+ const bulkBodyCopy = bulkBody.slice()
569
+ bulkBody.length = 0
570
+ chunkBytes = 0
571
+ try {
572
+ const send = await semaphore()
573
+ send(bulkBodyCopy)
574
+ } catch (err) {
575
+ /* istanbul ignore next */
576
+ helper.abort()
577
+ }
578
+ }
579
+ }
580
+
581
+ // This function builds a semaphore using the concurrency
582
+ // options of the bulk helper. It is used inside the iterator
583
+ // to guarantee that no more than the number of operations
584
+ // allowed to run at the same time are executed.
585
+ // It returns a semaphore function which resolves in the next tick
586
+ // if we didn't reach the maximim concurrency yet, otherwise it returns
587
+ // a promise that resolves as soon as one of the running request has finshed.
588
+ // The semaphore function resolves a send function, which will be used
589
+ // to send the actual bulk request.
590
+ // It also returns a finish function, which returns a promise that is resolved
591
+ // when there are no longer request running. It rejects an error if one
592
+ // of the request has failed for some reason.
593
+ function buildSemaphore () {
594
+ let resolveSemaphore = null
595
+ let resolveFinish = null
596
+ let rejectFinish = null
597
+ let error = null
598
+ let running = 0
599
+
600
+ return { semaphore, finish }
601
+
602
+ function finish () {
603
+ return new Promise((resolve, reject) => {
604
+ if (running === 0) {
605
+ if (error) {
606
+ reject(error)
607
+ } else {
608
+ resolve()
609
+ }
610
+ } else {
611
+ resolveFinish = resolve
612
+ rejectFinish = reject
613
+ }
614
+ })
615
+ }
616
+
617
+ function semaphore () {
618
+ if (running < concurrency) {
619
+ running += 1
620
+ return pImmediate(send)
621
+ } else {
622
+ return new Promise((resolve, reject) => {
623
+ resolveSemaphore = resolve
624
+ })
625
+ }
626
+ }
627
+
628
+ function send (bulkBody) {
629
+ /* istanbul ignore if */
630
+ if (running > concurrency) {
631
+ throw new Error('Max concurrency reached')
632
+ }
633
+ bulkOperation(bulkBody, err => {
634
+ running -= 1
635
+ if (err) {
636
+ shouldAbort = true
637
+ error = err
638
+ }
639
+ if (resolveSemaphore) {
640
+ running += 1
641
+ resolveSemaphore(send)
642
+ resolveSemaphore = null
643
+ } else if (resolveFinish && running === 0) {
644
+ if (error) {
645
+ rejectFinish(error)
646
+ } else {
647
+ resolveFinish()
648
+ }
649
+ }
650
+ })
651
+ }
652
+ }
653
+
654
+ function bulkOperation (bulkBody, callback) {
655
+ let retryCount = retries
656
+ let isRetrying = false
657
+
658
+ // Instead of going full on async-await, which would make the code easier to read,
659
+ // we have decided to use callback style instead.
660
+ // This because every time we use async await, V8 will create multiple promises
661
+ // behind the scenes, making the code slightly slower.
662
+ tryBulk(bulkBody, retryDocuments)
663
+ function retryDocuments (err, bulkBody) {
664
+ if (err) return callback(err)
665
+ if (shouldAbort === true) return callback()
666
+
667
+ if (bulkBody.length > 0) {
668
+ if (retryCount > 0) {
669
+ isRetrying = true
670
+ retryCount -= 1
671
+ stats.retry += bulkBody.length
672
+ setTimeout(tryBulk, wait, bulkBody, retryDocuments)
673
+ return
674
+ }
675
+ for (let i = 0, len = bulkBody.length; i < len; i = i + 2) {
676
+ const operation = Object.keys(serializer.deserialize(bulkBody[i]))[0]
677
+ onDrop({
678
+ status: 429,
679
+ error: null,
680
+ operation: serializer.deserialize(bulkBody[i]),
681
+ document: operation !== 'delete'
682
+ ? serializer.deserialize(bulkBody[i + 1])
683
+ /* istanbul ignore next */
684
+ : null,
685
+ retried: isRetrying
686
+ })
687
+ stats.failed += 1
688
+ }
689
+ }
690
+ callback()
691
+ }
692
+
693
+ function tryBulk (bulkBody, callback) {
694
+ if (shouldAbort === true) return callback(null, [])
695
+ client.bulk(Object.assign({}, bulkOptions, { body: bulkBody }), reqOptions, (err, { body }) => {
696
+ if (err) return callback(err, null)
697
+ if (body.errors === false) {
698
+ stats.successful += body.items.length
699
+ for (const item of body.items) {
700
+ if (item.update && item.update.result === 'noop') {
701
+ stats.noop++
702
+ }
703
+ }
704
+ return callback(null, [])
705
+ }
706
+ const retry = []
707
+ const { items } = body
708
+ for (let i = 0, len = items.length; i < len; i++) {
709
+ const action = items[i]
710
+ const operation = Object.keys(action)[0]
711
+ const { status } = action[operation]
712
+ const indexSlice = operation !== 'delete' ? i * 2 : i
713
+
714
+ if (status >= 400) {
715
+ // 429 is the only staus code where we might want to retry
716
+ // a document, because it was not an error in the document itself,
717
+ // but the ES node were handling too many operations.
718
+ if (status === 429) {
719
+ retry.push(bulkBody[indexSlice])
720
+ /* istanbul ignore next */
721
+ if (operation !== 'delete') {
722
+ retry.push(bulkBody[indexSlice + 1])
723
+ }
724
+ } else {
725
+ onDrop({
726
+ status: status,
727
+ error: action[operation].error,
728
+ operation: serializer.deserialize(bulkBody[indexSlice]),
729
+ document: operation !== 'delete'
730
+ ? serializer.deserialize(bulkBody[indexSlice + 1])
731
+ : null,
732
+ retried: isRetrying
733
+ })
734
+ stats.failed += 1
735
+ }
736
+ } else {
737
+ stats.successful += 1
738
+ }
739
+ }
740
+ callback(null, retry)
741
+ })
742
+ }
743
+ }
744
+ }
745
+ }
746
+
747
+ // Using a getter will improve the overall performances of the code,
748
+ // as we will reed the documents only if needed.
749
+ function addDocumentsGetter (result) {
750
+ Object.defineProperty(result, 'documents', {
751
+ get () {
752
+ if (this.body.hits && this.body.hits.hits) {
753
+ return this.body.hits.hits.map(d => d._source)
754
+ }
755
+ return []
756
+ }
757
+ })
758
+ }
759
+
760
+ function appendFilterPath (filter, params, force) {
761
+ if (params.filter_path !== undefined) {
762
+ params.filter_path += ',' + filter
763
+ } else if (params.filterPath !== undefined) {
764
+ params.filterPath += ',' + filter
765
+ } else if (force === true) {
766
+ params.filter_path = filter
767
+ }
768
+ }
769
+
770
+ module.exports = Helpers