@backstage/plugin-search-backend-node 0.3.0 → 0.4.3
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 +101 -0
- package/dist/index.cjs.js +31 -9
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +5 -27
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,106 @@
|
|
|
1
1
|
# @backstage/plugin-search-backend-node
|
|
2
2
|
|
|
3
|
+
## 0.4.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a369f19e7e: Handle special case when filter array has single value optimizing Lunr search behaviour.
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @backstage/search-common@0.2.1
|
|
10
|
+
|
|
11
|
+
## 0.4.2
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- a13f21cdc: Implement optional `pageCursor` based paging in search.
|
|
16
|
+
|
|
17
|
+
To use paging in your app, add a `<SearchResultPager />` to your
|
|
18
|
+
`SearchPage.tsx`.
|
|
19
|
+
|
|
20
|
+
- Updated dependencies
|
|
21
|
+
- @backstage/search-common@0.2.0
|
|
22
|
+
|
|
23
|
+
## 0.4.1
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- 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.
|
|
28
|
+
|
|
29
|
+
## Example configurations:
|
|
30
|
+
|
|
31
|
+
### AWS
|
|
32
|
+
|
|
33
|
+
Using AWS hosted ElasticSearch the only configuration options needed is the URL to the ElasticSearch service. The implementation assumes
|
|
34
|
+
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).
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
search:
|
|
38
|
+
elasticsearch:
|
|
39
|
+
provider: aws
|
|
40
|
+
node: https://my-backstage-search-asdfqwerty.eu-west-1.es.amazonaws.com
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Elastic.co
|
|
44
|
+
|
|
45
|
+
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)
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
search:
|
|
49
|
+
elasticsearch:
|
|
50
|
+
provider: elastic
|
|
51
|
+
cloudId: backstage-elastic:asdfqwertyasdfqwertyasdfqwertyasdfqwerty==
|
|
52
|
+
auth:
|
|
53
|
+
username: elastic
|
|
54
|
+
password: changeme
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Others
|
|
58
|
+
|
|
59
|
+
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)
|
|
60
|
+
|
|
61
|
+
#### Configuration examples
|
|
62
|
+
|
|
63
|
+
##### With username and password
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
search:
|
|
67
|
+
elasticsearch:
|
|
68
|
+
node: http://localhost:9200
|
|
69
|
+
auth:
|
|
70
|
+
username: elastic
|
|
71
|
+
password: changeme
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
##### With bearer token
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
search:
|
|
78
|
+
elasticsearch:
|
|
79
|
+
node: http://localhost:9200
|
|
80
|
+
auth:
|
|
81
|
+
bearer: token
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
##### With API key
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
search:
|
|
88
|
+
elasticsearch:
|
|
89
|
+
node: http://localhost:9200
|
|
90
|
+
auth:
|
|
91
|
+
apiKey: base64EncodedKey
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- Updated dependencies
|
|
95
|
+
- @backstage/search-common@0.1.3
|
|
96
|
+
|
|
97
|
+
## 0.4.0
|
|
98
|
+
|
|
99
|
+
### Minor Changes
|
|
100
|
+
|
|
101
|
+
- 97b2eb37b: Change return value of `SearchEngine.index` to `Promise<void>` to support
|
|
102
|
+
implementation of external search engines.
|
|
103
|
+
|
|
3
104
|
## 0.3.0
|
|
4
105
|
|
|
5
106
|
### Minor Changes
|
package/dist/index.cjs.js
CHANGED
|
@@ -65,7 +65,7 @@ class IndexBuilder {
|
|
|
65
65
|
this.logger.debug(`No documents for type "${type}" to index`);
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
this.searchEngine.index(type, documents);
|
|
68
|
+
await this.searchEngine.index(type, documents);
|
|
69
69
|
}, this.collators[type].refreshInterval * 1e3);
|
|
70
70
|
});
|
|
71
71
|
return {
|
|
@@ -134,6 +134,7 @@ class LunrSearchEngine {
|
|
|
134
134
|
filters,
|
|
135
135
|
types
|
|
136
136
|
}) => {
|
|
137
|
+
const pageSize = 25;
|
|
137
138
|
return {
|
|
138
139
|
lunrQueryBuilder: (q) => {
|
|
139
140
|
const termToken = lunr__default['default'].tokenizer(term);
|
|
@@ -152,10 +153,11 @@ class LunrSearchEngine {
|
|
|
152
153
|
boost: 1
|
|
153
154
|
});
|
|
154
155
|
if (filters) {
|
|
155
|
-
Object.entries(filters).forEach(([field,
|
|
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
162
|
q.term(lunr__default['default'].tokenizer(value == null ? void 0 : value.toString()), {
|
|
161
163
|
presence: lunr__default['default'].Query.presence.REQUIRED,
|
|
@@ -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;
|
|
@@ -182,7 +185,7 @@ class LunrSearchEngine {
|
|
|
182
185
|
setTranslator(translator) {
|
|
183
186
|
this.translator = translator;
|
|
184
187
|
}
|
|
185
|
-
index(type, documents) {
|
|
188
|
+
async index(type, documents) {
|
|
186
189
|
const lunrBuilder = new lunr__default['default'].Builder();
|
|
187
190
|
lunrBuilder.pipeline.add(lunr__default['default'].trimmer, lunr__default['default'].stopWordFilter, lunr__default['default'].stemmer);
|
|
188
191
|
lunrBuilder.searchPipeline.add(lunr__default['default'].stemmer);
|
|
@@ -196,8 +199,8 @@ class LunrSearchEngine {
|
|
|
196
199
|
});
|
|
197
200
|
this.lunrIndices[type] = lunrBuilder.build();
|
|
198
201
|
}
|
|
199
|
-
query(query) {
|
|
200
|
-
const {lunrQueryBuilder, documentTypes} = this.translator(query);
|
|
202
|
+
async query(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) => {
|
|
230
|
+
results: results.slice(offset, offset + pageSize).map((d) => {
|
|
222
231
|
return {type: d.type, document: this.docStore[d.result.ref]};
|
|
223
|
-
})
|
|
232
|
+
}),
|
|
233
|
+
nextPageCursor,
|
|
234
|
+
previousPageCursor
|
|
224
235
|
};
|
|
225
|
-
return
|
|
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;
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -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 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 SearchQuery,\n IndexableDocument,\n SearchResultSet,\n} from '@backstage/search-common';\nimport lunr from 'lunr';\nimport { Logger } from 'winston';\nimport { SearchEngine, QueryTranslator } 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 index(type: string, documents: IndexableDocument[]): 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 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 Promise.resolve(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[]): 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 {
|
|
@@ -120,8 +98,8 @@ declare class LunrSearchEngine implements SearchEngine {
|
|
|
120
98
|
});
|
|
121
99
|
protected translator: QueryTranslator;
|
|
122
100
|
setTranslator(translator: LunrQueryTranslator): void;
|
|
123
|
-
index(type: string, documents: IndexableDocument[]): void
|
|
101
|
+
index(type: string, documents: IndexableDocument[]): Promise<void>;
|
|
124
102
|
query(query: SearchQuery): Promise<SearchResultSet>;
|
|
125
103
|
}
|
|
126
104
|
|
|
127
|
-
export { IndexBuilder, LunrSearchEngine, Scheduler
|
|
105
|
+
export { IndexBuilder, LunrSearchEngine, Scheduler };
|
package/package.json
CHANGED
|
@@ -1,6 +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.3",
|
|
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
|
|
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
|
|
29
|
-
"@backstage/cli": "^0.
|
|
29
|
+
"@backstage/backend-common": "^0.9.8",
|
|
30
|
+
"@backstage/cli": "^0.8.1"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"dist"
|
|
33
34
|
],
|
|
34
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "3db0cb3683d3000666802af90a465ba4fb0d1e8d"
|
|
35
36
|
}
|