@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.
- package/.dockerignore +5 -0
- package/LICENSE +202 -0
- package/README.md +232 -0
- package/api/api/async_search.js +141 -0
- package/api/api/autoscaling.js +147 -0
- package/api/api/bulk.js +70 -0
- package/api/api/cat.js +648 -0
- package/api/api/ccr.js +403 -0
- package/api/api/clear_scroll.js +55 -0
- package/api/api/close_point_in_time.js +50 -0
- package/api/api/cluster.js +420 -0
- package/api/api/count.js +64 -0
- package/api/api/create.js +69 -0
- package/api/api/dangling_indices.js +115 -0
- package/api/api/delete.js +65 -0
- package/api/api/delete_by_query.js +71 -0
- package/api/api/delete_by_query_rethrottle.js +60 -0
- package/api/api/delete_script.js +56 -0
- package/api/api/enrich.js +173 -0
- package/api/api/eql.js +150 -0
- package/api/api/exists.js +65 -0
- package/api/api/exists_source.js +74 -0
- package/api/api/explain.js +65 -0
- package/api/api/features.js +81 -0
- package/api/api/field_caps.js +55 -0
- package/api/api/fleet.js +65 -0
- package/api/api/get.js +65 -0
- package/api/api/get_script.js +56 -0
- package/api/api/get_script_context.js +50 -0
- package/api/api/get_script_languages.js +50 -0
- package/api/api/get_source.js +65 -0
- package/api/api/graph.js +72 -0
- package/api/api/ilm.js +317 -0
- package/api/api/index.js +71 -0
- package/api/api/indices.js +1753 -0
- package/api/api/info.js +50 -0
- package/api/api/ingest.js +200 -0
- package/api/api/license.js +188 -0
- package/api/api/logstash.js +125 -0
- package/api/api/mget.js +70 -0
- package/api/api/migration.js +60 -0
- package/api/api/ml.js +2010 -0
- package/api/api/monitoring.js +66 -0
- package/api/api/msearch.js +70 -0
- package/api/api/msearch_template.js +70 -0
- package/api/api/mtermvectors.js +64 -0
- package/api/api/nodes.js +268 -0
- package/api/api/open_point_in_time.js +56 -0
- package/api/api/ping.js +50 -0
- package/api/api/put_script.js +71 -0
- package/api/api/rank_eval.js +61 -0
- package/api/api/reindex.js +56 -0
- package/api/api/reindex_rethrottle.js +60 -0
- package/api/api/render_search_template.js +55 -0
- package/api/api/rollup.js +319 -0
- package/api/api/scripts_painless_execute.js +50 -0
- package/api/api/scroll.js +55 -0
- package/api/api/search.js +64 -0
- package/api/api/search_mvt.js +87 -0
- package/api/api/search_shards.js +55 -0
- package/api/api/search_template.js +70 -0
- package/api/api/searchable_snapshots.js +186 -0
- package/api/api/security.js +1261 -0
- package/api/api/shutdown.js +124 -0
- package/api/api/slm.js +256 -0
- package/api/api/snapshot.js +439 -0
- package/api/api/sql.js +203 -0
- package/api/api/ssl.js +55 -0
- package/api/api/tasks.js +108 -0
- package/api/api/terms_enum.js +56 -0
- package/api/api/termvectors.js +67 -0
- package/api/api/text_structure.js +65 -0
- package/api/api/transform.js +268 -0
- package/api/api/update.js +69 -0
- package/api/api/update_by_query.js +67 -0
- package/api/api/update_by_query_rethrottle.js +60 -0
- package/api/api/watcher.js +333 -0
- package/api/api/xpack.js +76 -0
- package/api/index.js +508 -0
- package/api/new.d.ts +1585 -0
- package/api/requestParams.d.ts +2920 -0
- package/api/types.d.ts +15420 -0
- package/api/utils.js +58 -0
- package/codecov.yml +14 -0
- package/index.d.ts +2991 -0
- package/index.js +349 -0
- package/index.mjs +29 -0
- package/lib/Connection.d.ts +99 -0
- package/lib/Connection.js +392 -0
- package/lib/Helpers.d.ts +124 -0
- package/lib/Helpers.js +770 -0
- package/lib/Serializer.d.ts +30 -0
- package/lib/Serializer.js +94 -0
- package/lib/Transport.d.ts +162 -0
- package/lib/Transport.js +689 -0
- package/lib/errors.d.ts +90 -0
- package/lib/errors.js +159 -0
- package/lib/pool/BaseConnectionPool.js +262 -0
- package/lib/pool/CloudConnectionPool.js +64 -0
- package/lib/pool/ConnectionPool.js +246 -0
- package/lib/pool/index.d.ts +220 -0
- package/lib/pool/index.js +30 -0
- 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
|