@naanlang/naan 1.0.9 → 1.0.11
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/LICENSE.md +1 -1
- package/README.md +1 -1
- package/bin/index.js +2 -2
- package/dist/env_web.js +8 -2
- package/dist/naan.min.js +5 -5
- package/frameworks/browser/sworker.js +17 -17
- package/frameworks/common/common.nlg +1 -1
- package/frameworks/common/utils.nlg +77 -0
- package/frameworks/node/filesystem.nlg +1 -1
- package/frameworks/node/https_request.nlg +19 -2
- package/frameworks/node/node.nlg +1 -0
- package/frameworks/project/projects.nlg +4 -2
- package/frameworks/running/debugnub.nlg +4 -4
- package/frameworks/storage/file_manager.nlg +1 -1
- package/frameworks/storage/psm.nlg +3 -3
- package/lib/browser/env_web.js +14 -8
- package/lib/core/naanlib.js +5 -5
- package/lib/env_nodeworker.js +5 -0
- package/package.json +1 -1
- package/plugins/openSearch/openSearch.nlg +490 -0
- package/plugins/openSearch/os_dynamo.nlg +195 -0
- package/plugins/serviceAws/aws/aws-sdk-node.min.js +1 -1
- package/plugins/serviceAws/aws/aws-sdk.min.js +1 -1
- package/plugins/serviceAws/aws_cloudwatchlogs.nlg +3 -3
- package/plugins/serviceAws/aws_dynamo.nlg +14 -19
- package/plugins/serviceAws/aws_sqs.nlg +5 -1
- package/plugins/serviceAws/dbt_aws.nlg +15 -15
- package/test/test_02_context.nlg +1 -1
- package/test/test_03_jsinterop.nlg +4 -1
- package/test/test_07_lingo.nlg +2 -1
package/lib/env_nodeworker.js
CHANGED
|
@@ -137,10 +137,15 @@ exports.NaanWorkerActivate = function NaanWorkerActivate(cbReady) {
|
|
|
137
137
|
naanlib.js.h = naanlib.js.d; // set host directory path
|
|
138
138
|
if (msg.dirpath)
|
|
139
139
|
naanlib.js.d = msg.dirpath; // set owner directory path
|
|
140
|
+
if (msg.altcmd)
|
|
141
|
+
naanlib.textLine(msg.altcmd);
|
|
142
|
+
/*
|
|
143
|
+
### already started ###
|
|
140
144
|
naanlib.start({
|
|
141
145
|
state: msg.state,
|
|
142
146
|
cmd: msg.altcmd
|
|
143
147
|
});
|
|
148
|
+
*/
|
|
144
149
|
}
|
|
145
150
|
else if (msg.id == "import")
|
|
146
151
|
importScripts(msg.script); // load a JavaScript
|
package/package.json
CHANGED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* openSearch.nlg
|
|
3
|
+
* openSearch
|
|
4
|
+
*
|
|
5
|
+
* OpenSearch client.
|
|
6
|
+
*
|
|
7
|
+
* column positioning: // // !
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2023 by Richard C. Zulch
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* openSearchIndex
|
|
15
|
+
*
|
|
16
|
+
* Access object for API functions on a single index.
|
|
17
|
+
*
|
|
18
|
+
* This client provides a custom datatype facility as a layer on top of OpenSearch. A schema defines
|
|
19
|
+
* the fields in an index, which are compiled into a mapping dictionary. The extra layer allows for
|
|
20
|
+
* adding additional configuration to the field.
|
|
21
|
+
*
|
|
22
|
+
* The inbuilt schema types are:
|
|
23
|
+
* `literal -- keyword
|
|
24
|
+
* `title -- text for a title
|
|
25
|
+
* `hashtags -- set of hashtags
|
|
26
|
+
* `date -- date
|
|
27
|
+
* `boolean -- boolean
|
|
28
|
+
* `currency -- currency with type
|
|
29
|
+
* `text -- block of text
|
|
30
|
+
* `name -- proper name, first-last-middle-etc.
|
|
31
|
+
* `integer -- integer
|
|
32
|
+
* `url -- URL
|
|
33
|
+
*
|
|
34
|
+
* Current options are:
|
|
35
|
+
* {
|
|
36
|
+
* languages: [
|
|
37
|
+
* <language>: {
|
|
38
|
+
* keywords: [<strings>...] -- lowercase keywords not to stem
|
|
39
|
+
* }
|
|
40
|
+
* ...
|
|
41
|
+
* ]
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
closure openSearchIndex(prosc, indexName, ioptions, local osin) {
|
|
47
|
+
global()
|
|
48
|
+
osin = new(object, this)
|
|
49
|
+
if symbol(indexName)
|
|
50
|
+
indexName = indexName.tostring()
|
|
51
|
+
osin.analyzers = { // map names, language to analyzer
|
|
52
|
+
english: "english"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// langKeywords
|
|
56
|
+
//
|
|
57
|
+
// Customize the specified language with an array of keywords that will not be stemmed. This is
|
|
58
|
+
// useful for proper names like Дарья and Таня which otherwise become three-letter words that
|
|
59
|
+
// match too many things. Note that the keywords must be lowercase.
|
|
60
|
+
//
|
|
61
|
+
function langKeywords(language, keywords,
|
|
62
|
+
local keywordFilter, stopFilter, stemFilter, params) {
|
|
63
|
+
keywordFilter = "naan_${language}_keywords"
|
|
64
|
+
stopFilter = "naan_${language}_stop"
|
|
65
|
+
stemFilter = "naan_${language}_stemmer"
|
|
66
|
+
filters = [ "lowercase", keywordFilter, stopFilter, stemFilter ]
|
|
67
|
+
params = {
|
|
68
|
+
settings: {
|
|
69
|
+
analysis: {
|
|
70
|
+
filter: { }
|
|
71
|
+
analyzer: { }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
osin.analyzers[language] = "naan_${language}"
|
|
76
|
+
params.settings.analysis.analyzer[osin.analyzers[language]] = {
|
|
77
|
+
tokenizer: "standard"
|
|
78
|
+
filter: filters
|
|
79
|
+
}
|
|
80
|
+
params.settings.analysis.filter[keywordFilter] = {
|
|
81
|
+
type: "keyword_marker"
|
|
82
|
+
keywords: keywords
|
|
83
|
+
}
|
|
84
|
+
params.settings.analysis.filter[stopFilter] = {
|
|
85
|
+
type: "stop"
|
|
86
|
+
stopwords: "_${language}_"
|
|
87
|
+
}
|
|
88
|
+
params.settings.analysis.filter[stemFilter] = {
|
|
89
|
+
type: "stemmer"
|
|
90
|
+
language: language
|
|
91
|
+
}
|
|
92
|
+
params
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// textRawMF
|
|
96
|
+
//
|
|
97
|
+
// Return a text+raw multifield in the languages configured for our client.
|
|
98
|
+
//
|
|
99
|
+
function textRawMF(local fieldspec, language) {
|
|
100
|
+
fieldspec = {
|
|
101
|
+
type: "text"
|
|
102
|
+
fields: {
|
|
103
|
+
raw: {
|
|
104
|
+
"type": "keyword"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for language in osin.analyzers
|
|
109
|
+
fieldspec.fields[language] = { // add languages
|
|
110
|
+
type: "text"
|
|
111
|
+
analyzer: osin.analyzers[language]
|
|
112
|
+
}
|
|
113
|
+
fieldspec
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// makeMapping
|
|
117
|
+
//
|
|
118
|
+
// Make an OpenSearch mapping dictionary for the specified dictionary of field names/types.
|
|
119
|
+
//
|
|
120
|
+
function makeMapping(fields, local mapping, fname, ftype, osfield) {
|
|
121
|
+
osin.fields = fields
|
|
122
|
+
osin.fieldNames = []
|
|
123
|
+
mapping = { }
|
|
124
|
+
for `(fname, ftype) in fields {
|
|
125
|
+
osin.fieldNames.push(fname)
|
|
126
|
+
osfield = eval({
|
|
127
|
+
//
|
|
128
|
+
// literal
|
|
129
|
+
//
|
|
130
|
+
literal: quote({
|
|
131
|
+
type: "keyword"
|
|
132
|
+
})
|
|
133
|
+
//
|
|
134
|
+
// title
|
|
135
|
+
//
|
|
136
|
+
title: quote(
|
|
137
|
+
textRawMF()
|
|
138
|
+
)
|
|
139
|
+
//
|
|
140
|
+
// hashtags
|
|
141
|
+
//
|
|
142
|
+
hashtags: quote(
|
|
143
|
+
textRawMF()
|
|
144
|
+
)
|
|
145
|
+
//
|
|
146
|
+
// date
|
|
147
|
+
//
|
|
148
|
+
date: quote(
|
|
149
|
+
textRawMF()
|
|
150
|
+
)
|
|
151
|
+
//
|
|
152
|
+
// boolean
|
|
153
|
+
//
|
|
154
|
+
boolean: quote(
|
|
155
|
+
textRawMF()
|
|
156
|
+
)
|
|
157
|
+
//
|
|
158
|
+
// currency
|
|
159
|
+
//
|
|
160
|
+
currency: quote(
|
|
161
|
+
textRawMF()
|
|
162
|
+
)
|
|
163
|
+
//
|
|
164
|
+
// text
|
|
165
|
+
//
|
|
166
|
+
text: quote(
|
|
167
|
+
textRawMF()
|
|
168
|
+
)
|
|
169
|
+
//
|
|
170
|
+
// name
|
|
171
|
+
//
|
|
172
|
+
name: quote(
|
|
173
|
+
textRawMF()
|
|
174
|
+
)
|
|
175
|
+
//
|
|
176
|
+
// integer
|
|
177
|
+
//
|
|
178
|
+
integer: quote(
|
|
179
|
+
textRawMF()
|
|
180
|
+
)
|
|
181
|
+
//
|
|
182
|
+
// url
|
|
183
|
+
//
|
|
184
|
+
url: quote(
|
|
185
|
+
textRawMF()
|
|
186
|
+
)
|
|
187
|
+
}[ftype])
|
|
188
|
+
if !osfield
|
|
189
|
+
debuglog("OpenSearchClient.makeMapping: unknown field type ${ftype}")
|
|
190
|
+
else {
|
|
191
|
+
osfield.meta = {
|
|
192
|
+
type: ftype
|
|
193
|
+
}
|
|
194
|
+
mapping[fname] = osfield
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
mapping
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// bulkDocs
|
|
201
|
+
//
|
|
202
|
+
// Create a bulk docs request body with the specified operation. Returns a standard
|
|
203
|
+
// `(error, string) result tuple.
|
|
204
|
+
//
|
|
205
|
+
function bulkDocs(docs, action, local items, doc, index, operation, error, data) {
|
|
206
|
+
items = []
|
|
207
|
+
for `(doc, index) in docs {
|
|
208
|
+
operation = { }
|
|
209
|
+
operation[action] = { _index: indexName }
|
|
210
|
+
`(error, data) = JsonStringify(operation)
|
|
211
|
+
if !error
|
|
212
|
+
`(error, doc) = JsonStringify(doc)
|
|
213
|
+
if error {
|
|
214
|
+
error = Error("openSearchIndex.write: doc[${index}] unwritable:", error)
|
|
215
|
+
ErrorDebuglog(error)
|
|
216
|
+
return (list(error))
|
|
217
|
+
}
|
|
218
|
+
items.push(data)
|
|
219
|
+
items.push(doc)
|
|
220
|
+
}
|
|
221
|
+
list(false, items.join("\n").concat("\n"))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// schema
|
|
225
|
+
//
|
|
226
|
+
// Get the schema for the named index.
|
|
227
|
+
//
|
|
228
|
+
osin.schema = closure schema(local error, data, fields, key, osfield) {
|
|
229
|
+
`(error, data) = prosc.request(indexName.concat("/_mapping"), {
|
|
230
|
+
method: "GET"
|
|
231
|
+
})
|
|
232
|
+
if error
|
|
233
|
+
return (list(error))
|
|
234
|
+
osin.fieldNames = []
|
|
235
|
+
fields = {}
|
|
236
|
+
for `(key, osfield) in data[indexName].mappings.properties {
|
|
237
|
+
osin.fieldNames.push(key)
|
|
238
|
+
fields[key] = osfield.meta.type
|
|
239
|
+
}
|
|
240
|
+
list(false, fields)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// createIndex
|
|
244
|
+
//
|
|
245
|
+
// Create the index with the specified field types.
|
|
246
|
+
//
|
|
247
|
+
osin.createIndex = closure createIndex(fields, local params, language, opts) {
|
|
248
|
+
params = { }
|
|
249
|
+
if ioptions.languages {
|
|
250
|
+
for `(language, opts) in ioptions.languages
|
|
251
|
+
if opts.keywords
|
|
252
|
+
params = merge(params, langKeywords(language, opts.keywords))
|
|
253
|
+
else
|
|
254
|
+
osin.analyzers[language] = language
|
|
255
|
+
}
|
|
256
|
+
params = merge(params, {
|
|
257
|
+
mappings: {
|
|
258
|
+
properties: makeMapping(fields)
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
// printline("OpenSearchClient.create:\n${Dialect.print(params)}")
|
|
262
|
+
prosc.request(indexName, {
|
|
263
|
+
method: "PUT"
|
|
264
|
+
putdata: params
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// deleteIndex
|
|
269
|
+
//
|
|
270
|
+
// Delete the index.
|
|
271
|
+
//
|
|
272
|
+
osin.deleteIndex = closure deleteIndex() {
|
|
273
|
+
prosc.request(indexName, {
|
|
274
|
+
method: "DELETE"
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// write
|
|
279
|
+
//
|
|
280
|
+
// Write a document or an array of documents to our index. Returns a standard `(error, data)
|
|
281
|
+
// tuple where the data is the _id (or [_ids]) when there was no error.
|
|
282
|
+
//
|
|
283
|
+
osin.write = closure write(doc, local error, docs, data) {
|
|
284
|
+
if array(doc) {
|
|
285
|
+
`(error, docs) = bulkDocs(doc, "create")
|
|
286
|
+
if docs
|
|
287
|
+
`(error, data) = prosc.request(indexName.concat("/_bulk"), {
|
|
288
|
+
method: "POST"
|
|
289
|
+
putdata: docs
|
|
290
|
+
contentType: "application/json"
|
|
291
|
+
})
|
|
292
|
+
if data
|
|
293
|
+
data = data.items.map(function(item) {
|
|
294
|
+
item.create._id
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
`(error, data) = write([doc])
|
|
299
|
+
if data
|
|
300
|
+
data = data.0
|
|
301
|
+
}
|
|
302
|
+
list(error, data)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// update
|
|
306
|
+
//
|
|
307
|
+
// Update a document in our index.
|
|
308
|
+
//
|
|
309
|
+
osin.update = closure update(doc, _id) {
|
|
310
|
+
prosc.request(indexName.concat("/_update/${_id}"), {
|
|
311
|
+
method: "POST"
|
|
312
|
+
putdata: {
|
|
313
|
+
doc: doc
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// delete
|
|
319
|
+
//
|
|
320
|
+
// Remove a document from our index.
|
|
321
|
+
//
|
|
322
|
+
osin.delete = closure delete(_id) {
|
|
323
|
+
prosc.request(indexName.concat("/_doc/${_id}"), {
|
|
324
|
+
method: "DELETE"
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// search
|
|
329
|
+
//
|
|
330
|
+
// Simple search on our index, default to all fields. Options are:
|
|
331
|
+
// {
|
|
332
|
+
// where: <array> -- fields to search, defaulting to * (all)
|
|
333
|
+
// qopts: <dictionary> -- OpenSearch query options
|
|
334
|
+
// }
|
|
335
|
+
//
|
|
336
|
+
osin.search = closure search(what, soptions, local where, params) {
|
|
337
|
+
where = soptions.where
|
|
338
|
+
if !array(where)
|
|
339
|
+
where = ["*"]
|
|
340
|
+
params = {
|
|
341
|
+
query: {
|
|
342
|
+
multi_match: {
|
|
343
|
+
query: what
|
|
344
|
+
fields: where
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if dictionary(soptions.qopts)
|
|
349
|
+
params.query.multi_match = merge(params.query.multi_match, soptions.qopts)
|
|
350
|
+
// printline("OpenSearchClient.search:\n${Dialect.print(params)}")
|
|
351
|
+
prosc.request(indexName.concat("/_search"), {
|
|
352
|
+
method: "POST"
|
|
353
|
+
putdata: params
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// finis
|
|
358
|
+
|
|
359
|
+
osin
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
/*
|
|
364
|
+
* OpenSearchClient
|
|
365
|
+
*
|
|
366
|
+
* An OpenSearchClient client as a wrapper for the REST API. Options are:
|
|
367
|
+
* {
|
|
368
|
+
* url: <string> -- host URL with port
|
|
369
|
+
* cacert: <data> -- CA pem for host
|
|
370
|
+
* auth: <user>:<password> -- basic authentication
|
|
371
|
+
* debug: <boolean> -- debuglog additional error information
|
|
372
|
+
* languages: [
|
|
373
|
+
* <language>: {
|
|
374
|
+
* keywords: [<strings>...] -- lowercase keywords not to stem
|
|
375
|
+
* }
|
|
376
|
+
* ...
|
|
377
|
+
* ]
|
|
378
|
+
* }
|
|
379
|
+
*
|
|
380
|
+
*/
|
|
381
|
+
|
|
382
|
+
closure OpenSearchClient(goptions, local oscl, prosc) {
|
|
383
|
+
global(JSpath)
|
|
384
|
+
oscl = new(object, this)
|
|
385
|
+
prosc = new(object, this)
|
|
386
|
+
prosc.oscl = oscl
|
|
387
|
+
goptions = new(goptions)
|
|
388
|
+
if goptions.url.slice(-1) != "/"
|
|
389
|
+
goptions.url = goptions.url.concat("/")
|
|
390
|
+
|
|
391
|
+
// request
|
|
392
|
+
//
|
|
393
|
+
// Perform a request on the host as specified.
|
|
394
|
+
//
|
|
395
|
+
prosc.request = closure request(path, roptions, local url, error, data) {
|
|
396
|
+
if roptions
|
|
397
|
+
roptions = new(roptions)
|
|
398
|
+
else
|
|
399
|
+
roptions = { }
|
|
400
|
+
roptions.debug = goptions.debug
|
|
401
|
+
roptions.auth = goptions.auth
|
|
402
|
+
roptions.cacert = goptions.cacert
|
|
403
|
+
roptions.allowSelfSigned = true
|
|
404
|
+
url = goptions.url.concat(path)
|
|
405
|
+
`(error, data) = HttpsApiRequest(url, roptions)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ping
|
|
409
|
+
//
|
|
410
|
+
// Ping the cluster and get basic ID and version information.
|
|
411
|
+
//
|
|
412
|
+
oscl.ping = closure ping() {
|
|
413
|
+
request("")
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// index
|
|
417
|
+
//
|
|
418
|
+
// Return an object for the named index, which may not exist yet.
|
|
419
|
+
//
|
|
420
|
+
oscl.index = closure index(indexName) {
|
|
421
|
+
openSearchIndex(prosc, indexName, goptions)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// search
|
|
425
|
+
//
|
|
426
|
+
// Simple search on all indexes and fields. Options are:
|
|
427
|
+
// {
|
|
428
|
+
// indexes: <array> -- indexes to search, default all
|
|
429
|
+
// size: <integer> -- results to return, default 10
|
|
430
|
+
// query: <dictionary> -- OpenSearch query, default multi_match
|
|
431
|
+
// qopts: <dictionary> -- OpenSearch query options for multi_match
|
|
432
|
+
// filter: <dictionary> - OpenSearch query filter
|
|
433
|
+
// }
|
|
434
|
+
//
|
|
435
|
+
oscl.search = closure search(what, soptions, local params, query, path) {
|
|
436
|
+
params = { }
|
|
437
|
+
query = {
|
|
438
|
+
multi_match: {
|
|
439
|
+
query: what
|
|
440
|
+
fields: "*"
|
|
441
|
+
fuzziness: "auto"
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if soptions.size
|
|
445
|
+
params.size = soptions.size // results to return
|
|
446
|
+
if soptions.query
|
|
447
|
+
query = soptions.query
|
|
448
|
+
if dictionary(soptions.qopts)
|
|
449
|
+
query.multi_match = merge(query.multi_match, soptions.qopts)
|
|
450
|
+
if dictionary(soptions.filter) {
|
|
451
|
+
query = {
|
|
452
|
+
bool: {
|
|
453
|
+
must: query
|
|
454
|
+
filter: soptions.filter
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
params.query = query
|
|
459
|
+
path = "_search"
|
|
460
|
+
if soptions.indexes
|
|
461
|
+
path = soptions.indexes.join(",").concat("/", path)
|
|
462
|
+
prosc.request(path, {
|
|
463
|
+
method: "POST"
|
|
464
|
+
putdata: params
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// finis
|
|
469
|
+
|
|
470
|
+
oscl
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
/*
|
|
475
|
+
* opseInit
|
|
476
|
+
*
|
|
477
|
+
* Initialize the module.
|
|
478
|
+
*
|
|
479
|
+
*/
|
|
480
|
+
|
|
481
|
+
function opseInit(local manifest) {
|
|
482
|
+
manifest = `(openSearchIndex, OpenSearchClient, opseInit)
|
|
483
|
+
|
|
484
|
+
Naan.module.build(module.id, "openSearch", function(modobj, compobj) {
|
|
485
|
+
compobj.manifest = manifest
|
|
486
|
+
require("frameworks/common").LiveImport()
|
|
487
|
+
letimport(require("frameworks/node/https_request.nlg"))
|
|
488
|
+
modobj.exports.OpenSearchClient = OpenSearchClient
|
|
489
|
+
})
|
|
490
|
+
} ();
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* os_dynamo.nlg
|
|
3
|
+
* openSearch
|
|
4
|
+
*
|
|
5
|
+
* OpenSearch-DynamoDB integration.
|
|
6
|
+
*
|
|
7
|
+
* column positioning: // // !
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2023 by Richard C. Zulch
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* OSDynIndexer
|
|
15
|
+
*
|
|
16
|
+
* Index a dynamoDB database into an OpenSearch index. The index should be created before this is
|
|
17
|
+
* called, with the proper fields and types already configured. The database is the source of truth,
|
|
18
|
+
* so the index can be recreated from scratch if necessary. Source _ids are assigned by OpenSearch
|
|
19
|
+
* and therefore this creates all new records. It is not a sync tool.
|
|
20
|
+
* This operates by scanning the database for a page of records, then starting the indexing
|
|
21
|
+
* operation in the background, scanning another batch, and then waiting for the previous background
|
|
22
|
+
* index to complete. Each time it has both a set of database records and the ids from indexing, it
|
|
23
|
+
* can optionally update the database records with the newly-assigned ID. There is never more than
|
|
24
|
+
* one database or index operation proceeding at the same time, but they overlap.
|
|
25
|
+
*
|
|
26
|
+
* Options:
|
|
27
|
+
* {
|
|
28
|
+
* chunkSize: <integer> // size of each chunk of transfer
|
|
29
|
+
* dbFilter: <array-or-dict> // DynamoDB KeyExpression(s)
|
|
30
|
+
* converter: <procedure> // convert DB record to index info
|
|
31
|
+
* progress: <procedure> // called for each batch completed
|
|
32
|
+
* writeID: <string> // write OpenSearch _id to database with key writeID
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* The converter protocol is:
|
|
36
|
+
* converter(dbRecord) // return _source doc for database record
|
|
37
|
+
* The progress protocol is:
|
|
38
|
+
* progress(completed) // integer number of records completed
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
closure OSDynIndexer(index, table, options,
|
|
43
|
+
local dbOptions, scanPending, indexPending, updatePending, output, error, records, data, ids) {
|
|
44
|
+
if !options.converter
|
|
45
|
+
return (Error("OSDynIndexer: options.converter is required"))
|
|
46
|
+
dbOptions = {
|
|
47
|
+
filter: options.filter
|
|
48
|
+
limit: options.chunkSize || 100
|
|
49
|
+
}
|
|
50
|
+
//
|
|
51
|
+
// Scan records in the database, returning a nonce signaled with records array
|
|
52
|
+
//
|
|
53
|
+
closure scanRecords(local pending) {
|
|
54
|
+
if !output
|
|
55
|
+
output = [] // just starting to scan records
|
|
56
|
+
else if !dbOptions.paging
|
|
57
|
+
return (false) // scanned all records
|
|
58
|
+
pending = new(nonce)
|
|
59
|
+
future(function() {
|
|
60
|
+
pending.signal(table.scanRecords(dbOptions))
|
|
61
|
+
},0)
|
|
62
|
+
pending
|
|
63
|
+
}
|
|
64
|
+
//
|
|
65
|
+
// Update ids into the database, returning a nonce signaled with error array.
|
|
66
|
+
//
|
|
67
|
+
closure updateRecordIDs(docs, ids, writeID) {
|
|
68
|
+
asyncArray(docs, 10, function(entry, index, cancel, local error) {
|
|
69
|
+
ignore(error)
|
|
70
|
+
`(error) = table.updateRecord(
|
|
71
|
+
entry[table.hashKey],
|
|
72
|
+
entry[table.rangeKey], {
|
|
73
|
+
set: [list(writeID, ids[index])]
|
|
74
|
+
})
|
|
75
|
+
error
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
//
|
|
79
|
+
// Batch index documents into OpenSearch, returning a nonce signaled with ids array.
|
|
80
|
+
//
|
|
81
|
+
closure batchIndex(records, local pending, docs) {
|
|
82
|
+
pending = new(nonce)
|
|
83
|
+
docs = records.map(function(record) {
|
|
84
|
+
options.converter(record)
|
|
85
|
+
})
|
|
86
|
+
future(function(local error, ids) {
|
|
87
|
+
`(error, ids) = index.write(docs)
|
|
88
|
+
pending.signal(list(error, records, ids))
|
|
89
|
+
}, 0)
|
|
90
|
+
pending
|
|
91
|
+
}
|
|
92
|
+
//
|
|
93
|
+
// execute
|
|
94
|
+
//
|
|
95
|
+
// loop operation
|
|
96
|
+
// 1 scan(a)start
|
|
97
|
+
// 2 scan(a)end,index(a)start,scan(b)start,index(a)end,update(a)start
|
|
98
|
+
// 3 scan(b)end,index(b)start,scan(c)start,index(b)end,update(a)end,update(b)start
|
|
99
|
+
// 4 scan(c)end,index(c)start,scan(d)start,index(c)end,update(b)end,update(c)start
|
|
100
|
+
//
|
|
101
|
+
// |-----------|-----------| |-----------|-----------|
|
|
102
|
+
// | 1.scan(a) | 3.scan(b) | | 6.scan(c) | 9.scan(d) |
|
|
103
|
+
// |-----------|-----------| |-----------|-----------|
|
|
104
|
+
// |------------|------------| |-------------|
|
|
105
|
+
// | 2.index(a) | 5.index(b) | | 8.index(c) |
|
|
106
|
+
// |------------|------------| |-------------|
|
|
107
|
+
// |---------------|---------------| |----------------|
|
|
108
|
+
// | 4.update(a,a) | 7.update(b,b) | | 10.update(c,c) |
|
|
109
|
+
// |---------------|---------------| |----------------|
|
|
110
|
+
//
|
|
111
|
+
// As of 2023-04-12 with Yandex Cloud and a minimal OpenSearch cluster, The indexing operation is
|
|
112
|
+
// the long pole in the tent--taking the longest time to process. However the timing has a lot of
|
|
113
|
+
// variation, e.g. the first DynamoDB update being longer but then very fast after that.
|
|
114
|
+
//
|
|
115
|
+
loop {
|
|
116
|
+
//
|
|
117
|
+
// finish scanning
|
|
118
|
+
//
|
|
119
|
+
if scanPending {
|
|
120
|
+
`(error, records) = scanPending.wait()
|
|
121
|
+
scanPending = false
|
|
122
|
+
if error
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
//
|
|
126
|
+
// start indexing
|
|
127
|
+
//
|
|
128
|
+
if records.length > 0
|
|
129
|
+
indexPending = batchIndex(records)
|
|
130
|
+
records = false
|
|
131
|
+
//
|
|
132
|
+
// start scanning
|
|
133
|
+
//
|
|
134
|
+
if !output || output.length <= Number.MAX_SAFE_INTEGER // for future limit option
|
|
135
|
+
scanPending = scanRecords()
|
|
136
|
+
//
|
|
137
|
+
// finish indexing
|
|
138
|
+
//
|
|
139
|
+
if indexPending {
|
|
140
|
+
`(error, records, ids) = indexPending.wait()
|
|
141
|
+
indexPending = false
|
|
142
|
+
if error
|
|
143
|
+
break
|
|
144
|
+
output = output.concat(ids)
|
|
145
|
+
}
|
|
146
|
+
//
|
|
147
|
+
// finish updating
|
|
148
|
+
//
|
|
149
|
+
if updatePending {
|
|
150
|
+
`(data) = updatePending.wait()
|
|
151
|
+
updatePending = false
|
|
152
|
+
error = data.find(function(item) { item })
|
|
153
|
+
if error
|
|
154
|
+
break
|
|
155
|
+
options.progress(output.length) // progress with update
|
|
156
|
+
}
|
|
157
|
+
//
|
|
158
|
+
// start updating
|
|
159
|
+
//
|
|
160
|
+
if options.writeID {
|
|
161
|
+
if records && ids
|
|
162
|
+
updatePending = updateRecordIDs(records, ids, options.writeID)
|
|
163
|
+
}
|
|
164
|
+
else
|
|
165
|
+
options.progress(output.length || 0) // progress without update
|
|
166
|
+
records = ids = false
|
|
167
|
+
//
|
|
168
|
+
// check for termination
|
|
169
|
+
//
|
|
170
|
+
if !scanPending && !indexPending && !updatePending
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
if error
|
|
174
|
+
list(error)
|
|
175
|
+
else
|
|
176
|
+
list(false, output)
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
* osdyInit
|
|
182
|
+
*
|
|
183
|
+
* Initialize the module.
|
|
184
|
+
*
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
function osdyInit(local manifest) {
|
|
188
|
+
manifest = `(OSDynIndexer, osdyInit)
|
|
189
|
+
|
|
190
|
+
Naan.module.build(module.id, "os_dynamo", function(modobj, compobj) {
|
|
191
|
+
compobj.manifest = manifest
|
|
192
|
+
require("./openSearch.nlg")
|
|
193
|
+
modobj.exports.OSDynIndexer = OSDynIndexer
|
|
194
|
+
})
|
|
195
|
+
} ();
|