@backstage/plugin-search-backend-node 0.4.1 → 0.4.5
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 +34 -0
- package/dist/index.cjs.js +51 -22
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @backstage/plugin-search-backend-node
|
|
2
2
|
|
|
3
|
+
## 0.4.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f6389e9e5d: Track visibility permissions by document type in IndexBuilder
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @backstage/search-common@0.2.2
|
|
10
|
+
|
|
11
|
+
## 0.4.4
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 5333451def: Cleaned up API exports
|
|
16
|
+
|
|
17
|
+
## 0.4.3
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- a369f19e7e: Handle special case when filter array has single value optimizing Lunr search behaviour.
|
|
22
|
+
- Updated dependencies
|
|
23
|
+
- @backstage/search-common@0.2.1
|
|
24
|
+
|
|
25
|
+
## 0.4.2
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- a13f21cdc: Implement optional `pageCursor` based paging in search.
|
|
30
|
+
|
|
31
|
+
To use paging in your app, add a `<SearchResultPager />` to your
|
|
32
|
+
`SearchPage.tsx`.
|
|
33
|
+
|
|
34
|
+
- Updated dependencies
|
|
35
|
+
- @backstage/search-common@0.2.0
|
|
36
|
+
|
|
3
37
|
## 0.4.1
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/dist/index.cjs.js
CHANGED
|
@@ -9,15 +9,19 @@ 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
|
+
this.documentTypes = {};
|
|
15
16
|
this.logger = logger;
|
|
16
17
|
this.searchEngine = searchEngine;
|
|
17
18
|
}
|
|
18
19
|
getSearchEngine() {
|
|
19
20
|
return this.searchEngine;
|
|
20
21
|
}
|
|
22
|
+
getDocumentTypes() {
|
|
23
|
+
return this.documentTypes;
|
|
24
|
+
}
|
|
21
25
|
addCollator({
|
|
22
26
|
collator,
|
|
23
27
|
defaultRefreshIntervalSeconds
|
|
@@ -27,8 +31,11 @@ class IndexBuilder {
|
|
|
27
31
|
refreshInterval: defaultRefreshIntervalSeconds,
|
|
28
32
|
collate: collator
|
|
29
33
|
};
|
|
34
|
+
this.documentTypes[collator.type] = {
|
|
35
|
+
visibilityPermission: collator.visibilityPermission
|
|
36
|
+
};
|
|
30
37
|
}
|
|
31
|
-
addDecorator({decorator}) {
|
|
38
|
+
addDecorator({ decorator }) {
|
|
32
39
|
const types = decorator.types || ["*"];
|
|
33
40
|
this.logger.info(`Added decorator ${decorator.constructor.name} to types ${types.join(", ")}`);
|
|
34
41
|
types.forEach((type) => {
|
|
@@ -40,7 +47,7 @@ class IndexBuilder {
|
|
|
40
47
|
});
|
|
41
48
|
}
|
|
42
49
|
async build() {
|
|
43
|
-
const scheduler = new Scheduler({logger: this.logger});
|
|
50
|
+
const scheduler = new Scheduler({ logger: this.logger });
|
|
44
51
|
Object.keys(this.collators).forEach((type) => {
|
|
45
52
|
scheduler.addToSchedule(async () => {
|
|
46
53
|
const decorators = (this.decorators["*"] || []).concat(this.decorators[type] || []);
|
|
@@ -100,7 +107,7 @@ function runPeriodically(fn, delayMs) {
|
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
class Scheduler {
|
|
103
|
-
constructor({logger}) {
|
|
110
|
+
constructor({ logger }) {
|
|
104
111
|
this.runningTasks = [];
|
|
105
112
|
this.logger = logger;
|
|
106
113
|
this.schedule = [];
|
|
@@ -109,11 +116,11 @@ class Scheduler {
|
|
|
109
116
|
if (this.runningTasks.length) {
|
|
110
117
|
throw new Error("Cannot add task to schedule that has already been started.");
|
|
111
118
|
}
|
|
112
|
-
this.schedule.push({task, interval});
|
|
119
|
+
this.schedule.push({ task, interval });
|
|
113
120
|
}
|
|
114
121
|
start() {
|
|
115
122
|
this.logger.info("Starting all scheduled search tasks.");
|
|
116
|
-
this.schedule.forEach(({task, interval}) => {
|
|
123
|
+
this.schedule.forEach(({ task, interval }) => {
|
|
117
124
|
this.runningTasks.push(runPeriodically(() => task(), interval));
|
|
118
125
|
});
|
|
119
126
|
}
|
|
@@ -127,16 +134,17 @@ class Scheduler {
|
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
class LunrSearchEngine {
|
|
130
|
-
constructor({logger}) {
|
|
137
|
+
constructor({ logger }) {
|
|
131
138
|
this.lunrIndices = {};
|
|
132
139
|
this.translator = ({
|
|
133
140
|
term,
|
|
134
141
|
filters,
|
|
135
142
|
types
|
|
136
143
|
}) => {
|
|
144
|
+
const pageSize = 25;
|
|
137
145
|
return {
|
|
138
146
|
lunrQueryBuilder: (q) => {
|
|
139
|
-
const termToken = lunr__default[
|
|
147
|
+
const termToken = lunr__default["default"].tokenizer(term);
|
|
140
148
|
q.term(termToken, {
|
|
141
149
|
usePipeline: true,
|
|
142
150
|
boost: 100
|
|
@@ -144,7 +152,7 @@ class LunrSearchEngine {
|
|
|
144
152
|
q.term(termToken, {
|
|
145
153
|
usePipeline: false,
|
|
146
154
|
boost: 10,
|
|
147
|
-
wildcard: lunr__default[
|
|
155
|
+
wildcard: lunr__default["default"].Query.wildcard.TRAILING
|
|
148
156
|
});
|
|
149
157
|
q.term(termToken, {
|
|
150
158
|
usePipeline: false,
|
|
@@ -152,19 +160,20 @@ class LunrSearchEngine {
|
|
|
152
160
|
boost: 1
|
|
153
161
|
});
|
|
154
162
|
if (filters) {
|
|
155
|
-
Object.entries(filters).forEach(([field,
|
|
163
|
+
Object.entries(filters).forEach(([field, fieldValue]) => {
|
|
156
164
|
if (!q.allFields.includes(field)) {
|
|
157
165
|
throw new Error(`unrecognised field ${field}`);
|
|
158
166
|
}
|
|
167
|
+
const value = Array.isArray(fieldValue) && fieldValue.length === 1 ? fieldValue[0] : fieldValue;
|
|
159
168
|
if (["string", "number", "boolean"].includes(typeof value)) {
|
|
160
|
-
q.term(lunr__default[
|
|
161
|
-
presence: lunr__default[
|
|
169
|
+
q.term(lunr__default["default"].tokenizer(value == null ? void 0 : value.toString()), {
|
|
170
|
+
presence: lunr__default["default"].Query.presence.REQUIRED,
|
|
162
171
|
fields: [field]
|
|
163
172
|
});
|
|
164
173
|
} else if (Array.isArray(value)) {
|
|
165
174
|
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[
|
|
167
|
-
presence: lunr__default[
|
|
175
|
+
q.term(lunr__default["default"].tokenizer(value), {
|
|
176
|
+
presence: lunr__default["default"].Query.presence.OPTIONAL,
|
|
168
177
|
fields: [field]
|
|
169
178
|
});
|
|
170
179
|
} else {
|
|
@@ -173,7 +182,8 @@ class LunrSearchEngine {
|
|
|
173
182
|
});
|
|
174
183
|
}
|
|
175
184
|
},
|
|
176
|
-
documentTypes: types
|
|
185
|
+
documentTypes: types,
|
|
186
|
+
pageSize
|
|
177
187
|
};
|
|
178
188
|
};
|
|
179
189
|
this.logger = logger;
|
|
@@ -183,9 +193,9 @@ class LunrSearchEngine {
|
|
|
183
193
|
this.translator = translator;
|
|
184
194
|
}
|
|
185
195
|
async index(type, documents) {
|
|
186
|
-
const lunrBuilder = new lunr__default[
|
|
187
|
-
lunrBuilder.pipeline.add(lunr__default[
|
|
188
|
-
lunrBuilder.searchPipeline.add(lunr__default[
|
|
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);
|
|
189
199
|
Object.keys(documents[0]).forEach((field) => {
|
|
190
200
|
lunrBuilder.field(field);
|
|
191
201
|
});
|
|
@@ -197,7 +207,7 @@ class LunrSearchEngine {
|
|
|
197
207
|
this.lunrIndices[type] = lunrBuilder.build();
|
|
198
208
|
}
|
|
199
209
|
async query(query) {
|
|
200
|
-
const {lunrQueryBuilder, documentTypes} = this.translator(query);
|
|
210
|
+
const { lunrQueryBuilder, documentTypes, pageSize } = this.translator(query);
|
|
201
211
|
const results = [];
|
|
202
212
|
Object.keys(this.lunrIndices).filter((type) => !documentTypes || documentTypes.includes(type)).forEach((type) => {
|
|
203
213
|
try {
|
|
@@ -217,14 +227,33 @@ class LunrSearchEngine {
|
|
|
217
227
|
results.sort((doc1, doc2) => {
|
|
218
228
|
return doc2.result.score - doc1.result.score;
|
|
219
229
|
});
|
|
230
|
+
const { page } = decodePageCursor(query.pageCursor);
|
|
231
|
+
const offset = page * pageSize;
|
|
232
|
+
const hasPreviousPage = page > 0;
|
|
233
|
+
const hasNextPage = results.length > offset + pageSize;
|
|
234
|
+
const nextPageCursor = hasNextPage ? encodePageCursor({ page: page + 1 }) : void 0;
|
|
235
|
+
const previousPageCursor = hasPreviousPage ? encodePageCursor({ page: page - 1 }) : void 0;
|
|
220
236
|
const realResultSet = {
|
|
221
|
-
results: results.map((d) => {
|
|
222
|
-
return {type: d.type, document: this.docStore[d.result.ref]};
|
|
223
|
-
})
|
|
237
|
+
results: results.slice(offset, offset + pageSize).map((d) => {
|
|
238
|
+
return { type: d.type, document: this.docStore[d.result.ref] };
|
|
239
|
+
}),
|
|
240
|
+
nextPageCursor,
|
|
241
|
+
previousPageCursor
|
|
224
242
|
};
|
|
225
243
|
return realResultSet;
|
|
226
244
|
}
|
|
227
245
|
}
|
|
246
|
+
function decodePageCursor(pageCursor) {
|
|
247
|
+
if (!pageCursor) {
|
|
248
|
+
return { page: 0 };
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
page: Number(Buffer.from(pageCursor, "base64").toString("utf-8"))
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function encodePageCursor({ page }) {
|
|
255
|
+
return Buffer.from(`${page}`, "utf-8").toString("base64");
|
|
256
|
+
}
|
|
228
257
|
|
|
229
258
|
exports.IndexBuilder = IndexBuilder;
|
|
230
259
|
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 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};\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 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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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DocumentCollator, DocumentDecorator, SearchEngine, IndexableDocument, QueryTranslator, SearchQuery, SearchResultSet } from '@backstage/search-common';
|
|
1
|
+
import { DocumentCollator, DocumentDecorator, SearchEngine, DocumentTypeInfo, IndexableDocument, QueryTranslator, SearchQuery, SearchResultSet } from '@backstage/search-common';
|
|
2
2
|
export { SearchEngine } from '@backstage/search-common';
|
|
3
3
|
import { Logger } from 'winston';
|
|
4
4
|
import lunr from 'lunr';
|
|
@@ -33,10 +33,12 @@ declare type IndexBuilderOptions = {
|
|
|
33
33
|
declare class IndexBuilder {
|
|
34
34
|
private collators;
|
|
35
35
|
private decorators;
|
|
36
|
+
private documentTypes;
|
|
36
37
|
private searchEngine;
|
|
37
38
|
private logger;
|
|
38
39
|
constructor({ logger, searchEngine }: IndexBuilderOptions);
|
|
39
40
|
getSearchEngine(): SearchEngine;
|
|
41
|
+
getDocumentTypes(): Record<string, DocumentTypeInfo>;
|
|
40
42
|
/**
|
|
41
43
|
* Makes the index builder aware of a collator that should be executed at the
|
|
42
44
|
* given refresh interval.
|
|
@@ -86,6 +88,7 @@ declare class Scheduler {
|
|
|
86
88
|
declare type ConcreteLunrQuery = {
|
|
87
89
|
lunrQueryBuilder: lunr.Index.QueryBuilder;
|
|
88
90
|
documentTypes?: string[];
|
|
91
|
+
pageSize: number;
|
|
89
92
|
};
|
|
90
93
|
declare type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery;
|
|
91
94
|
declare class LunrSearchEngine implements SearchEngine {
|
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.5",
|
|
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.
|
|
23
|
+
"@backstage/search-common": "^0.2.2",
|
|
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.
|
|
29
|
-
"@backstage/cli": "^0.
|
|
29
|
+
"@backstage/backend-common": "^0.10.5",
|
|
30
|
+
"@backstage/cli": "^0.13.0"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"dist"
|
|
33
34
|
],
|
|
34
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "493394603a2c47ea1d141159af9bc7bb84fac9e5"
|
|
35
36
|
}
|