@backstage/plugin-search-backend-node 0.4.6 → 0.5.1-next.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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # @backstage/plugin-search-backend-node
2
2
 
3
+ ## 0.5.1-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`.
8
+ - Updated dependencies
9
+ - @backstage/plugin-search-common@0.3.1-next.0
10
+
11
+ ## 0.5.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 022507c860: **BREAKING**
16
+
17
+ The Backstage Search Platform's indexing process has been rewritten as a stream
18
+ pipeline in order to improve efficiency and performance on large document sets.
19
+
20
+ The concepts of `Collator` and `Decorator` have been replaced with readable and
21
+ transform object streams (respectively), as well as factory classes to
22
+ instantiate them. Accordingly, the `SearchEngine.index()` method has also been
23
+ replaced with a `getIndexer()` factory method that resolves to a writable
24
+ object stream.
25
+
26
+ Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta)
27
+ for further details.
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies
32
+ - @backstage/search-common@0.3.0
33
+
34
+ ## 0.4.7
35
+
36
+ ### Patch Changes
37
+
38
+ - Fix for the previous release with missing type declarations.
39
+ - Updated dependencies
40
+ - @backstage/search-common@0.2.4
41
+
3
42
  ## 0.4.6
4
43
 
5
44
  ### Patch Changes
package/dist/index.cjs.js CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var stream = require('stream');
5
6
  var lunr = require('lunr');
7
+ var errors = require('@backstage/errors');
6
8
 
7
9
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
8
10
 
@@ -23,26 +25,26 @@ class IndexBuilder {
23
25
  return this.documentTypes;
24
26
  }
