@backstage/plugin-search-backend-node 1.3.3-next.0 → 1.3.3-next.2
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/CHANGELOG.md +24 -0
- package/alpha/package.json +1 -1
- package/dist/IndexBuilder.cjs.js +123 -0
- package/dist/IndexBuilder.cjs.js.map +1 -0
- package/dist/Scheduler.cjs.js +62 -0
- package/dist/Scheduler.cjs.js.map +1 -0
- package/dist/collators/NewlineDelimitedJsonCollatorFactory.cjs.js +61 -0
- package/dist/collators/NewlineDelimitedJsonCollatorFactory.cjs.js.map +1 -0
- package/dist/engines/LunrSearchEngine.cjs.js +213 -0
- package/dist/engines/LunrSearchEngine.cjs.js.map +1 -0
- package/dist/engines/LunrSearchEngineIndexer.cjs.js +50 -0
- package/dist/engines/LunrSearchEngineIndexer.cjs.js.map +1 -0
- package/dist/errors.cjs.js +19 -0
- package/dist/errors.cjs.js.map +1 -0
- package/dist/index.cjs.js +19 -733
- package/dist/index.cjs.js.map +1 -1
- package/dist/indexing/BatchSearchEngineIndexer.cjs.js +65 -0
- package/dist/indexing/BatchSearchEngineIndexer.cjs.js.map +1 -0
- package/dist/indexing/DecoratorBase.cjs.js +64 -0
- package/dist/indexing/DecoratorBase.cjs.js.map +1 -0
- package/dist/test-utils/TestPipeline.cjs.js +130 -0
- package/dist/test-utils/TestPipeline.cjs.js.map +1 -0
- package/package.json +9 -9
package/dist/index.cjs.js
CHANGED
|
@@ -1,736 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.isRunning = false;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Adds each task and interval to the schedule.
|
|
26
|
-
* When running the tasks, the scheduler waits at least for the time specified
|
|
27
|
-
* in the interval once the task was completed, before running it again.
|
|
28
|
-
*/
|
|
29
|
-
addToSchedule(options) {
|
|
30
|
-
const { id, task, scheduledRunner } = options;
|
|
31
|
-
if (this.isRunning) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
"Cannot add task to schedule that has already been started."
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
if (this.schedule[id]) {
|
|
37
|
-
throw new Error(`Task with id ${id} already exists.`);
|
|
38
|
-
}
|
|
39
|
-
this.schedule[id] = { task, scheduledRunner };
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Starts the scheduling process for each task
|
|
43
|
-
*/
|
|
44
|
-
start() {
|
|
45
|
-
this.logger.info("Starting all scheduled search tasks.");
|
|
46
|
-
this.isRunning = true;
|
|
47
|
-
Object.keys(this.schedule).forEach((id) => {
|
|
48
|
-
const abortController = new AbortController();
|
|
49
|
-
this.abortControllers.push(abortController);
|
|
50
|
-
const { task, scheduledRunner } = this.schedule[id];
|
|
51
|
-
scheduledRunner.run({
|
|
52
|
-
id,
|
|
53
|
-
fn: task,
|
|
54
|
-
signal: abortController.signal
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Stop all scheduled tasks.
|
|
60
|
-
*/
|
|
61
|
-
stop() {
|
|
62
|
-
this.logger.info("Stopping all scheduled search tasks.");
|
|
63
|
-
for (const abortController of this.abortControllers) {
|
|
64
|
-
abortController.abort();
|
|
65
|
-
}
|
|
66
|
-
this.abortControllers = [];
|
|
67
|
-
this.isRunning = false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
class IndexBuilder {
|
|
72
|
-
collators;
|
|
73
|
-
decorators;
|
|
74
|
-
documentTypes;
|
|
75
|
-
searchEngine;
|
|
76
|
-
logger;
|
|
77
|
-
constructor(options) {
|
|
78
|
-
this.collators = {};
|
|
79
|
-
this.decorators = {};
|
|
80
|
-
this.documentTypes = {};
|
|
81
|
-
this.logger = options.logger;
|
|
82
|
-
this.searchEngine = options.searchEngine;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Responsible for returning the registered search engine.
|
|
86
|
-
*/
|
|
87
|
-
getSearchEngine() {
|
|
88
|
-
return this.searchEngine;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Responsible for returning the registered document types.
|
|
92
|
-
*/
|
|
93
|
-
getDocumentTypes() {
|
|
94
|
-
return this.documentTypes;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Makes the index builder aware of a collator that should be executed at the
|
|
98
|
-
* given refresh interval.
|
|
99
|
-
*/
|
|
100
|
-
addCollator(options) {
|
|
101
|
-
const { factory, schedule } = options;
|
|
102
|
-
this.logger.info(
|
|
103
|
-
`Added ${factory.constructor.name} collator factory for type ${factory.type}`
|
|
104
|
-
);
|
|
105
|
-
this.collators[factory.type] = {
|
|
106
|
-
factory,
|
|
107
|
-
schedule
|
|
108
|
-
};
|
|
109
|
-
this.documentTypes[factory.type] = {
|
|
110
|
-
visibilityPermission: factory.visibilityPermission
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Makes the index builder aware of a decorator. If no types are provided on
|
|
115
|
-
* the decorator, it will be applied to documents from all known collators,
|
|
116
|
-
* otherwise it will only be applied to documents of the given types.
|
|
117
|
-
*/
|
|
118
|
-
addDecorator(options) {
|
|
119
|
-
const { factory } = options;
|
|
120
|
-
const types = factory.types || ["*"];
|
|
121
|
-
this.logger.info(
|
|
122
|
-
`Added decorator ${factory.constructor.name} to types ${types.join(
|
|
123
|
-
", "
|
|
124
|
-
)}`
|
|
125
|
-
);
|
|
126
|
-
types.forEach((type) => {
|
|
127
|
-
if (this.decorators.hasOwnProperty(type)) {
|
|
128
|
-
this.decorators[type].push(factory);
|
|
129
|
-
} else {
|
|
130
|
-
this.decorators[type] = [factory];
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Compiles collators and decorators into tasks, which are added to a
|
|
136
|
-
* scheduler returned to the caller.
|
|
137
|
-
*/
|
|
138
|
-
async build() {
|
|
139
|
-
const scheduler = new Scheduler({
|
|
140
|
-
logger: this.logger
|
|
141
|
-
});
|
|
142
|
-
Object.keys(this.collators).forEach((type) => {
|
|
143
|
-
const taskLogger = this.logger.child({ documentType: type });
|
|
144
|
-
scheduler.addToSchedule({
|
|
145
|
-
id: `search_index_${type.replace("-", "_").toLocaleLowerCase("en-US")}`,
|
|
146
|
-
scheduledRunner: this.collators[type].schedule,
|
|
147
|
-
task: async () => {
|
|
148
|
-
const collator = await this.collators[type].factory.getCollator();
|
|
149
|
-
taskLogger.info(
|
|
150
|
-
`Collating documents for ${type} via ${this.collators[type].factory.constructor.name}`
|
|
151
|
-
);
|
|
152
|
-
const decorators = await Promise.all(
|
|
153
|
-
(this.decorators["*"] || []).concat(this.decorators[type] || []).map(async (factory) => {
|
|
154
|
-
const decorator = await factory.getDecorator();
|
|
155
|
-
taskLogger.info(
|
|
156
|
-
`Attached decorator via ${factory.constructor.name} to ${type} index pipeline.`
|
|
157
|
-
);
|
|
158
|
-
return decorator;
|
|
159
|
-
})
|
|
160
|
-
);
|
|
161
|
-
const indexer = await this.searchEngine.getIndexer(type);
|
|
162
|
-
return new Promise((resolve, reject) => {
|
|
163
|
-
stream.pipeline(
|
|
164
|
-
[collator, ...decorators, indexer],
|
|
165
|
-
(error) => {
|
|
166
|
-
if (error) {
|
|
167
|
-
taskLogger.error(
|
|
168
|
-
`Collating documents for ${type} failed: ${error}`
|
|
169
|
-
);
|
|
170
|
-
reject(error);
|
|
171
|
-
} else {
|
|
172
|
-
taskLogger.info(`Collating documents for ${type} succeeded`);
|
|
173
|
-
resolve();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
return {
|
|
182
|
-
scheduler
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
class NewlineDelimitedJsonCollatorFactory {
|
|
188
|
-
constructor(type, searchPattern, reader, logger, visibilityPermission) {
|
|
189
|
-
this.searchPattern = searchPattern;
|
|
190
|
-
this.reader = reader;
|
|
191
|
-
this.logger = logger;
|
|
192
|
-
this.type = type;
|
|
193
|
-
this.visibilityPermission = visibilityPermission;
|
|
194
|
-
}
|
|
195
|
-
type;
|
|
196
|
-
visibilityPermission;
|
|
197
|
-
/**
|
|
198
|
-
* Returns a NewlineDelimitedJsonCollatorFactory instance from configuration
|
|
199
|
-
* and a set of options.
|
|
200
|
-
*/
|
|
201
|
-
static fromConfig(_config, options) {
|
|
202
|
-
return new NewlineDelimitedJsonCollatorFactory(
|
|
203
|
-
options.type,
|
|
204
|
-
options.searchPattern,
|
|
205
|
-
options.reader,
|
|
206
|
-
options.logger.child({ documentType: options.type }),
|
|
207
|
-
options.visibilityPermission
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Returns the "latest" URL for the given search pattern (e.g. the one at the
|
|
212
|
-
* end of the list, sorted alphabetically).
|
|
213
|
-
*/
|
|
214
|
-
async lastUrl() {
|
|
215
|
-
try {
|
|
216
|
-
this.logger.info(
|
|
217
|
-
`Attempting to find latest .ndjson matching ${this.searchPattern}`
|
|
218
|
-
);
|
|
219
|
-
const { files } = await this.reader.search(this.searchPattern);
|
|
220
|
-
const candidates = files.filter((file) => file.url.endsWith(".ndjson")).sort((a, b) => a.url.localeCompare(b.url)).reverse();
|
|
221
|
-
return candidates[0]?.url;
|
|
222
|
-
} catch (e) {
|
|
223
|
-
this.logger.error(`Could not search for ${this.searchPattern}`, e);
|
|
224
|
-
throw e;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
async getCollator() {
|
|
228
|
-
const lastUrl = await this.lastUrl();
|
|
229
|
-
if (!lastUrl) {
|
|
230
|
-
const noMatchingFile = `Could not find an .ndjson file matching ${this.searchPattern}`;
|
|
231
|
-
this.logger.error(noMatchingFile);
|
|
232
|
-
throw new Error(noMatchingFile);
|
|
233
|
-
} else {
|
|
234
|
-
this.logger.info(`Using latest .ndjson file ${lastUrl}`);
|
|
235
|
-
}
|
|
236
|
-
const readerResponse = await this.reader.readUrl(lastUrl);
|
|
237
|
-
const stream = readerResponse.stream();
|
|
238
|
-
return stream.pipe(ndjson.parse());
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
class MissingIndexError extends Error {
|
|
243
|
-
/**
|
|
244
|
-
* An inner error that caused this error to be thrown, if any.
|
|
245
|
-
*/
|
|
246
|
-
cause;
|
|
247
|
-
constructor(message, cause) {
|
|
248
|
-
super(message);
|
|
249
|
-
Error.captureStackTrace?.(this, this.constructor);
|
|
250
|
-
this.name = this.constructor.name;
|
|
251
|
-
this.cause = errors.isError(cause) ? cause : void 0;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
class BatchSearchEngineIndexer extends stream.Writable {
|
|
256
|
-
batchSize;
|
|
257
|
-
currentBatch = [];
|
|
258
|
-
constructor(options) {
|
|
259
|
-
super({ objectMode: true });
|
|
260
|
-
this.batchSize = options.batchSize;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Encapsulates initialization logic.
|
|
264
|
-
* @internal
|
|
265
|
-
*/
|
|
266
|
-
async _construct(done) {
|
|
267
|
-
try {
|
|
268
|
-
await this.initialize();
|
|
269
|
-
done();
|
|
270
|
-
} catch (e) {
|
|
271
|
-
errors.assertError(e);
|
|
272
|
-
done(e);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Encapsulates batch stream write logic.
|
|
277
|
-
* @internal
|
|
278
|
-
*/
|
|
279
|
-
async _write(doc, _e, done) {
|
|
280
|
-
this.currentBatch.push(doc);
|
|
281
|
-
if (this.currentBatch.length < this.batchSize) {
|
|
282
|
-
done();
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
try {
|
|
286
|
-
await this.index(this.currentBatch);
|
|
287
|
-
this.currentBatch = [];
|
|
288
|
-
done();
|
|
289
|
-
} catch (e) {
|
|
290
|
-
errors.assertError(e);
|
|
291
|
-
done(e);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Encapsulates finalization and final error handling logic.
|
|
296
|
-
* @internal
|
|
297
|
-
*/
|
|
298
|
-
async _final(done) {
|
|
299
|
-
try {
|
|
300
|
-
if (this.currentBatch.length) {
|
|
301
|
-
await this.index(this.currentBatch);
|
|
302
|
-
this.currentBatch = [];
|
|
303
|
-
}
|
|
304
|
-
await this.finalize();
|
|
305
|
-
done();
|
|
306
|
-
} catch (e) {
|
|
307
|
-
errors.assertError(e);
|
|
308
|
-
done(e);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
class DecoratorBase extends stream.Transform {
|
|
314
|
-
constructor() {
|
|
315
|
-
super({ objectMode: true });
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Encapsulates initialization logic.
|
|
319
|
-
* @internal
|
|
320
|
-
*/
|
|
321
|
-
async _construct(done) {
|
|
322
|
-
try {
|
|
323
|
-
await this.initialize();
|
|
324
|
-
done();
|
|
325
|
-
} catch (e) {
|
|
326
|
-
errors.assertError(e);
|
|
327
|
-
done(e);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Encapsulates simple transform stream logic.
|
|
332
|
-
* @internal
|
|
333
|
-
*/
|
|
334
|
-
async _transform(document, _, done) {
|
|
335
|
-
try {
|
|
336
|
-
const decorated = await this.decorate(document);
|
|
337
|
-
if (decorated === void 0) {
|
|
338
|
-
done();
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (Array.isArray(decorated)) {
|
|
342
|
-
decorated.forEach((doc) => {
|
|
343
|
-
this.push(doc);
|
|
344
|
-
});
|
|
345
|
-
done();
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
this.push(decorated);
|
|
349
|
-
done();
|
|
350
|
-
} catch (e) {
|
|
351
|
-
errors.assertError(e);
|
|
352
|
-
done(e);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Encapsulates finalization and final error handling logic.
|
|
357
|
-
* @internal
|
|
358
|
-
*/
|
|
359
|
-
async _final(done) {
|
|
360
|
-
try {
|
|
361
|
-
await this.finalize();
|
|
362
|
-
done();
|
|
363
|
-
} catch (e) {
|
|
364
|
-
errors.assertError(e);
|
|
365
|
-
done(e);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
|
|
371
|
-
schemaInitialized = false;
|
|
372
|
-
builder;
|
|
373
|
-
docStore = {};
|
|
374
|
-
constructor() {
|
|
375
|
-
super({ batchSize: 1e3 });
|
|
376
|
-
this.builder = new lunr__default.default.Builder();
|
|
377
|
-
this.builder.pipeline.add(lunr__default.default.trimmer, lunr__default.default.stopWordFilter, lunr__default.default.stemmer);
|
|
378
|
-
this.builder.searchPipeline.add(lunr__default.default.stemmer);
|
|
379
|
-
this.builder.metadataWhitelist = ["position"];
|
|
380
|
-
}
|
|
381
|
-
// No async initialization required.
|
|
382
|
-
async initialize() {
|
|
383
|
-
}
|
|
384
|
-
async finalize() {
|
|
385
|
-
}
|
|
386
|
-
async index(documents) {
|
|
387
|
-
if (!this.schemaInitialized) {
|
|
388
|
-
Object.keys(documents[0]).forEach((field) => {
|
|
389
|
-
this.builder.field(field);
|
|
390
|
-
});
|
|
391
|
-
this.builder.ref("location");
|
|
392
|
-
this.schemaInitialized = true;
|
|
393
|
-
}
|
|
394
|
-
documents.forEach((document) => {
|
|
395
|
-
this.builder.add(document);
|
|
396
|
-
this.docStore[document.location] = document;
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
buildIndex() {
|
|
400
|
-
return this.builder.build();
|
|
401
|
-
}
|
|
402
|
-
getDocumentStore() {
|
|
403
|
-
return this.docStore;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
class LunrSearchEngine {
|
|
408
|
-
lunrIndices = {};
|
|
409
|
-
docStore;
|
|
410
|
-
logger;
|
|
411
|
-
highlightPreTag;
|
|
412
|
-
highlightPostTag;
|
|
413
|
-
constructor(options) {
|
|
414
|
-
this.logger = options.logger;
|
|
415
|
-
this.docStore = {};
|
|
416
|
-
const uuidTag = uuid.v4();
|
|
417
|
-
this.highlightPreTag = `<${uuidTag}>`;
|
|
418
|
-
this.highlightPostTag = `</${uuidTag}>`;
|
|
419
|
-
}
|
|
420
|
-
translator = ({
|
|
421
|
-
term,
|
|
422
|
-
filters,
|
|
423
|
-
types,
|
|
424
|
-
pageLimit
|
|
425
|
-
}) => {
|
|
426
|
-
const pageSize = pageLimit || 25;
|
|
427
|
-
return {
|
|
428
|
-
lunrQueryBuilder: (q) => {
|
|
429
|
-
const termToken = lunr__default.default.tokenizer(term);
|
|
430
|
-
q.term(termToken, {
|
|
431
|
-
usePipeline: true,
|
|
432
|
-
boost: 100
|
|
433
|
-
});
|
|
434
|
-
q.term(termToken, {
|
|
435
|
-
usePipeline: false,
|
|
436
|
-
boost: 10,
|
|
437
|
-
wildcard: lunr__default.default.Query.wildcard.TRAILING
|
|
438
|
-
});
|
|
439
|
-
q.term(termToken, {
|
|
440
|
-
usePipeline: false,
|
|
441
|
-
editDistance: 2,
|
|
442
|
-
boost: 1
|
|
443
|
-
});
|
|
444
|
-
if (filters) {
|
|
445
|
-
Object.entries(filters).forEach(([field, fieldValue]) => {
|
|
446
|
-
if (!q.allFields.includes(field)) {
|
|
447
|
-
throw new Error(`unrecognised field ${field}`);
|
|
448
|
-
}
|
|
449
|
-
const value = Array.isArray(fieldValue) && fieldValue.length === 1 ? fieldValue[0] : fieldValue;
|
|
450
|
-
if (["string", "number", "boolean"].includes(typeof value)) {
|
|
451
|
-
q.term(
|
|
452
|
-
lunr__default.default.tokenizer(value?.toString()).map(lunr__default.default.stopWordFilter).filter((element) => element !== void 0),
|
|
453
|
-
{
|
|
454
|
-
presence: lunr__default.default.Query.presence.REQUIRED,
|
|
455
|
-
fields: [field]
|
|
456
|
-
}
|
|
457
|
-
);
|
|
458
|
-
} else if (Array.isArray(value)) {
|
|
459
|
-
this.logger.warn(
|
|
460
|
-
`Non-scalar filter value used for field ${field}. Consider using a different Search Engine for better results.`
|
|
461
|
-
);
|
|
462
|
-
q.term(lunr__default.default.tokenizer(value), {
|
|
463
|
-
presence: lunr__default.default.Query.presence.OPTIONAL,
|
|
464
|
-
fields: [field]
|
|
465
|
-
});
|
|
466
|
-
} else {
|
|
467
|
-
this.logger.warn(`Unknown filter type used on field ${field}`);
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
documentTypes: types,
|
|
473
|
-
pageSize
|
|
474
|
-
};
|
|
475
|
-
};
|
|
476
|
-
setTranslator(translator) {
|
|
477
|
-
this.translator = translator;
|
|
478
|
-
}
|
|
479
|
-
async getIndexer(type) {
|
|
480
|
-
const indexer = new LunrSearchEngineIndexer();
|
|
481
|
-
const indexerLogger = this.logger.child({ documentType: type });
|
|
482
|
-
let errorThrown;
|
|
483
|
-
indexer.on("error", (err) => {
|
|
484
|
-
errorThrown = err;
|
|
485
|
-
});
|
|
486
|
-
indexer.on("close", () => {
|
|
487
|
-
const newDocuments = indexer.getDocumentStore();
|
|
488
|
-
const docStoreExists = this.lunrIndices[type] !== void 0;
|
|
489
|
-
const documentsIndexed = Object.keys(newDocuments).length;
|
|
490
|
-
if (!errorThrown && documentsIndexed > 0) {
|
|
491
|
-
this.lunrIndices[type] = indexer.buildIndex();
|
|
492
|
-
this.docStore = { ...this.docStore, ...newDocuments };
|
|
493
|
-
} else {
|
|
494
|
-
indexerLogger.warn(
|
|
495
|
-
`Index for ${type} was not ${docStoreExists ? "replaced" : "created"}: ${errorThrown ? "an error was encountered" : "indexer received 0 documents"}`
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
return indexer;
|
|
500
|
-
}
|
|
501
|
-
async query(query) {
|
|
502
|
-
const { lunrQueryBuilder, documentTypes, pageSize } = this.translator(
|
|
503
|
-
query
|
|
504
|
-
);
|
|
505
|
-
const results = [];
|
|
506
|
-
const indexKeys = Object.keys(this.lunrIndices).filter(
|
|
507
|
-
(type) => !documentTypes || documentTypes.includes(type)
|
|
508
|
-
);
|
|
509
|
-
if (documentTypes?.length && !indexKeys.length) {
|
|
510
|
-
throw new MissingIndexError(
|
|
511
|
-
`Missing index for ${documentTypes?.toString()}. This could be because the index hasn't been created yet or there was a problem during index creation.`
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
indexKeys.forEach((type) => {
|
|
515
|
-
try {
|
|
516
|
-
results.push(
|
|
517
|
-
...this.lunrIndices[type].query(lunrQueryBuilder).map((result) => {
|
|
518
|
-
return {
|
|
519
|
-
result,
|
|
520
|
-
type
|
|
521
|
-
};
|
|
522
|
-
})
|
|
523
|
-
);
|
|
524
|
-
} catch (err) {
|
|
525
|
-
if (err instanceof Error && err.message.startsWith("unrecognised field")) {
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
throw err;
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
results.sort((doc1, doc2) => {
|
|
532
|
-
return doc2.result.score - doc1.result.score;
|
|
533
|
-
});
|
|
534
|
-
const { page } = decodePageCursor(query.pageCursor);
|
|
535
|
-
const offset = page * pageSize;
|
|
536
|
-
const hasPreviousPage = page > 0;
|
|
537
|
-
const hasNextPage = results.length > offset + pageSize;
|
|
538
|
-
const nextPageCursor = hasNextPage ? encodePageCursor({ page: page + 1 }) : void 0;
|
|
539
|
-
const previousPageCursor = hasPreviousPage ? encodePageCursor({ page: page - 1 }) : void 0;
|
|
540
|
-
const realResultSet = {
|
|
541
|
-
results: results.slice(offset, offset + pageSize).map((d, index) => ({
|
|
542
|
-
type: d.type,
|
|
543
|
-
document: this.docStore[d.result.ref],
|
|
544
|
-
rank: page * pageSize + index + 1,
|
|
545
|
-
highlight: {
|
|
546
|
-
preTag: this.highlightPreTag,
|
|
547
|
-
postTag: this.highlightPostTag,
|
|
548
|
-
fields: parseHighlightFields({
|
|
549
|
-
preTag: this.highlightPreTag,
|
|
550
|
-
postTag: this.highlightPostTag,
|
|
551
|
-
doc: this.docStore[d.result.ref],
|
|
552
|
-
positionMetadata: d.result.matchData.metadata
|
|
553
|
-
})
|
|
554
|
-
}
|
|
555
|
-
})),
|
|
556
|
-
numberOfResults: results.length,
|
|
557
|
-
nextPageCursor,
|
|
558
|
-
previousPageCursor
|
|
559
|
-
};
|
|
560
|
-
return realResultSet;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
function decodePageCursor(pageCursor) {
|
|
564
|
-
if (!pageCursor) {
|
|
565
|
-
return { page: 0 };
|
|
566
|
-
}
|
|
567
|
-
return {
|
|
568
|
-
page: Number(Buffer.from(pageCursor, "base64").toString("utf-8"))
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
function encodePageCursor({ page }) {
|
|
572
|
-
return Buffer.from(`${page}`, "utf-8").toString("base64");
|
|
573
|
-
}
|
|
574
|
-
function parseHighlightFields({
|
|
575
|
-
preTag,
|
|
576
|
-
postTag,
|
|
577
|
-
doc,
|
|
578
|
-
positionMetadata
|
|
579
|
-
}) {
|
|
580
|
-
const highlightFieldPositions = Object.values(positionMetadata).reduce(
|
|
581
|
-
(fieldPositions, metadata) => {
|
|
582
|
-
Object.keys(metadata).map((fieldKey) => {
|
|
583
|
-
const validFieldMetadataPositions = metadata[fieldKey]?.position?.filter((position) => Array.isArray(position));
|
|
584
|
-
if (validFieldMetadataPositions.length) {
|
|
585
|
-
fieldPositions[fieldKey] = fieldPositions[fieldKey] ?? [];
|
|
586
|
-
fieldPositions[fieldKey].push(...validFieldMetadataPositions);
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
return fieldPositions;
|
|
590
|
-
},
|
|
591
|
-
{}
|
|
592
|
-
);
|
|
593
|
-
return Object.fromEntries(
|
|
594
|
-
Object.entries(highlightFieldPositions).map(([field, positions]) => {
|
|
595
|
-
positions.sort((a, b) => b[0] - a[0]);
|
|
596
|
-
const highlightedField = positions.reduce((content, pos) => {
|
|
597
|
-
return `${String(content).substring(0, pos[0])}${preTag}${String(content).substring(pos[0], pos[0] + pos[1])}${postTag}${String(content).substring(pos[0] + pos[1])}`;
|
|
598
|
-
}, doc[field] ?? "");
|
|
599
|
-
return [field, highlightedField];
|
|
600
|
-
})
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
class TestPipeline {
|
|
605
|
-
collator;
|
|
606
|
-
decorator;
|
|
607
|
-
indexer;
|
|
608
|
-
constructor({
|
|
609
|
-
collator,
|
|
610
|
-
decorator,
|
|
611
|
-
indexer
|
|
612
|
-
}) {
|
|
613
|
-
this.collator = collator;
|
|
614
|
-
this.decorator = decorator;
|
|
615
|
-
this.indexer = indexer;
|
|
616
|
-
}
|
|
617
|
-
/**
|
|
618
|
-
* Provide the collator, decorator, or indexer to be tested.
|
|
619
|
-
*
|
|
620
|
-
* @deprecated Use `fromCollator`, `fromDecorator` or `fromIndexer` static
|
|
621
|
-
* methods to create a test pipeline instead.
|
|
622
|
-
*/
|
|
623
|
-
static withSubject(subject) {
|
|
624
|
-
if (subject instanceof stream.Transform) {
|
|
625
|
-
return new TestPipeline({ decorator: subject });
|
|
626
|
-
}
|
|
627
|
-
if (subject instanceof stream.Writable) {
|
|
628
|
-
return new TestPipeline({ indexer: subject });
|
|
629
|
-
}
|
|
630
|
-
if (subject.readable || subject instanceof stream.Readable) {
|
|
631
|
-
return new TestPipeline({ collator: subject });
|
|
632
|
-
}
|
|
633
|
-
throw new Error(
|
|
634
|
-
"Unknown test subject: are you passing a readable, writable, or transform stream?"
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Create a test pipeline given a collator you want to test.
|
|
639
|
-
*/
|
|
640
|
-
static fromCollator(collator) {
|
|
641
|
-
return new TestPipeline({ collator });
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
* Add a collator to the test pipeline.
|
|
645
|
-
*/
|
|
646
|
-
withCollator(collator) {
|
|
647
|
-
this.collator = collator;
|
|
648
|
-
return this;
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* Create a test pipeline given a decorator you want to test.
|
|
652
|
-
*/
|
|
653
|
-
static fromDecorator(decorator) {
|
|
654
|
-
return new TestPipeline({ decorator });
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* Add a decorator to the test pipeline.
|
|
658
|
-
*/
|
|
659
|
-
withDecorator(decorator) {
|
|
660
|
-
this.decorator = decorator;
|
|
661
|
-
return this;
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Create a test pipeline given an indexer you want to test.
|
|
665
|
-
*/
|
|
666
|
-
static fromIndexer(indexer) {
|
|
667
|
-
return new TestPipeline({ indexer });
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* Add an indexer to the test pipeline.
|
|
671
|
-
*/
|
|
672
|
-
withIndexer(indexer) {
|
|
673
|
-
this.indexer = indexer;
|
|
674
|
-
return this;
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Provide documents for testing decorators and indexers.
|
|
678
|
-
*/
|
|
679
|
-
withDocuments(documents) {
|
|
680
|
-
if (this.collator) {
|
|
681
|
-
throw new Error("Cannot provide documents when testing a collator.");
|
|
682
|
-
}
|
|
683
|
-
this.collator = new stream.Readable({ objectMode: true });
|
|
684
|
-
this.collator._read = () => {
|
|
685
|
-
};
|
|
686
|
-
process.nextTick(() => {
|
|
687
|
-
documents.forEach((document) => {
|
|
688
|
-
this.collator.push(document);
|
|
689
|
-
});
|
|
690
|
-
this.collator.push(null);
|
|
691
|
-
});
|
|
692
|
-
return this;
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Execute the test pipeline so that you can make assertions about the result
|
|
696
|
-
* or behavior of the given test subject.
|
|
697
|
-
*/
|
|
698
|
-
async execute() {
|
|
699
|
-
const documents = [];
|
|
700
|
-
if (!this.collator) {
|
|
701
|
-
throw new Error(
|
|
702
|
-
"Cannot execute pipeline without a collator or documents"
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
if (!this.indexer) {
|
|
706
|
-
this.indexer = new stream.Writable({ objectMode: true });
|
|
707
|
-
this.indexer._write = (document, _, done) => {
|
|
708
|
-
documents.push(document);
|
|
709
|
-
done();
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
return new Promise((done) => {
|
|
713
|
-
const pipes = [this.collator];
|
|
714
|
-
if (this.decorator) {
|
|
715
|
-
pipes.push(this.decorator);
|
|
716
|
-
}
|
|
717
|
-
pipes.push(this.indexer);
|
|
718
|
-
stream.pipeline(pipes, (error) => {
|
|
719
|
-
done({
|
|
720
|
-
error,
|
|
721
|
-
documents
|
|
722
|
-
});
|
|
723
|
-
});
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
exports.BatchSearchEngineIndexer = BatchSearchEngineIndexer;
|
|
729
|
-
exports.DecoratorBase = DecoratorBase;
|
|
730
|
-
exports.IndexBuilder = IndexBuilder;
|
|
731
|
-
exports.LunrSearchEngine = LunrSearchEngine;
|
|
732
|
-
exports.MissingIndexError = MissingIndexError;
|
|
733
|
-
exports.NewlineDelimitedJsonCollatorFactory = NewlineDelimitedJsonCollatorFactory;
|
|
734
|
-
exports.Scheduler = Scheduler;
|
|
735
|
-
exports.TestPipeline = TestPipeline;
|
|
3
|
+
var IndexBuilder = require('./IndexBuilder.cjs.js');
|
|
4
|
+
var Scheduler = require('./Scheduler.cjs.js');
|
|
5
|
+
var NewlineDelimitedJsonCollatorFactory = require('./collators/NewlineDelimitedJsonCollatorFactory.cjs.js');
|
|
6
|
+
var LunrSearchEngine = require('./engines/LunrSearchEngine.cjs.js');
|
|
7
|
+
var errors = require('./errors.cjs.js');
|
|
8
|
+
var BatchSearchEngineIndexer = require('./indexing/BatchSearchEngineIndexer.cjs.js');
|
|
9
|
+
var DecoratorBase = require('./indexing/DecoratorBase.cjs.js');
|
|
10
|
+
var TestPipeline = require('./test-utils/TestPipeline.cjs.js');
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
exports.IndexBuilder = IndexBuilder.IndexBuilder;
|
|
15
|
+
exports.Scheduler = Scheduler.Scheduler;
|
|
16
|
+
exports.NewlineDelimitedJsonCollatorFactory = NewlineDelimitedJsonCollatorFactory.NewlineDelimitedJsonCollatorFactory;
|
|
17
|
+
exports.LunrSearchEngine = LunrSearchEngine.LunrSearchEngine;
|
|
18
|
+
exports.MissingIndexError = errors.MissingIndexError;
|
|
19
|
+
exports.BatchSearchEngineIndexer = BatchSearchEngineIndexer.BatchSearchEngineIndexer;
|
|
20
|
+
exports.DecoratorBase = DecoratorBase.DecoratorBase;
|
|
21
|
+
exports.TestPipeline = TestPipeline.TestPipeline;
|
|
736
22
|
//# sourceMappingURL=index.cjs.js.map
|