@backstage/plugin-search-backend-node 0.4.0 → 0.4.4

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,105 @@
1
1
  # @backstage/plugin-search-backend-node
2
2
 
3
+ ## 0.4.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 5333451def: Cleaned up API exports
8
+
9
+ ## 0.4.3
10
+
11
+ ### Patch Changes
12
+
13
+ - a369f19e7e: Handle special case when filter array has single value optimizing Lunr search behaviour.
14
+ - Updated dependencies
15
+ - @backstage/search-common@0.2.1
16
+
17
+ ## 0.4.2
18
+
19
+ ### Patch Changes
20
+
21
+ - a13f21cdc: Implement optional `pageCursor` based paging in search.
22
+
23
+ To use paging in your app, add a `<SearchResultPager />` to your
24
+ `SearchPage.tsx`.
25
+
26
+ - Updated dependencies
27
+ - @backstage/search-common@0.2.0
28
+
29
+ ## 0.4.1
30
+
31
+ ### Patch Changes
32
+
33
+ - d9c13d535: Implements configuration and indexing functionality for ElasticSearch search engine. Adds indexing, searching and default translator for ElasticSearch and modifies default backend example-app to use ES if it is configured.
34
+
35
+ ## Example configurations:
36
+
37
+ ### AWS
38
+
39
+ Using AWS hosted ElasticSearch the only configuration options needed is the URL to the ElasticSearch service. The implementation assumes
40
+ that environment variables for AWS access key id and secret access key are defined in accordance to the [default AWS credential chain.](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html).
41
+
42
+ ```yaml
43
+ search:
44
+ elasticsearch:
45
+ provider: aws
46
+ node: https://my-backstage-search-asdfqwerty.eu-west-1.es.amazonaws.com
47
+ ```
48
+
49
+ ### Elastic.co
50
+
51
+ Elastic Cloud hosted ElasticSearch uses a Cloud ID to determine the instance of hosted ElasticSearch to connect to. Additionally, username and password needs to be provided either directly or using environment variables like defined in [Backstage documentation.](https://backstage.io/docs/conf/writing#includes-and-dynamic-data)
52
+
53
+ ```yaml
54
+ search:
55
+ elasticsearch:
56
+ provider: elastic
57
+ cloudId: backstage-elastic:asdfqwertyasdfqwertyasdfqwertyasdfqwerty==
58
+ auth:
59
+ username: elastic
60
+ password: changeme
61
+ ```
62
+
63
+ ### Others
64
+
65
+ Other ElasticSearch instances can be connected to by using standard ElasticSearch authentication methods and exposed URL, provided that the cluster supports that. The configuration options needed are the URL to the node and authentication information. Authentication can be handled by either providing username/password or and API key or a bearer token. In case both username/password combination and one of the tokens are provided, token takes precedence. For more information how to create an API key, see [Elastic documentation on API keys](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html) and how to create a bearer token, see [Elastic documentation on tokens.](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html)
66
+
67
+ #### Configuration examples
68
+
69
+ ##### With username and password
70
+
71
+ ```yaml
72
+ search:
73
+ elasticsearch:
74
+ node: http://localhost:9200
75
+ auth:
76
+ username: elastic
77
+ password: changeme
78
+ ```
79
+
80
+ ##### With bearer token
81
+
82
+ ```yaml
83
+ search:
84
+ elasticsearch:
85
+ node: http://localhost:9200
86
+ auth:
87
+ bearer: token
88
+ ```
89
+
90
+ ##### With API key
91
+
92
+ ```yaml
93
+ search:
94
+ elasticsearch:
95
+ node: http://localhost:9200
96
+ auth:
97
+ apiKey: base64EncodedKey
98
+ ```
99
+
100
+ - Updated dependencies
101
+ - @backstage/search-common@0.1.3
102
+
3
103
  ## 0.4.0
4
104
 
5
105
  ### Minor Changes
package/dist/index.cjs.js CHANGED
@@ -9,7 +9,7 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
9
9
  var lunr__default = /*#__PURE__*/_interopDefaultLegacy(lunr);
10
10
 
11
11
  class IndexBuilder {
12
- constructor({logger, searchEngine}) {
12
+ constructor({ logger, searchEngine }) {
13
13
  this.collators = {};
14
14
  this.decorators = {};
15
15
  this.logger = logger;
@@ -28,7 +28,7 @@ class IndexBuilder {
28
28
  collate: collator
29
29
  };
30
30
  }
31
- addDecorator({decorator}) {
31
+ addDecorator({ decorator }) {
32
32
  const types = decorator.types || ["*"];
33
33
  this.logger.info(`Added decorator ${decorator.constructor.name} to types ${types.join(", ")}`);
34
34
  types.forEach((type) => {
@@ -40,7 +40,7 @@ class IndexBuilder {
40
40
  });
41
41
  }
42
42
  async build() {
43
- const scheduler = new Scheduler({logger: this.logger});
43
+ const scheduler = new Scheduler({ logger: this.logger });
44
44
  Object.keys(this.collators).forEach((type) => {
45
45
  scheduler.addToSchedule(async () => {
46
46
  const decorators = (this.decorators["*"] || []).concat(this.decorators[type] || []);
@@ -100,7 +100,7 @@ function runPeriodically(fn, delayMs) {
100
100
  }
101
101
 
102
102
  class Scheduler {
103
- constructor({logger}) {
103
+ constructor({ logger }) {
104
104
  this.runningTasks = [];
105
105
  this.logger = logger;
106
106
  this.schedule = [];
@@ -109,11 +109,11 @@ class Scheduler {
109
109
  if (this.runningTasks.length) {
110
110
  throw new Error("Cannot add task to schedule that has already been started.");
111
111
  }
112
- this.schedule.push({task, interval});
112
+ this.schedule.push({ task, interval });
113
113
  }
114
114
  start() {
115
115
  this.logger.info("Starting all scheduled search tasks.");
116
- this.schedule.forEach(({task, interval}) => {
116
+ this.schedule.forEach(({ task, interval }) => {
117
117
  this.runningTasks.push(runPeriodically(() => task(), interval));
118
118
  });
119
119
  }
@@ -127,16 +127,17 @@ class Scheduler {
127
127
  }
128
128
 
129
129
  class LunrSearchEngine {
130
- constructor({logger}) {
130
+ constructor({ logger }) {
131
131
  this.lunrIndices = {};
132
132
  this.translator = ({
133
133
  term,
134
134
  filters,
135
135
  types
136
136
  }) => {
137
+ const pageSize = 25;
137
138
  return {
138
139
  lunrQueryBuilder: (q) => {
139
- const termToken = lunr__default['default'].tokenizer(term);
140
+ const termToken = lunr__default["default"].tokenizer(term);
140
141
  q.term(termToken, {
141
142
  usePipeline: true,
142
143
  boost: 100
@@ -144,7 +145,7 @@ class LunrSearchEngine {
144
145
  q.term(termToken, {
145
146
  usePipeline: false,
146
147
  boost: 10,
147
- wildcard: lunr__default['default'].Query.wildcard.TRAILING
148
+ wildcard: lunr__default["default"].Query.wildcard.TRAILING
148
149
  });
149
150
  q.term(termToken, {
150
151
  usePipeline: false,
@@ -152,19 +153,20 @@ class LunrSearchEngine {
152
153
  boost: 1
153
154
  });
154
155
  if (filters) {
155
- Object.entries(filters).forEach(([field, value]) => {
156
+ Object.entries(filters).forEach(([field, fieldValue]) => {
156
157
  if (!q.allFields.includes(field)) {
157
158
  throw new Error(`unrecognised field ${field}`);
158
159
  }
160
+ const value = Array.isArray(fieldValue) && fieldValue.length === 1 ? fieldValue[0] : fieldValue;
159
161
  if (["string", "number", "boolean"].includes(typeof value)) {
160
- q.term(lunr__default['default'].tokenizer(value == null ? void 0 : value.toString()), {
161
- presence: lunr__default['default'].Query.presence.REQUIRED,
162
+ q.term(lunr__default["default"].tokenizer(value == null ? void 0 : value.toString()), {
163
+ presence: lunr__default["default"].Query.presence.REQUIRED,
162
164
  fields: [field]
163
165
  });
164
166
  } else if (Array.isArray(value)) {
165
167
  this.logger.warn(`Non-scalar filter value used for field ${field}. Consider using a different Search Engine for better results.`);
166
- q.term(lunr__default['default'].tokenizer(value), {
167
- presence: lunr__default['default'].Query.presence.OPTIONAL,
168
+ q.term(lunr__default["default"].tokenizer(value), {
169
+ presence: lunr__default["default"].Query.presence.OPTIONAL,
168
170
  fields: [field]
169
171
  });
170
172
  } else {
@@ -173,7 +175,8 @@ class LunrSearchEngine {
173
175
  });
174
176
  }
175
177
  },
176
- documentTypes: types
178
+ documentTypes: types,
179
+ pageSize
177
180
  };
178
181
  };
179
182
  this.logger = logger;
@@ -183,9 +186,9 @@ class LunrSearchEngine {
183
186
  this.translator = translator;
184
187
  }
185
188
  async index(type, documents) {
186
- const lunrBuilder = new lunr__default['default'].Builder();
187
- lunrBuilder.pipeline.add(lunr__default['default'].trimmer, lunr__default['default'].stopWordFilter, lunr__default['default'].stemmer);
188
- lunrBuilder.searchPipeline.add(lunr__default['default'].stemmer);
189
+ const lunrBuilder = new lunr__default["default"].Builder();
190
+ lunrBuilder.pipeline.add(lunr__default["default"].trimmer, lunr__default["default"].stopWordFilter, lunr__default["default"].stemmer);
191
+ lunrBuilder.searchPipeline.add(lunr__default["default"].stemmer);
189
192
  Object.keys(documents[0]).forEach((field) => {
190
193
  lunrBuilder.field(field);
191
194
  });
@@ -197,7 +200,7 @@ class LunrSearchEngine {
197
200
  this.lunrIndices[type] = lunrBuilder.build();
198
201
  }
199
202
  async query(query) {
200
- const {lunrQueryBuilder, documentTypes} = this.translator(query);
203
+ const { lunrQueryBuilder, documentTypes, pageSize } = this.translator(query);
201
204
  const results = [];
202
205
  Object.keys(this.lunrIndices).filter((type) => !documentTypes || documentTypes.includes(type)).forEach((type) => {
203
206
  try {
@@ -217,14 +220,33 @@ class LunrSearchEngine {
217
220
  results.sort((doc1, doc2) => {
218
221
  return doc2.result.score - doc1.result.score;
219
222
  });
223
+ const { page } = decodePageCursor(query.pageCursor);
224
+ const offset = page * pageSize;
225
+ const hasPreviousPage = page > 0;
226
+ const hasNextPage = results.length > offset + pageSize;
227
+ const nextPageCursor = hasNextPage ? encodePageCursor({ page: page + 1 }) : void 0;
228
+ const previousPageCursor = hasPreviousPage ? encodePageCursor({ page: page - 1 }) : void 0;
220
229
  const realResultSet = {
221
- results: results.map((d) => {
222
- return {type: d.type, document: this.docStore[d.result.ref]};
223
- })
230
+ results: results.slice(offset, offset + pageSize).map((d) => {
231
+ return { type: d.type, document: this.docStore[d.result.ref] };
232
+ }),
233
+ nextPageCursor,
234
+ previousPageCursor
224
235
  };
225
236
  return realResultSet;
226
237
  }
227
238
  }
239
+ function decodePageCursor(pageCursor) {
240
+ if (!pageCursor) {
241
+ return { page: 0 };
242
+ }
243
+ return {
244
+ page: Number(Buffer.from(pageCursor, "base64").toString("utf-8"))
245
+ };
246
+ }
247
+ function encodePageCursor({ page }) {
248
+ return Buffer.from(`${page}`, "utf-8").toString("base64");
249
+ }
228
250
 
229
251
  exports.IndexBuilder = IndexBuilder;
230
252
  exports.LunrSearchEngine = LunrSearchEngine;
@@ -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 IndexableDocument,\n} from '@backstage/search-common';\nimport { Logger } from 'winston';\nimport { Scheduler } from './index';\nimport {\n RegisterCollatorParameters,\n RegisterDecoratorParameters,\n SearchEngine,\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 searchEngine: SearchEngine;\n private logger: Logger;\n\n constructor({ logger, searchEngine }: IndexBuilderOptions) {\n this.collators = {};\n this.decorators = {};\n this.logger = logger;\n this.searchEngine = searchEngine;\n }\n\n getSearchEngine(): SearchEngine {\n return this.searchEngine;\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 }\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} from '@backstage/search-common';\nimport lunr from 'lunr';\nimport { Logger } from 'winston';\nimport { QueryTranslator, SearchEngine } from '../types';\n\nexport type ConcreteLunrQuery = {\n lunrQueryBuilder: lunr.Index.QueryBuilder;\n documentTypes?: string[];\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 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, value]) => {\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\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 };\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 } = 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 // Translate results into SearchResultSet\n const realResultSet: SearchResultSet = {\n results: results.map(d => {\n return { type: d.type, document: this.docStore[d.result.ref] };\n }),\n };\n\n return realResultSet;\n }\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/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 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 searchEngine: SearchEngine;\n private logger: Logger;\n\n constructor({ logger, searchEngine }: IndexBuilderOptions) {\n this.collators = {};\n this.decorators = {};\n this.logger = logger;\n this.searchEngine = searchEngine;\n }\n\n getSearchEngine(): SearchEngine {\n return this.searchEngine;\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 }\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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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { DocumentCollator, DocumentDecorator, SearchEngine, IndexableDocument, QueryTranslator, SearchQuery, SearchResultSet } from '@backstage/search-common';
2
+ export { SearchEngine } from '@backstage/search-common';
1
3
  import { Logger } from 'winston';
2
- import { IndexableDocument, SearchQuery, SearchResultSet, DocumentCollator, DocumentDecorator } from '@backstage/search-common';
3
4
  import lunr from 'lunr';
4
5
 
5
6
  /**
@@ -24,30 +25,6 @@ interface RegisterDecoratorParameters {
24
25
  */
25
26
  decorator: DocumentDecorator;
26
27
  }
27
- /**
28
- * A type of function responsible for translating an abstract search query into
29
- * a concrete query relevant to a particular search engine.
30
- */
31
- declare type QueryTranslator = (query: SearchQuery) => unknown;
32
- /**
33
- * Interface that must be implemented by specific search engines, responsible
34
- * for performing indexing and querying and translating abstract queries into
35
- * concrete, search engine-specific queries.
36
- */
37
- interface SearchEngine {
38
- /**
39
- * Override the default translator provided by the SearchEngine.
40
- */
41
- setTranslator(translator: QueryTranslator): void;
42
- /**
43
- * Add the given documents to the SearchEngine index of the given type.
44
- */
45
- index(type: string, documents: IndexableDocument[]): Promise<void>;
46
- /**
47
- * Perform a search query against the SearchEngine.
48
- */
49
- query(query: SearchQuery): Promise<SearchResultSet>;
50
- }
51
28
 
52
29
  declare type IndexBuilderOptions = {
53
30
  searchEngine: SearchEngine;
@@ -109,6 +86,7 @@ declare class Scheduler {
109
86
  declare type ConcreteLunrQuery = {
110
87
  lunrQueryBuilder: lunr.Index.QueryBuilder;
111
88
  documentTypes?: string[];
89
+ pageSize: number;
112
90
  };
113
91
  declare type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery;
114
92
  declare class LunrSearchEngine implements SearchEngine {
@@ -124,4 +102,4 @@ declare class LunrSearchEngine implements SearchEngine {
124
102
  query(query: SearchQuery): Promise<SearchResultSet>;
125
103
  }
126
104
 
127
- export { IndexBuilder, LunrSearchEngine, Scheduler, SearchEngine };
105
+ export { IndexBuilder, LunrSearchEngine, Scheduler };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@backstage/plugin-search-backend-node",
3
- "version": "0.4.0",
3
+ "description": "A library for Backstage backend plugins that want to interact with the search backend plugin",
4
+ "version": "0.4.4",
4
5
  "main": "dist/index.cjs.js",
5
6
  "types": "dist/index.d.ts",
6
7
  "license": "Apache-2.0",
@@ -19,17 +20,17 @@
19
20
  "clean": "backstage-cli clean"
20
21
  },
21
22
  "dependencies": {
22
- "@backstage/search-common": "^0.1.2",
23
+ "@backstage/search-common": "^0.2.1",
23
24
  "@types/lunr": "^2.3.3",
24
25
  "lunr": "^2.3.9",
25
26
  "winston": "^3.2.1"
26
27
  },
27
28
  "devDependencies": {
28
- "@backstage/backend-common": "^0.8.6",
29
- "@backstage/cli": "^0.7.4"
29
+ "@backstage/backend-common": "^0.10.3",
30
+ "@backstage/cli": "^0.11.0"
30
31
  },
31
32
  "files": [
32
33
  "dist"
33
34
  ],
34
- "gitHead": "ffae1bb6e47ca75858b142ec2721ecc61fe8e083"
35
+ "gitHead": "da66c61bdd63cdb3f0f0cd2e26dc9e6454d93c7b"
35
36
  }