25
27
  addCollator({
26
- collator,
28
+ factory,
27
29
  defaultRefreshIntervalSeconds
28
30
  }) {
29
- this.logger.info(`Added ${collator.constructor.name} collator for type ${collator.type}`);
30
- this.collators[collator.type] = {
31
+ this.logger.info(`Added ${factory.constructor.name} collator factory for type ${factory.type}`);
32
+ this.collators[factory.type] = {
31
33
  refreshInterval: defaultRefreshIntervalSeconds,
32
- collate: collator
34
+ factory
33
35
  };
34
- this.documentTypes[collator.type] = {
35
- visibilityPermission: collator.visibilityPermission
36
+ this.documentTypes[factory.type] = {
37
+ visibilityPermission: factory.visibilityPermission
36
38
  };
37
39
  }
38
- addDecorator({ decorator }) {
39
- const types = decorator.types || ["*"];
40
- this.logger.info(`Added decorator ${decorator.constructor.name} to types ${types.join(", ")}`);
40
+ addDecorator({ factory }) {
41
+ const types = factory.types || ["*"];
42
+ this.logger.info(`Added decorator ${factory.constructor.name} to types ${types.join(", ")}`);
41
43
  types.forEach((type) => {
42
44
  if (this.decorators.hasOwnProperty(type)) {
43
- this.decorators[type].push(decorator);
45
+ this.decorators[type].push(factory);
44
46
  } else {
45
- this.decorators[type] = [decorator];
47
+ this.decorators[type] = [factory];
46
48
  }
47
49
  });
48
50
  }
@@ -50,29 +52,24 @@ class IndexBuilder {
50
52
  const scheduler = new Scheduler({ logger: this.logger });
51
53
  Object.keys(this.collators).forEach((type) => {
52
54
  scheduler.addToSchedule(async () => {
53
- const decorators = (this.decorators["*"] || []).concat(this.decorators[type] || []);
54
- this.logger.debug(`Collating documents for ${type} via ${this.collators[type].collate.constructor.name}`);
55
- let documents;
56
- try {
57
- documents = await this.collators[type].collate.execute();
58
- } catch (e) {
59
- this.logger.error(`Collating documents for ${type} via ${this.collators[type].collate.constructor.name} failed: ${e}`);
60
- return;
61
- }
62
- for (let i = 0; i < decorators.length; i++) {
63
- this.logger.debug(`Decorating ${type} documents via ${decorators[i].constructor.name}`);
64
- try {
65
- documents = await decorators[i].execute(documents);
66
- } catch (e) {
67
- this.logger.error(`Decorating ${type} documents via ${decorators[i].constructor.name} failed: ${e}`);
68
- return;
69
- }
70
- }
71
- if (!documents || documents.length === 0) {
72
- this.logger.debug(`No documents for type "${type}" to index`);
73
- return;
74
- }
75
- await this.searchEngine.index(type, documents);
55
+ const collator = await this.collators[type].factory.getCollator();
56
+ this.logger.info(`Collating documents for ${type} via ${this.collators[type].factory.constructor.name}`);
57
+ const decorators = await Promise.all((this.decorators["*"] || []).concat(this.decorators[type] || []).map(async (factory) => {
58
+ const decorator = await factory.getDecorator();
59
+ this.logger.info(`Attached decorator via ${factory.constructor.name} to ${type} index pipeline.`);
60
+ return decorator;
61
+ }));
62
+ const indexer = await this.searchEngine.getIndexer(type);
63
+ return new Promise((done) => {
64
+ stream.pipeline([collator, ...decorators, indexer], (error) => {
65
+ if (error) {
66
+ this.logger.error(`Collating documents for ${type} failed: ${error}`);
67
+ } else {
68
+ this.logger.info(`Collating documents for ${type} succeeded`);
69
+ }
70
+ done();
71
+ });
72
+ });
76
73
  }, this.collators[type].refreshInterval * 1e3);
77
74
  });
78
75
  return {
@@ -133,6 +130,144 @@ class Scheduler {
133
130
  }
134
131
  }
135
132
 
133
+ class BatchSearchEngineIndexer extends stream.Writable {
134
+ constructor(options) {
135
+ super({ objectMode: true });
136
+ this.currentBatch = [];
137
+ this.batchSize = options.batchSize;
138
+ this.initialized = new Promise((done) => {
139
+ setImmediate(async () => {
140
+ try {
141
+ await this.initialize();
142
+ done(void 0);
143
+ } catch (e) {
144
+ errors.assertError(e);
145
+ done(e);
146
+ }
147
+ });
148
+ });
149
+ }
150
+ async _write(doc, _e, done) {
151
+ const maybeError = await this.initialized;
152
+ if (maybeError) {
153
+ done(maybeError);
154
+ return;
155
+ }
156
+ this.currentBatch.push(doc);
157
+ if (this.currentBatch.length < this.batchSize) {
158
+ done();
159
+ return;
160
+ }
161
+ try {
162
+ await this.index(this.currentBatch);
163
+ this.currentBatch = [];
164
+ done();
165
+ } catch (e) {
166
+ errors.assertError(e);
167
+ done(e);
168
+ }
169
+ }
170
+ async _final(done) {
171
+ try {
172
+ if (this.currentBatch.length) {
173
+ await this.index(this.currentBatch);
174
+ this.currentBatch = [];
175
+ }
176
+ await this.finalize();
177
+ done();
178
+ } catch (e) {
179
+ errors.assertError(e);
180
+ done(e);
181
+ }
182
+ }
183
+ }
184
+
185
+ class DecoratorBase extends stream.Transform {
186
+ constructor() {
187
+ super({ objectMode: true });
188
+ this.initialized = new Promise((done) => {
189
+ setImmediate(async () => {
190
+ try {
191
+ await this.initialize();
192
+ done(void 0);
193
+ } catch (e) {
194
+ errors.assertError(e);
195
+ done(e);
196
+ }
197
+ });
198
+ });
199
+ }
200
+ async _transform(document, _, done) {
201
+ const maybeError = await this.initialized;
202
+ if (maybeError) {
203
+ done(maybeError);
204
+ return;
205
+ }
206
+ try {
207
+ const decorated = await this.decorate(document);
208
+ if (decorated === void 0) {
209
+ done();
210
+ return;
211
+ }
212
+ if (Array.isArray(decorated)) {
213
+ decorated.forEach((doc) => {
214
+ this.push(doc);
215
+ });
216
+ done();
217
+ return;
218
+ }
219
+ this.push(decorated);
220
+ done();
221
+ } catch (e) {
222
+ errors.assertError(e);
223
+ done(e);
224
+ }
225
+ }
226
+ async _final(done) {
227
+ try {
228
+ await this.finalize();
229
+ done();
230
+ } catch (e) {
231
+ errors.assertError(e);
232
+ done(e);
233
+ }
234
+ }
235
+ }
236
+
237
+ class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
238
+ constructor() {
239
+ super({ batchSize: 100 });
240
+ this.schemaInitialized = false;
241
+ this.docStore = {};
242
+ this.builder = new lunr__default["default"].Builder();
243
+ this.builder.pipeline.add(lunr__default["default"].trimmer, lunr__default["default"].stopWordFilter, lunr__default["default"].stemmer);
244
+ this.builder.searchPipeline.add(lunr__default["default"].stemmer);
245
+ }
246
+ async initialize() {
247
+ }
248
+ async finalize() {
249
+ }
250
+ async index(documents) {
251
+ if (!this.schemaInitialized) {
252
+ Object.keys(documents[0]).forEach((field) => {
253
+ this.builder.field(field);
254
+ });
255
+ this.builder.ref("location");
256
+ this.schemaInitialized = true;
257
+ }
258
+ documents.forEach((document) => {
259
+ this.builder.add(document);
260
+ this.docStore[document.location] = document;
261
+ });
262
+ }
263
+ buildIndex() {
264
+ return this.builder.build();
265
+ }
266
+ getDocumentStore() {
267
+ return this.docStore;
268
+ }
269
+ }
270
+
136
271
  class LunrSearchEngine {
137
272
  constructor({ logger }) {
138
273
  this.lunrIndices = {};
@@ -192,19 +327,13 @@ class LunrSearchEngine {
192
327
  setTranslator(translator) {
193
328
  this.translator = translator;
194
329
  }
195
- async index(type, documents) {
196
- const lunrBuilder = new lunr__default["default"].Builder();
197
- lunrBuilder.pipeline.add(lunr__default["default"].trimmer, lunr__default["default"].stopWordFilter, lunr__default["default"].stemmer);
198
- lunrBuilder.searchPipeline.add(lunr__default["default"].stemmer);
199
- Object.keys(documents[0]).forEach((field) => {
200
- lunrBuilder.field(field);
330
+ async getIndexer(type) {
331
+ const indexer = new LunrSearchEngineIndexer();
332
+ indexer.on("close", () => {
333
+ this.lunrIndices[type] = indexer.buildIndex();
334
+ this.docStore = { ...this.docStore, ...indexer.getDocumentStore() };
201
335
  });
202
- lunrBuilder.ref("location");
203
- documents.forEach((document) => {
204
- lunrBuilder.add(document);
205
- this.docStore[document.location] = document;
206
- });
207
- this.lunrIndices[type] = lunrBuilder.build();
336
+ return indexer;
208
337
  }
209
338
  async query(query) {
210
339
  const { lunrQueryBuilder, documentTypes, pageSize } = this.translator(query);
@@ -255,7 +384,75 @@ function encodePageCursor({ page }) {
255
384
  return Buffer.from(`${page}`, "utf-8").toString("base64");
256
385
  }
257
386
 
387
+ class TestPipeline {
388
+ constructor({
389
+ collator,
390
+ decorator,
391
+ indexer
392
+ }) {
393
+ this.collator = collator;
394
+ this.decorator = decorator;
395
+ this.indexer = indexer;
396
+ }
397
+ static withSubject(subject) {
398
+ if (subject instanceof stream.Transform) {
399
+ return new TestPipeline({ decorator: subject });
400
+ }
401
+ if (subject instanceof stream.Readable) {
402
+ return new TestPipeline({ collator: subject });
403
+ }
404
+ if (subject instanceof stream.Writable) {
405
+ return new TestPipeline({ indexer: subject });
406
+ }
407
+ throw new Error("Unknown test subject: are you passing a readable, writable, or transform stream?");
408
+ }
409
+ withDocuments(documents) {
410
+ if (this.collator) {
411
+ throw new Error("Cannot provide documents when testing a collator.");
412
+ }
413
+ this.collator = new stream.Readable({ objectMode: true });
414
+ this.collator._read = () => {
415
+ };
416
+ process.nextTick(() => {
417
+ documents.forEach((document) => {
418
+ this.collator.push(document);
419
+ });
420
+ this.collator.push(null);
421
+ });
422
+ return this;
423
+ }
424
+ async execute() {
425
+ const documents = [];
426
+ if (!this.collator) {
427
+ throw new Error("Cannot execute pipeline without a collator or documents");
428
+ }
429
+ if (!this.indexer) {
430
+ this.indexer = new stream.Writable({ objectMode: true });
431
+ this.indexer._write = (document, _, done) => {
432
+ documents.push(document);
433
+ done();
434
+ };
435
+ }
436
+ return new Promise((done) => {
437
+ const pipes = [this.collator];
438
+ if (this.decorator) {
439
+ pipes.push(this.decorator);
440
+ }
441
+ pipes.push(this.indexer);
442
+ stream.pipeline(pipes, (error) => {
443
+ done({
444
+ error,
445
+ documents
446
+ });
447
+ });
448
+ });
449
+ }
450
+ }
451
+
452
+ exports.BatchSearchEngineIndexer = BatchSearchEngineIndexer;
453
+ exports.DecoratorBase = DecoratorBase;
258
454
  exports.IndexBuilder = IndexBuilder;
259
455
  exports.LunrSearchEngine = LunrSearchEngine;
260
456
  exports.Scheduler = Scheduler;
457
+ exports.TestPipeline = TestPipeline;
261
458
  //# sourceMappingURL=index.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":["../src/IndexBuilder.ts","../src/runPeriodically.ts","../src/Scheduler.ts","../src/engines/LunrSearchEngine.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DocumentCollator,\n DocumentDecorator,\n DocumentTypeInfo,\n IndexableDocument,\n SearchEngine,\n} from '@backstage/search-common';\nimport { Logger } from 'winston';\nimport { Scheduler } from './index';\nimport {\n RegisterCollatorParameters,\n RegisterDecoratorParameters,\n} from './types';\n\ninterface CollatorEnvelope {\n collate: DocumentCollator;\n refreshInterval: number;\n}\n\ntype IndexBuilderOptions = {\n searchEngine: SearchEngine;\n logger: Logger;\n};\n\nexport class IndexBuilder {\n private collators: Record<string, CollatorEnvelope>;\n private decorators: Record<string, DocumentDecorator[]>;\n private documentTypes: Record<string, DocumentTypeInfo>;\n private searchEngine: SearchEngine;\n private logger: Logger;\n\n constructor({ logger, searchEngine }: IndexBuilderOptions) {\n this.collators = {};\n this.decorators = {};\n this.documentTypes = {};\n this.logger = logger;\n this.searchEngine = searchEngine;\n }\n\n getSearchEngine(): SearchEngine {\n return this.searchEngine;\n }\n\n getDocumentTypes(): Record<string, DocumentTypeInfo> {\n return this.documentTypes;\n }\n\n /**\n * Makes the index builder aware of a collator that should be executed at the\n * given refresh interval.\n */\n addCollator({\n collator,\n defaultRefreshIntervalSeconds,\n }: RegisterCollatorParameters): void {\n this.logger.info(\n `Added ${collator.constructor.name} collator for type ${collator.type}`,\n );\n this.collators[collator.type] = {\n refreshInterval: defaultRefreshIntervalSeconds,\n collate: collator,\n };\n this.documentTypes[collator.type] = {\n visibilityPermission: collator.visibilityPermission,\n };\n }\n\n /**\n * Makes the index builder aware of a decorator. If no types are provided on\n * the decorator, it will be applied to documents from all known collators,\n * otherwise it will only be applied to documents of the given types.\n */\n addDecorator({ decorator }: RegisterDecoratorParameters): void {\n const types = decorator.types || ['*'];\n this.logger.info(\n `Added decorator ${decorator.constructor.name} to types ${types.join(\n ', ',\n )}`,\n );\n types.forEach(type => {\n if (this.decorators.hasOwnProperty(type)) {\n this.decorators[type].push(decorator);\n } else {\n this.decorators[type] = [decorator];\n }\n });\n }\n\n /**\n * Compiles collators and decorators into tasks, which are added to a\n * scheduler returned to the caller.\n */\n async build(): Promise<{ scheduler: Scheduler }> {\n const scheduler = new Scheduler({ logger: this.logger });\n\n Object.keys(this.collators).forEach(type => {\n scheduler.addToSchedule(async () => {\n // Collate, Decorate, Index.\n const decorators: DocumentDecorator[] = (\n this.decorators['*'] || []\n ).concat(this.decorators[type] || []);\n\n this.logger.debug(\n `Collating documents for ${type} via ${this.collators[type].collate.constructor.name}`,\n );\n let documents: IndexableDocument[];\n\n try {\n documents = await this.collators[type].collate.execute();\n } catch (e) {\n this.logger.error(\n `Collating documents for ${type} via ${this.collators[type].collate.constructor.name} failed: ${e}`,\n );\n return;\n }\n\n for (let i = 0; i < decorators.length; i++) {\n this.logger.debug(\n `Decorating ${type} documents via ${decorators[i].constructor.name}`,\n );\n try {\n documents = await decorators[i].execute(documents);\n } catch (e) {\n this.logger.error(\n `Decorating ${type} documents via ${decorators[i].constructor.name} failed: ${e}`,\n );\n return;\n }\n }\n\n if (!documents || documents.length === 0) {\n this.logger.debug(`No documents for type \"${type}\" to index`);\n return;\n }\n\n // pushing documents to index to a configured search engine.\n await this.searchEngine.index(type, documents);\n }, this.collators[type].refreshInterval * 1000);\n });\n\n return {\n scheduler,\n };\n }\n}\n","/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Runs a function repeatedly, with a fixed wait between invocations.\n *\n * Supports async functions, and silently ignores exceptions and rejections.\n *\n * @param fn - The function to run. May return a Promise.\n * @param delayMs - The delay between a completed function invocation and the\n * next.\n * @returns A function that, when called, stops the invocation loop.\n */\nexport function runPeriodically(fn: () => any, delayMs: number): () => void {\n let cancel: () => void;\n let cancelled = false;\n const cancellationPromise = new Promise<void>(resolve => {\n cancel = () => {\n resolve();\n cancelled = true;\n };\n });\n\n const startRefresh = async () => {\n while (!cancelled) {\n try {\n await fn();\n } catch {\n // ignore intentionally\n }\n\n await Promise.race([\n new Promise(resolve => setTimeout(resolve, delayMs)),\n cancellationPromise,\n ]);\n }\n };\n startRefresh();\n\n return cancel!;\n}\n","/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Logger } from 'winston';\nimport { runPeriodically } from './runPeriodically';\n\ntype TaskEnvelope = {\n task: Function;\n interval: number;\n};\n\n/**\n * TODO: coordination, error handling\n */\n\nexport class Scheduler {\n private logger: Logger;\n private schedule: TaskEnvelope[];\n private runningTasks: Function[] = [];\n\n constructor({ logger }: { logger: Logger }) {\n this.logger = logger;\n this.schedule = [];\n }\n\n /**\n * Adds each task and interval to the schedule.\n * When running the tasks, the scheduler waits at least for the time specified\n * in the interval once the task was completed, before running it again.\n */\n addToSchedule(task: Function, interval: number) {\n if (this.runningTasks.length) {\n throw new Error(\n 'Cannot add task to schedule that has already been started.',\n );\n }\n this.schedule.push({ task, interval });\n }\n\n /**\n * Starts the scheduling process for each task\n */\n start() {\n this.logger.info('Starting all scheduled search tasks.');\n this.schedule.forEach(({ task, interval }) => {\n this.runningTasks.push(runPeriodically(() => task(), interval));\n });\n }\n\n /**\n * Stop all scheduled tasks.\n */\n stop() {\n this.logger.info('Stopping all scheduled search tasks.');\n this.runningTasks.forEach(cancel => {\n cancel();\n });\n this.runningTasks = [];\n }\n}\n","/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n IndexableDocument,\n SearchQuery,\n SearchResultSet,\n QueryTranslator,\n SearchEngine,\n} from '@backstage/search-common';\nimport lunr from 'lunr';\nimport { Logger } from 'winston';\n\nexport type ConcreteLunrQuery = {\n lunrQueryBuilder: lunr.Index.QueryBuilder;\n documentTypes?: string[];\n pageSize: number;\n};\n\ntype LunrResultEnvelope = {\n result: lunr.Index.Result;\n type: string;\n};\n\ntype LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery;\n\nexport class LunrSearchEngine implements SearchEngine {\n protected lunrIndices: Record<string, lunr.Index> = {};\n protected docStore: Record<string, IndexableDocument>;\n protected logger: Logger;\n\n constructor({ logger }: { logger: Logger }) {\n this.logger = logger;\n this.docStore = {};\n }\n\n protected translator: QueryTranslator = ({\n term,\n filters,\n types,\n }: SearchQuery): ConcreteLunrQuery => {\n const pageSize = 25;\n\n return {\n lunrQueryBuilder: q => {\n const termToken = lunr.tokenizer(term);\n\n // Support for typeahead seach is based on https://github.com/olivernn/lunr.js/issues/256#issuecomment-295407852\n // look for an exact match and apply a large positive boost\n q.term(termToken, {\n usePipeline: true,\n boost: 100,\n });\n // look for terms that match the beginning of this term and apply a\n // medium boost\n q.term(termToken, {\n usePipeline: false,\n boost: 10,\n wildcard: lunr.Query.wildcard.TRAILING,\n });\n // look for terms that match with an edit distance of 2 and apply a\n // small boost\n q.term(termToken, {\n usePipeline: false,\n editDistance: 2,\n boost: 1,\n });\n\n if (filters) {\n Object.entries(filters).forEach(([field, fieldValue]) => {\n if (!q.allFields.includes(field)) {\n // Throw for unknown field, as this will be a non match\n throw new Error(`unrecognised field ${field}`);\n }\n // Arrays are poorly supported, but we can make it better for single-item arrays,\n // which should be a common case\n const value =\n Array.isArray(fieldValue) && fieldValue.length === 1\n ? fieldValue[0]\n : fieldValue;\n\n // Require that the given field has the given value\n if (['string', 'number', 'boolean'].includes(typeof value)) {\n q.term(lunr.tokenizer(value?.toString()), {\n presence: lunr.Query.presence.REQUIRED,\n fields: [field],\n });\n } else if (Array.isArray(value)) {\n // Illustrate how multi-value filters could work.\n // But warn that Lurn supports this poorly.\n this.logger.warn(\n `Non-scalar filter value used for field ${field}. Consider using a different Search Engine for better results.`,\n );\n q.term(lunr.tokenizer(value), {\n presence: lunr.Query.presence.OPTIONAL,\n fields: [field],\n });\n } else {\n // Log a warning or something about unknown filter value\n this.logger.warn(`Unknown filter type used on field ${field}`);\n }\n });\n }\n },\n documentTypes: types,\n pageSize,\n };\n };\n\n setTranslator(translator: LunrQueryTranslator) {\n this.translator = translator;\n }\n\n async index(type: string, documents: IndexableDocument[]): Promise<void> {\n const lunrBuilder = new lunr.Builder();\n\n lunrBuilder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer);\n lunrBuilder.searchPipeline.add(lunr.stemmer);\n\n // Make this lunr index aware of all relevant fields.\n Object.keys(documents[0]).forEach(field => {\n lunrBuilder.field(field);\n });\n\n // Set \"location\" field as reference field\n lunrBuilder.ref('location');\n\n documents.forEach((document: IndexableDocument) => {\n // Add document to Lunar index\n lunrBuilder.add(document);\n // Store documents in memory to be able to look up document using the ref during query time\n // This is not how you should implement your SearchEngine implementation! Do not copy!\n this.docStore[document.location] = document;\n });\n\n // \"Rotate\" the index by simply overwriting any existing index of the same name.\n this.lunrIndices[type] = lunrBuilder.build();\n }\n\n async query(query: SearchQuery): Promise<SearchResultSet> {\n const { lunrQueryBuilder, documentTypes, pageSize } = this.translator(\n query,\n ) as ConcreteLunrQuery;\n\n const results: LunrResultEnvelope[] = [];\n\n // Iterate over the filtered list of this.lunrIndex keys.\n Object.keys(this.lunrIndices)\n .filter(type => !documentTypes || documentTypes.includes(type))\n .forEach(type => {\n try {\n results.push(\n ...this.lunrIndices[type].query(lunrQueryBuilder).map(result => {\n return {\n result: result,\n type: type,\n };\n }),\n );\n } catch (err) {\n // if a field does not exist on a index, we can see that as a no-match\n if (\n err instanceof Error &&\n err.message.startsWith('unrecognised field')\n ) {\n return;\n }\n throw err;\n }\n });\n\n // Sort results.\n results.sort((doc1, doc2) => {\n return doc2.result.score - doc1.result.score;\n });\n\n // Perform paging\n const { page } = decodePageCursor(query.pageCursor);\n const offset = page * pageSize;\n const hasPreviousPage = page > 0;\n const hasNextPage = results.length > offset + pageSize;\n const nextPageCursor = hasNextPage\n ? encodePageCursor({ page: page + 1 })\n : undefined;\n const previousPageCursor = hasPreviousPage\n ? encodePageCursor({ page: page - 1 })\n : undefined;\n\n // Translate results into SearchResultSet\n const realResultSet: SearchResultSet = {\n results: results.slice(offset, offset + pageSize).map(d => {\n return { type: d.type, document: this.docStore[d.result.ref] };\n }),\n nextPageCursor,\n previousPageCursor,\n };\n\n return realResultSet;\n }\n}\n\nexport function decodePageCursor(pageCursor?: string): { page: number } {\n if (!pageCursor) {\n return { page: 0 };\n }\n\n return {\n page: Number(Buffer.from(pageCursor, 'base64').toString('utf-8')),\n };\n}\n\nexport function encodePageCursor({ page }: { page: number }): string {\n return Buffer.from(`${page}`, 'utf-8').toString('base64');\n}\n"],"names":["lunr"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/IndexBuilder.ts","../src/runPeriodically.ts","../src/Scheduler.ts","../src/indexing/BatchSearchEngineIndexer.ts","../src/indexing/DecoratorBase.ts","../src/engines/LunrSearchEngineIndexer.ts","../src/engines/LunrSearchEngine.ts","../src/test-utils/TestPipeline.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DocumentCollatorFactory,\n DocumentDecoratorFactory,\n DocumentTypeInfo,\n SearchEngine,\n} from '@backstage/plugin-search-common';\nimport { Transform, pipeline } from 'stream';\nimport { Logger } from 'winston';\nimport { Scheduler } from './index';\nimport {\n IndexBuilderOptions,\n RegisterCollatorParameters,\n RegisterDecoratorParameters,\n} from './types';\n\ninterface CollatorEnvelope {\n factory: DocumentCollatorFactory;\n refreshInterval: number;\n}\n\n/**\n * @beta\n */\nexport class IndexBuilder {\n private collators: Record<string, CollatorEnvelope>;\n private decorators: Record<string, DocumentDecoratorFactory[]>;\n private documentTypes: Record<string, DocumentTypeInfo>;\n private searchEngine: SearchEngine;\n private logger: Logger;\n\n constructor({ logger, searchEngine }: IndexBuilderOptions) {\n this.collators = {};\n this.decorators = {};\n this.documentTypes = {};\n this.logger = logger;\n this.searchEngine = searchEngine;\n }\n\n getSearchEngine(): SearchEngine {\n return this.searchEngine;\n }\n\n getDocumentTypes(): Record<string, DocumentTypeInfo> {\n return this.documentTypes;\n }\n\n /**\n * Makes the index builder aware of a collator that should be executed at the\n * given refresh interval.\n */\n addCollator({\n factory,\n defaultRefreshIntervalSeconds,\n }: RegisterCollatorParameters): void {\n this.logger.info(\n `Added ${factory.constructor.name} collator factory for type ${factory.type}`,\n );\n this.collators[factory.type] = {\n refreshInterval: defaultRefreshIntervalSeconds,\n factory,\n };\n this.documentTypes[factory.type] = {\n visibilityPermission: factory.visibilityPermission,\n };\n }\n\n /**\n * Makes the index builder aware of a decorator. If no types are provided on\n * the decorator, it will be applied to documents from all known collators,\n * otherwise it will only be applied to documents of the given types.\n */\n addDecorator({ factory }: RegisterDecoratorParameters): void {\n const types = factory.types || ['*'];\n this.logger.info(\n `Added decorator ${factory.constructor.name} to types ${types.join(\n ', ',\n )}`,\n );\n types.forEach(type => {\n if (this.decorators.hasOwnProperty(type)) {\n this.decorators[type].push(factory);\n } else {\n this.decorators[type] = [factory];\n }\n });\n }\n\n /**\n * Compiles collators and decorators into tasks, which are added to a\n * scheduler returned to the caller.\n */\n async build(): Promise<{ scheduler: Scheduler }> {\n const scheduler = new Scheduler({ logger: this.logger });\n\n Object.keys(this.collators).forEach(type => {\n scheduler.addToSchedule(async () => {\n // Instantiate the collator.\n const collator = await this.collators[type].factory.getCollator();\n this.logger.info(\n `Collating documents for ${type} via ${this.collators[type].factory.constructor.name}`,\n );\n\n // Instantiate all relevant decorators.\n const decorators: Transform[] = await Promise.all(\n (this.decorators['*'] || [])\n .concat(this.decorators[type] || [])\n .map(async factory => {\n const decorator = await factory.getDecorator();\n this.logger.info(\n `Attached decorator via ${factory.constructor.name} to ${type} index pipeline.`,\n );\n return decorator;\n }),\n );\n\n // Instantiate the indexer.\n const indexer = await this.searchEngine.getIndexer(type);\n\n // Compose collator/decorators/indexer into a pipeline\n return new Promise<void>(done => {\n pipeline([collator, ...decorators, indexer], error => {\n if (error) {\n this.logger.error(\n `Collating documents for ${type} failed: ${error}`,\n );\n } else {\n this.logger.info(`Collating documents for ${type} succeeded`);\n }\n\n // Signal index pipeline completion!\n done();\n });\n });\n }, this.collators[type].refreshInterval * 1000);\n });\n\n return {\n scheduler,\n };\n }\n}\n","/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Runs a function repeatedly, with a fixed wait between invocations.\n *\n * Supports async functions, and silently ignores exceptions and rejections.\n *\n * @param fn - The function to run. May return a Promise.\n * @param delayMs - The delay between a completed function invocation and the\n * next.\n * @returns A function that, when called, stops the invocation loop.\n */\nexport function runPeriodically(fn: () => any, delayMs: number): () => void {\n let cancel: () => void;\n let cancelled = false;\n const cancellationPromise = new Promise<void>(resolve => {\n cancel = () => {\n resolve();\n cancelled = true;\n };\n });\n\n const startRefresh = async () => {\n while (!cancelled) {\n try {\n await fn();\n } catch {\n // ignore intentionally\n }\n\n await Promise.race([\n new Promise(resolve => setTimeout(resolve, delayMs)),\n cancellationPromise,\n ]);\n }\n };\n startRefresh();\n\n return cancel!;\n}\n","/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Logger } from 'winston';\nimport { runPeriodically } from './runPeriodically';\n\ntype TaskEnvelope = {\n task: Function;\n interval: number;\n};\n\n/**\n * TODO: coordination, error handling\n */\n\n/**\n * @beta\n */\nexport class Scheduler {\n private logger: Logger;\n private schedule: TaskEnvelope[];\n private runningTasks: Function[] = [];\n\n constructor({ logger }: { logger: Logger }) {\n this.logger = logger;\n this.schedule = [];\n }\n\n /**\n * Adds each task and interval to the schedule.\n * When running the tasks, the scheduler waits at least for the time specified\n * in the interval once the task was completed, before running it again.\n */\n addToSchedule(task: Function, interval: number) {\n if (this.runningTasks.length) {\n throw new Error(\n 'Cannot add task to schedule that has already been started.',\n );\n }\n this.schedule.push({ task, interval });\n }\n\n /**\n * Starts the scheduling process for each task\n */\n start() {\n this.logger.info('Starting all scheduled search tasks.');\n this.schedule.forEach(({ task, interval }) => {\n this.runningTasks.push(runPeriodically(() => task(), interval));\n });\n }\n\n /**\n * Stop all scheduled tasks.\n */\n stop() {\n this.logger.info('Stopping all scheduled search tasks.');\n this.runningTasks.forEach(cancel => {\n cancel();\n });\n this.runningTasks = [];\n }\n}\n","/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { assertError } from '@backstage/errors';\nimport { IndexableDocument } from '@backstage/plugin-search-common';\nimport { Writable } from 'stream';\n\n/**\n * @beta\n */\nexport type BatchSearchEngineOptions = {\n batchSize: number;\n};\n\n/**\n * Base class encapsulating batch-based stream processing. Useful as a base\n * class for search engine indexers.\n * @beta\n */\nexport abstract class BatchSearchEngineIndexer extends Writable {\n private batchSize: number;\n private currentBatch: IndexableDocument[] = [];\n private initialized: Promise<undefined | Error>;\n\n constructor(options: BatchSearchEngineOptions) {\n super({ objectMode: true });\n this.batchSize = options.batchSize;\n\n // @todo Once node v15 is minimum, convert to _construct implementation.\n this.initialized = new Promise(done => {\n // Necessary to allow concrete implementation classes to construct\n // themselves before calling their initialize() methods.\n setImmediate(async () => {\n try {\n await this.initialize();\n done(undefined);\n } catch (e) {\n assertError(e);\n done(e);\n }\n });\n });\n }\n\n /**\n * Receives an array of indexable documents (of size this.batchSize) which\n * should be written to the search engine. This method won't be called again\n * at least until it resolves.\n */\n public abstract index(documents: IndexableDocument[]): Promise<void>;\n\n /**\n * Any asynchronous setup tasks can be performed here.\n */\n public abstract initialize(): Promise<void>;\n\n /**\n * Any asynchronous teardown tasks can be performed here.\n */\n public abstract finalize(): Promise<void>;\n\n /**\n * Encapsulates batch stream write logic.\n * @internal\n */\n async _write(\n doc: IndexableDocument,\n _e: any,\n done: (error?: Error | null) => void,\n ) {\n // Wait for init before proceeding. Throw error if initialization failed.\n const maybeError = await this.initialized;\n if (maybeError) {\n done(maybeError);\n return;\n }\n\n this.currentBatch.push(doc);\n if (this.currentBatch.length < this.batchSize) {\n done();\n return;\n }\n\n try {\n await this.index(this.currentBatch);\n this.currentBatch = [];\n done();\n } catch (e) {\n assertError(e);\n done(e);\n }\n }\n\n /**\n * Encapsulates finalization and final error handling logic.\n * @internal\n */\n async _final(done: (error?: Error | null) => void) {\n try {\n // Index any remaining documents.\n if (this.currentBatch.length) {\n await this.index(this.currentBatch);\n this.currentBatch = [];\n }\n await this.finalize();\n done();\n } catch (e) {\n assertError(e);\n done(e);\n }\n }\n}\n","/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { assertError } from '@backstage/errors';\nimport { IndexableDocument } from '@backstage/plugin-search-common';\nimport { Transform } from 'stream';\n\n/**\n * Base class encapsulating simple async transformations. Useful as a base\n * class for Backstage search decorators.\n * @beta\n */\nexport abstract class DecoratorBase extends Transform {\n private initialized: Promise<undefined | Error>;\n\n constructor() {\n super({ objectMode: true });\n\n // @todo Once node v15 is minimum, convert to _construct implementation.\n this.initialized = new Promise(done => {\n // Necessary to allow concrete implementation classes to construct\n // themselves before calling their initialize() methods.\n setImmediate(async () => {\n try {\n await this.initialize();\n done(undefined);\n } catch (e) {\n assertError(e);\n done(e);\n }\n });\n });\n }\n\n /**\n * Any asynchronous setup tasks can be performed here.\n */\n public abstract initialize(): Promise<void>;\n\n /**\n * Receives a single indexable document. In your decorate method, you can:\n *\n * - Resolve `undefined` to indicate the record should be omitted.\n * - Resolve a single modified document, which could contain new fields,\n * edited fields, or removed fields.\n * - Resolve an array of indexable documents, if the purpose if the decorator\n * is to convert one document into multiple derivative documents.\n */\n public abstract decorate(\n document: IndexableDocument,\n ): Promise<IndexableDocument | IndexableDocument[] | undefined>;\n\n /**\n * Any asynchronous teardown tasks can be performed here.\n */\n public abstract finalize(): Promise<void>;\n\n /**\n * Encapsulates simple transform stream logic.\n * @internal\n */\n async _transform(\n document: IndexableDocument,\n _: any,\n done: (error?: Error | null) => void,\n ) {\n // Wait for init before proceeding. Throw error if initialization failed.\n const maybeError = await this.initialized;\n if (maybeError) {\n done(maybeError);\n return;\n }\n\n try {\n const decorated = await this.decorate(document);\n\n // If undefined was returned, omit the record and move on.\n if (decorated === undefined) {\n done();\n return;\n }\n\n // If an array of documents was given, push them all.\n if (Array.isArray(decorated)) {\n decorated.forEach(doc => {\n this.push(doc);\n });\n done();\n return;\n }\n\n // Otherwise, just push the decorated document.\n this.push(decorated);\n done();\n } catch (e) {\n assertError(e);\n done(e);\n }\n }\n\n /**\n * Encapsulates finalization and final error handling logic.\n * @internal\n */\n async _final(done: (error?: Error | null) => void) {\n try {\n await this.finalize();\n done();\n } catch (e) {\n assertError(e);\n done(e);\n }\n }\n}\n","/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { IndexableDocument } from '@backstage/plugin-search-common';\nimport lunr from 'lunr';\nimport { BatchSearchEngineIndexer } from '../indexing';\n\n/**\n * @beta\n */\nexport class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {\n private schemaInitialized = false;\n private builder: lunr.Builder;\n private docStore: Record<string, IndexableDocument> = {};\n\n constructor() {\n super({ batchSize: 100 });\n\n this.builder = new lunr.Builder();\n this.builder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer);\n this.builder.searchPipeline.add(lunr.stemmer);\n }\n\n // No async initialization required.\n async initialize(): Promise<void> {}\n async finalize(): Promise<void> {}\n\n async index(documents: IndexableDocument[]): Promise<void> {\n if (!this.schemaInitialized) {\n // Make this lunr index aware of all relevant fields.\n Object.keys(documents[0]).forEach(field => {\n this.builder.field(field);\n });\n\n // Set \"location\" field as reference field\n this.builder.ref('location');\n\n this.schemaInitialized = true;\n }\n\n documents.forEach(document => {\n // Add document to Lunar index\n this.builder.add(document);\n\n // Store documents in memory to be able to look up document using the ref during query time\n // This is not how you should implement your SearchEngine implementation! Do not copy!\n this.docStore[document.location] = document;\n });\n }\n\n buildIndex() {\n return this.builder.build();\n }\n\n getDocumentStore() {\n return this.docStore;\n }\n}\n","/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n IndexableDocument,\n SearchQuery,\n SearchResultSet,\n QueryTranslator,\n SearchEngine,\n} from '@backstage/plugin-search-common';\nimport lunr from 'lunr';\nimport { Logger } from 'winston';\nimport { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer';\n\n/**\n * @beta\n */\nexport type ConcreteLunrQuery = {\n lunrQueryBuilder: lunr.Index.QueryBuilder;\n documentTypes?: string[];\n pageSize: number;\n};\n\ntype LunrResultEnvelope = {\n result: lunr.Index.Result;\n type: string;\n};\n\n/**\n * @beta\n */\nexport type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery;\n\n/**\n * @beta\n */\nexport class LunrSearchEngine implements SearchEngine {\n protected lunrIndices: Record<string, lunr.Index> = {};\n protected docStore: Record<string, IndexableDocument>;\n protected logger: Logger;\n\n constructor({ logger }: { logger: Logger }) {\n this.logger = logger;\n this.docStore = {};\n }\n\n protected translator: QueryTranslator = ({\n term,\n filters,\n types,\n }: SearchQuery): ConcreteLunrQuery => {\n const pageSize = 25;\n\n return {\n lunrQueryBuilder: q => {\n const termToken = lunr.tokenizer(term);\n\n // Support for typeahead seach is based on https://github.com/olivernn/lunr.js/issues/256#issuecomment-295407852\n // look for an exact match and apply a large positive boost\n q.term(termToken, {\n usePipeline: true,\n boost: 100,\n });\n // look for terms that match the beginning of this term and apply a\n // medium boost\n q.term(termToken, {\n usePipeline: false,\n boost: 10,\n wildcard: lunr.Query.wildcard.TRAILING,\n });\n // look for terms that match with an edit distance of 2 and apply a\n // small boost\n q.term(termToken, {\n usePipeline: false,\n editDistance: 2,\n boost: 1,\n });\n\n if (filters) {\n Object.entries(filters).forEach(([field, fieldValue]) => {\n if (!q.allFields.includes(field)) {\n // Throw for unknown field, as this will be a non match\n throw new Error(`unrecognised field ${field}`);\n }\n // Arrays are poorly supported, but we can make it better for single-item arrays,\n // which should be a common case\n const value =\n Array.isArray(fieldValue) && fieldValue.length === 1\n ? fieldValue[0]\n : fieldValue;\n\n // Require that the given field has the given value\n if (['string', 'number', 'boolean'].includes(typeof value)) {\n q.term(lunr.tokenizer(value?.toString()), {\n presence: lunr.Query.presence.REQUIRED,\n fields: [field],\n });\n } else if (Array.isArray(value)) {\n // Illustrate how multi-value filters could work.\n // But warn that Lurn supports this poorly.\n this.logger.warn(\n `Non-scalar filter value used for field ${field}. Consider using a different Search Engine for better results.`,\n );\n q.term(lunr.tokenizer(value), {\n presence: lunr.Query.presence.OPTIONAL,\n fields: [field],\n });\n } else {\n // Log a warning or something about unknown filter value\n this.logger.warn(`Unknown filter type used on field ${field}`);\n }\n });\n }\n },\n documentTypes: types,\n pageSize,\n };\n };\n\n setTranslator(translator: LunrQueryTranslator) {\n this.translator = translator;\n }\n\n async getIndexer(type: string) {\n const indexer = new LunrSearchEngineIndexer();\n\n indexer.on('close', () => {\n // Once the stream is closed, build the index and store the documents in\n // memory for later retrieval.\n this.lunrIndices[type] = indexer.buildIndex();\n this.docStore = { ...this.docStore, ...indexer.getDocumentStore() };\n });\n\n return indexer;\n }\n\n async query(query: SearchQuery): Promise<SearchResultSet> {\n const { lunrQueryBuilder, documentTypes, pageSize } = this.translator(\n query,\n ) as ConcreteLunrQuery;\n\n const results: LunrResultEnvelope[] = [];\n\n // Iterate over the filtered list of this.lunrIndex keys.\n Object.keys(this.lunrIndices)\n .filter(type => !documentTypes || documentTypes.includes(type))\n .forEach(type => {\n try {\n results.push(\n ...this.lunrIndices[type].query(lunrQueryBuilder).map(result => {\n return {\n result: result,\n type: type,\n };\n }),\n );\n } catch (err) {\n // if a field does not exist on a index, we can see that as a no-match\n if (\n err instanceof Error &&\n err.message.startsWith('unrecognised field')\n ) {\n return;\n }\n throw err;\n }\n });\n\n // Sort results.\n results.sort((doc1, doc2) => {\n return doc2.result.score - doc1.result.score;\n });\n\n // Perform paging\n const { page } = decodePageCursor(query.pageCursor);\n const offset = page * pageSize;\n const hasPreviousPage = page > 0;\n const hasNextPage = results.length > offset + pageSize;\n const nextPageCursor = hasNextPage\n ? encodePageCursor({ page: page + 1 })\n : undefined;\n const previousPageCursor = hasPreviousPage\n ? encodePageCursor({ page: page - 1 })\n : undefined;\n\n // Translate results into SearchResultSet\n const realResultSet: SearchResultSet = {\n results: results.slice(offset, offset + pageSize).map(d => {\n return { type: d.type, document: this.docStore[d.result.ref] };\n }),\n nextPageCursor,\n previousPageCursor,\n };\n\n return realResultSet;\n }\n}\n\nexport function decodePageCursor(pageCursor?: string): { page: number } {\n if (!pageCursor) {\n return { page: 0 };\n }\n\n return {\n page: Number(Buffer.from(pageCursor, 'base64').toString('utf-8')),\n };\n}\n\nexport function encodePageCursor({ page }: { page: number }): string {\n return Buffer.from(`${page}`, 'utf-8').toString('base64');\n}\n","/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { IndexableDocument } from '@backstage/plugin-search-common';\nimport { pipeline, Readable, Transform, Writable } from 'stream';\n\n/**\n * Object resolved after a test pipeline is executed.\n * @beta\n */\nexport type TestPipelineResult = {\n /**\n * If an error was emitted by the pipeline, it will be set here.\n */\n error: unknown;\n\n /**\n * A list of documents collected at the end of the pipeline. If the subject\n * under test is an indexer, this will be an empty array (because your\n * indexer should have received the documents instead).\n */\n documents: IndexableDocument[];\n};\n\n/**\n * Test utility for Backstage Search collators, decorators, and indexers.\n * @beta\n */\nexport class TestPipeline {\n private collator?: Readable;\n private decorator?: Transform;\n private indexer?: Writable;\n\n private constructor({\n collator,\n decorator,\n indexer,\n }: {\n collator?: Readable;\n decorator?: Transform;\n indexer?: Writable;\n }) {\n this.collator = collator;\n this.decorator = decorator;\n this.indexer = indexer;\n }\n\n /**\n * Provide the collator, decorator, or indexer to be tested.\n */\n static withSubject(subject: Readable | Transform | Writable) {\n if (subject instanceof Transform) {\n return new TestPipeline({ decorator: subject });\n }\n\n if (subject instanceof Readable) {\n return new TestPipeline({ collator: subject });\n }\n\n if (subject instanceof Writable) {\n return new TestPipeline({ indexer: subject });\n }\n\n throw new Error(\n 'Unknown test subject: are you passing a readable, writable, or transform stream?',\n );\n }\n\n /**\n * Provide documents for testing decorators and indexers.\n */\n withDocuments(documents: IndexableDocument[]): TestPipeline {\n if (this.collator) {\n throw new Error('Cannot provide documents when testing a collator.');\n }\n\n // Set a naive readable stream that just pushes all given documents.\n this.collator = new Readable({ objectMode: true });\n this.collator._read = () => {};\n process.nextTick(() => {\n documents.forEach(document => {\n this.collator!.push(document);\n });\n this.collator!.push(null);\n });\n\n return this;\n }\n\n /**\n * Execute the test pipeline so that you can make assertions about the result\n * or behavior of the given test subject.\n */\n async execute(): Promise<TestPipelineResult> {\n const documents: IndexableDocument[] = [];\n if (!this.collator) {\n throw new Error(\n 'Cannot execute pipeline without a collator or documents',\n );\n }\n\n // If we are here and there is no indexer, we are testing a collator or a\n // decorator. Set up a naive writable that captures documents in memory.\n if (!this.indexer) {\n this.indexer = new Writable({ objectMode: true });\n this.indexer._write = (document: IndexableDocument, _, done) => {\n documents.push(document);\n done();\n };\n }\n\n return new Promise<TestPipelineResult>(done => {\n const pipes: (Readable | Transform | Writable)[] = [this.collator!];\n if (this.decorator) {\n pipes.push(this.decorator);\n }\n pipes.push(this.indexer!);\n\n pipeline(pipes, error => {\n done({\n error,\n documents,\n });\n });\n });\n }\n}\n"],"names":["Writable","Transform","lunr","Readable"],"mappings":"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"}
@@ -0,0 +1,244 @@
1
+ /// <reference types="node" />
2
+ import { SearchEngine, DocumentCollatorFactory, DocumentDecoratorFactory, DocumentTypeInfo, IndexableDocument, SearchQuery, QueryTranslator, SearchResultSet } from '@backstage/plugin-search-common';
3
+ export { SearchEngine } from '@backstage/plugin-search-common';
4
+ import { Logger } from 'winston';
5
+ import lunr from 'lunr';
6
+ import { Writable, Transform, Readable } from 'stream';
7
+
8
+ /**
9
+ * @beta
10
+ */
11
+ declare type IndexBuilderOptions = {
12
+ searchEngine: SearchEngine;
13
+ logger: Logger;
14
+ };
15
+ /**
16
+ * Parameters required to register a collator.
17
+ * @beta
18
+ */
19
+ interface RegisterCollatorParameters {
20
+ /**
21
+ * The default interval (in seconds) that the provided collator will be called (can be overridden in config).
22
+ */
23
+ defaultRefreshIntervalSeconds: number;
24
+ /**
25
+ * The class responsible for returning the document collator of the given type.
26
+ */
27
+ factory: DocumentCollatorFactory;
28
+ }
29
+ /**
30
+ * Parameters required to register a decorator
31
+ * @beta
32
+ */
33
+ interface RegisterDecoratorParameters {
34
+ /**
35
+ * The class responsible for returning the decorator which appends, modifies, or filters documents.
36
+ */
37
+ factory: DocumentDecoratorFactory;
38
+ }
39
+
40
+ /**
41
+ * @beta
42
+ */
43
+ declare class IndexBuilder {
44
+ private collators;
45
+ private decorators;
46
+ private documentTypes;
47
+ private searchEngine;
48
+ private logger;
49
+ constructor({ logger, searchEngine }: IndexBuilderOptions);
50
+ getSearchEngine(): SearchEngine;
51
+ getDocumentTypes(): Record<string, DocumentTypeInfo>;
52
+ /**
53
+ * Makes the index builder aware of a collator that should be executed at the
54
+ * given refresh interval.
55
+ */
56
+ addCollator({ factory, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void;
57
+ /**
58
+ * Makes the index builder aware of a decorator. If no types are provided on
59
+ * the decorator, it will be applied to documents from all known collators,
60
+ * otherwise it will only be applied to documents of the given types.
61
+ */
62
+ addDecorator({ factory }: RegisterDecoratorParameters): void;
63
+ /**
64
+ * Compiles collators and decorators into tasks, which are added to a
65
+ * scheduler returned to the caller.
66
+ */
67
+ build(): Promise<{
68
+ scheduler: Scheduler;
69
+ }>;
70
+ }
71
+
72
+ /**
73
+ * TODO: coordination, error handling
74
+ */
75
+ /**
76
+ * @beta
77
+ */
78
+ declare class Scheduler {
79
+ private logger;
80
+ private schedule;
81
+ private runningTasks;
82
+ constructor({ logger }: {
83
+ logger: Logger;
84
+ });
85
+ /**
86
+ * Adds each task and interval to the schedule.
87
+ * When running the tasks, the scheduler waits at least for the time specified
88
+ * in the interval once the task was completed, before running it again.
89
+ */
90
+ addToSchedule(task: Function, interval: number): void;
91
+ /**
92
+ * Starts the scheduling process for each task
93
+ */
94
+ start(): void;
95
+ /**
96
+ * Stop all scheduled tasks.
97
+ */
98
+ stop(): void;
99
+ }
100
+
101
+ /**
102
+ * @beta
103
+ */
104
+ declare type BatchSearchEngineOptions = {
105
+ batchSize: number;
106
+ };
107
+ /**
108
+ * Base class encapsulating batch-based stream processing. Useful as a base
109
+ * class for search engine indexers.
110
+ * @beta
111
+ */
112
+ declare abstract class BatchSearchEngineIndexer extends Writable {
113
+ private batchSize;
114
+ private currentBatch;
115
+ private initialized;
116
+ constructor(options: BatchSearchEngineOptions);
117
+ /**
118
+ * Receives an array of indexable documents (of size this.batchSize) which
119
+ * should be written to the search engine. This method won't be called again
120
+ * at least until it resolves.
121
+ */
122
+ abstract index(documents: IndexableDocument[]): Promise<void>;
123
+ /**
124
+ * Any asynchronous setup tasks can be performed here.
125
+ */
126
+ abstract initialize(): Promise<void>;
127
+ /**
128
+ * Any asynchronous teardown tasks can be performed here.
129
+ */
130
+ abstract finalize(): Promise<void>;
131
+ }
132
+
133
+ /**
134
+ * Base class encapsulating simple async transformations. Useful as a base
135
+ * class for Backstage search decorators.
136
+ * @beta
137
+ */
138
+ declare abstract class DecoratorBase extends Transform {
139
+ private initialized;
140
+ constructor();
141
+ /**
142
+ * Any asynchronous setup tasks can be performed here.
143
+ */
144
+ abstract initialize(): Promise<void>;
145
+ /**
146
+ * Receives a single indexable document. In your decorate method, you can:
147
+ *
148
+ * - Resolve `undefined` to indicate the record should be omitted.
149
+ * - Resolve a single modified document, which could contain new fields,
150
+ * edited fields, or removed fields.
151
+ * - Resolve an array of indexable documents, if the purpose if the decorator
152
+ * is to convert one document into multiple derivative documents.
153
+ */
154
+ abstract decorate(document: IndexableDocument): Promise<IndexableDocument | IndexableDocument[] | undefined>;
155
+ /**
156
+ * Any asynchronous teardown tasks can be performed here.
157
+ */
158
+ abstract finalize(): Promise<void>;
159
+ }
160
+
161
+ /**
162
+ * @beta
163
+ */
164
+ declare class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
165
+ private schemaInitialized;
166
+ private builder;
167
+ private docStore;
168
+ constructor();
169
+ initialize(): Promise<void>;
170
+ finalize(): Promise<void>;
171
+ index(documents: IndexableDocument[]): Promise<void>;
172
+ buildIndex(): lunr.Index;
173
+ getDocumentStore(): Record<string, IndexableDocument>;
174
+ }
175
+
176
+ /**
177
+ * @beta
178
+ */
179
+ declare type ConcreteLunrQuery = {
180
+ lunrQueryBuilder: lunr.Index.QueryBuilder;
181
+ documentTypes?: string[];
182
+ pageSize: number;
183
+ };
184
+ /**
185
+ * @beta
186
+ */
187
+ declare type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery;
188
+ /**
189
+ * @beta
190
+ */
191
+ declare class LunrSearchEngine implements SearchEngine {
192
+ protected lunrIndices: Record<string, lunr.Index>;
193
+ protected docStore: Record<string, IndexableDocument>;
194
+ protected logger: Logger;
195
+ constructor({ logger }: {
196
+ logger: Logger;
197
+ });
198
+ protected translator: QueryTranslator;
199
+ setTranslator(translator: LunrQueryTranslator): void;
200
+ getIndexer(type: string): Promise<LunrSearchEngineIndexer>;
201
+ query(query: SearchQuery): Promise<SearchResultSet>;
202
+ }
203
+
204
+ /**
205
+ * Object resolved after a test pipeline is executed.
206
+ * @beta
207
+ */
208
+ declare type TestPipelineResult = {
209
+ /**
210
+ * If an error was emitted by the pipeline, it will be set here.
211
+ */
212
+ error: unknown;
213
+ /**
214
+ * A list of documents collected at the end of the pipeline. If the subject
215
+ * under test is an indexer, this will be an empty array (because your
216
+ * indexer should have received the documents instead).
217
+ */
218
+ documents: IndexableDocument[];
219
+ };
220
+ /**
221
+ * Test utility for Backstage Search collators, decorators, and indexers.
222
+ * @beta
223
+ */
224
+ declare class TestPipeline {
225
+ private collator?;
226
+ private decorator?;
227
+ private indexer?;
228
+ private constructor();
229
+ /**
230
+ * Provide the collator, decorator, or indexer to be tested.
231
+ */
232
+ static withSubject(subject: Readable | Transform | Writable): TestPipeline;
233
+ /**
234
+ * Provide documents for testing decorators and indexers.
235
+ */
236
+ withDocuments(documents: IndexableDocument[]): TestPipeline;
237
+ /**
238
+ * Execute the test pipeline so that you can make assertions about the result
239
+ * or behavior of the given test subject.
240
+ */
241
+ execute(): Promise<TestPipelineResult>;
242
+ }
243
+
244
+ export { BatchSearchEngineIndexer, BatchSearchEngineOptions, ConcreteLunrQuery, DecoratorBase, IndexBuilder, IndexBuilderOptions, LunrQueryTranslator, LunrSearchEngine, LunrSearchEngineIndexer, RegisterCollatorParameters, RegisterDecoratorParameters, Scheduler, TestPipeline, TestPipelineResult };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@backstage/plugin-search-backend-node",
3
3
  "description": "A library for Backstage backend plugins that want to interact with the search backend plugin",
4
- "version": "0.4.6",
4
+ "version": "0.5.1-next.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "Apache-2.0",
@@ -23,17 +23,19 @@
23
23
  "clean": "backstage-cli package clean"
24
24
  },
25
25
  "dependencies": {
26
- "@backstage/search-common": "^0.2.3",
26
+ "@backstage/errors": "^0.2.2",
27
+ "@backstage/plugin-search-common": "^0.3.1-next.0",
27
28
  "@types/lunr": "^2.3.3",
29
+ "lodash": "^4.17.21",
28
30
  "lunr": "^2.3.9",
29
31
  "winston": "^3.2.1"
30
32
  },
31
33
  "devDependencies": {
32
- "@backstage/backend-common": "^0.10.8",
33
- "@backstage/cli": "^0.14.0"
34
+ "@backstage/backend-common": "^0.13.0-next.0",
35
+ "@backstage/cli": "^0.15.2-next.0"
34
36
  },
35
37
  "files": [
36
38
  "dist"
37
39
  ],
38
- "gitHead": "4805c3d13ce9bfc369e53c271b1b95e722b3b4dc"
40
+ "gitHead": "e90d3ed129ebfce978f1adfa40c1dc2cef3f7e65"
39
41
  }