@backstage/plugin-search-backend-module-elasticsearch 0.0.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/dist/index.cjs.js +114 -50
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +40 -4
- package/package.json +18 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @backstage/plugin-search-backend-module-elasticsearch
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 022507c860: **BREAKING**
|
|
8
|
+
|
|
9
|
+
The `ElasticSearchSearchEngine` implements the new stream-based indexing
|
|
10
|
+
process expected by the latest `@backstage/search-backend-node`.
|
|
11
|
+
|
|
12
|
+
When updating to this version, you must also update to the latest version of
|
|
13
|
+
`@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta)
|
|
14
|
+
for further details.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @backstage/plugin-search-backend-node@0.5.0
|
|
20
|
+
- @backstage/search-common@0.3.0
|
|
21
|
+
|
|
22
|
+
## 0.0.10
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Fix for the previous release with missing type declarations.
|
|
27
|
+
- Updated dependencies
|
|
28
|
+
- @backstage/config@0.1.15
|
|
29
|
+
- @backstage/search-common@0.2.4
|
|
30
|
+
|
|
31
|
+
## 0.0.9
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- c77c5c7eb6: Added `backstage.role` to `package.json`
|
|
36
|
+
- 4c0332e55c: chore(deps-dev): bump `@elastic/elasticsearch-mock` from 0.3.0 to 1.0.0
|
|
37
|
+
- Updated dependencies
|
|
38
|
+
- @backstage/config@0.1.14
|
|
39
|
+
- @backstage/search-common@0.2.3
|
|
40
|
+
|
|
3
41
|
## 0.0.8
|
|
4
42
|
|
|
5
43
|
### Patch Changes
|
package/dist/index.cjs.js
CHANGED
|
@@ -6,6 +6,8 @@ var awsEsConnection = require('@acuris/aws-es-connection');
|
|
|
6
6
|
var elasticsearch = require('@elastic/elasticsearch');
|
|
7
7
|
var esb = require('elastic-builder');
|
|
8
8
|
var lodash = require('lodash');
|
|
9
|
+
var pluginSearchBackendNode = require('@backstage/plugin-search-backend-node');
|
|
10
|
+
var stream = require('stream');
|
|
9
11
|
|
|
10
12
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
11
13
|
|
|
@@ -16,6 +18,94 @@ function duration(startTimestamp) {
|
|
|
16
18
|
const seconds = delta[0] + delta[1] / 1e9;
|
|
17
19
|
return `${seconds.toFixed(1)}s`;
|
|
18
20
|
}
|
|
21
|
+
class ElasticSearchSearchEngineIndexer extends pluginSearchBackendNode.BatchSearchEngineIndexer {
|
|
22
|
+
constructor(options) {
|
|
23
|
+
super({ batchSize: 100 });
|
|
24
|
+
this.received = 0;
|
|
25
|
+
this.processed = 0;
|
|
26
|
+
this.removableIndices = [];
|
|
27
|
+
this.logger = options.logger;
|
|
28
|
+
this.startTimestamp = process.hrtime();
|
|
29
|
+
this.type = options.type;
|
|
30
|
+
this.indexPrefix = options.indexPrefix;
|
|
31
|
+
this.indexSeparator = options.indexSeparator;
|
|
32
|
+
this.indexName = this.constructIndexName(`${Date.now()}`);
|
|
33
|
+
this.alias = options.alias;
|
|
34
|
+
this.elasticSearchClient = options.elasticSearchClient;
|
|
35
|
+
this.sourceStream = new stream.Readable({ objectMode: true });
|
|
36
|
+
this.sourceStream._read = () => {
|
|
37
|
+
};
|
|
38
|
+
const that = this;
|
|
39
|
+
this.bulkResult = this.elasticSearchClient.helpers.bulk({
|
|
40
|
+
datasource: this.sourceStream,
|
|
41
|
+
onDocument() {
|
|
42
|
+
that.processed++;
|
|
43
|
+
return {
|
|
44
|
+
index: { _index: that.indexName }
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
refreshOnCompletion: that.indexName
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async initialize() {
|
|
51
|
+
this.logger.info(`Started indexing documents for index ${this.type}`);
|
|
52
|
+
const aliases = await this.elasticSearchClient.cat.aliases({
|
|
53
|
+
format: "json",
|
|
54
|
+
name: this.alias
|
|
55
|
+
});
|
|
56
|
+
this.removableIndices = aliases.body.map((r) => r.index);
|
|
57
|
+
await this.elasticSearchClient.indices.create({
|
|
58
|
+
index: this.indexName
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async index(documents) {
|
|
62
|
+
await this.isReady();
|
|
63
|
+
documents.forEach((document) => {
|
|
64
|
+
this.received++;
|
|
65
|
+
this.sourceStream.push(document);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async finalize() {
|
|
69
|
+
await this.isReady();
|
|
70
|
+
this.sourceStream.push(null);
|
|
71
|
+
const result = await this.bulkResult;
|
|
72
|
+
this.logger.info(`Indexing completed for index ${this.type} in ${duration(this.startTimestamp)}`, result);
|
|
73
|
+
await this.elasticSearchClient.indices.updateAliases({
|
|
74
|
+
body: {
|
|
75
|
+
actions: [
|
|
76
|
+
{
|
|
77
|
+
remove: { index: this.constructIndexName("*"), alias: this.alias }
|
|
78
|
+
},
|
|
79
|
+
{ add: { index: this.indexName, alias: this.alias } }
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (this.removableIndices.length) {
|
|
84
|
+
this.logger.info("Removing stale search indices", this.removableIndices);
|
|
85
|
+
try {
|
|
86
|
+
await this.elasticSearchClient.indices.delete({
|
|
87
|
+
index: this.removableIndices
|
|
88
|
+
});
|
|
89
|
+
} catch (e) {
|
|
90
|
+
this.logger.warn(`Failed to remove stale search indices: ${e}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
isReady() {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
const interval = setInterval(() => {
|
|
97
|
+
if (this.received === this.processed) {
|
|
98
|
+
clearInterval(interval);
|
|
99
|
+
resolve();
|
|
100
|
+
}
|
|
101
|
+
}, 50);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
constructIndexName(postFix) {
|
|
105
|
+
return `${this.indexPrefix}${this.type}${this.indexSeparator}${postFix}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
19
109
|
function isBlank(str) {
|
|
20
110
|
return lodash.isEmpty(str) && !lodash.isNumber(str) || lodash.isNaN(str);
|
|
21
111
|
}
|
|
@@ -71,57 +161,34 @@ class ElasticSearchSearchEngine {
|
|
|
71
161
|
setTranslator(translator) {
|
|
72
162
|
this.translator = translator;
|
|
73
163
|
}
|
|
74
|
-
async
|
|
75
|
-
this.logger.info(`Started indexing ${documents.length} documents for index ${type}`);
|
|
76
|
-
const startTimestamp = process.hrtime();
|
|
164
|
+
async getIndexer(type) {
|
|
77
165
|
const alias = this.constructSearchAlias(type);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
88
|
-
const result = await this.elasticSearchClient.helpers.bulk({
|
|
89
|
-
datasource: documents,
|
|
90
|
-
onDocument() {
|
|
91
|
-
return {
|
|
92
|
-
index: { _index: index }
|
|
93
|
-
};
|
|
94
|
-
},
|
|
95
|
-
refreshOnCompletion: index
|
|
96
|
-
});
|
|
97
|
-
this.logger.info(`Indexing completed for index ${type} in ${duration(startTimestamp)}`, result);
|
|
98
|
-
await this.elasticSearchClient.indices.updateAliases({
|
|
99
|
-
body: {
|
|
100
|
-
actions: [
|
|
101
|
-
{ remove: { index: this.constructIndexName(type, "*"), alias } },
|
|
102
|
-
{ add: { index, alias } }
|
|
103
|
-
]
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
this.logger.info("Removing stale search indices", removableIndices);
|
|
107
|
-
if (removableIndices.length) {
|
|
108
|
-
await this.elasticSearchClient.indices.delete({
|
|
109
|
-
index: removableIndices
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
} catch (e) {
|
|
166
|
+
const indexer = new ElasticSearchSearchEngineIndexer({
|
|
167
|
+
type,
|
|
168
|
+
indexPrefix: this.indexPrefix,
|
|
169
|
+
indexSeparator: this.indexSeparator,
|
|
170
|
+
alias,
|
|
171
|
+
elasticSearchClient: this.elasticSearchClient,
|
|
172
|
+
logger: this.logger
|
|
173
|
+
});
|
|
174
|
+
indexer.on("error", async (e) => {
|
|
113
175
|
this.logger.error(`Failed to index documents for type ${type}`, e);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const indexCreated = response.body;
|
|
118
|
-
if (indexCreated) {
|
|
119
|
-
this.logger.info(`Removing created index ${index}`);
|
|
120
|
-
await this.elasticSearchClient.indices.delete({
|
|
121
|
-
index
|
|
176
|
+
try {
|
|
177
|
+
const response = await this.elasticSearchClient.indices.exists({
|
|
178
|
+
index: indexer.indexName
|
|
122
179
|
});
|
|
180
|
+
const indexCreated = response.body;
|
|
181
|
+
if (indexCreated) {
|
|
182
|
+
this.logger.info(`Removing created index ${indexer.indexName}`);
|
|
183
|
+
await this.elasticSearchClient.indices.delete({
|
|
184
|
+
index: indexer.indexName
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
this.logger.error(`Unable to clean up elastic index: ${error}`);
|
|
123
189
|
}
|
|
124
|
-
}
|
|
190
|
+
});
|
|
191
|
+
return indexer;
|
|
125
192
|
}
|
|
126
193
|
async query(query) {
|
|
127
194
|
const { elasticSearchQuery, documentTypes, pageSize } = this.translator(query);
|
|
@@ -149,9 +216,6 @@ class ElasticSearchSearchEngine {
|
|
|
149
216
|
return Promise.reject({ results: [] });
|
|
150
217
|
}
|
|
151
218
|
}
|
|
152
|
-
constructIndexName(type, postFix) {
|
|
153
|
-
return `${this.indexPrefix}${type}${this.indexSeparator}${postFix}`;
|
|
154
|
-
}
|
|
155
219
|
getTypeFromIndex(index) {
|
|
156
220
|
return index.substring(this.indexPrefix.length).split(this.indexSeparator)[0];
|
|
157
221
|
}
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":["../src/engines/ElasticSearchSearchEngine.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 awsGetCredentials,\n createAWSConnection,\n} from '@acuris/aws-es-connection';\nimport { Config } from '@backstage/config';\nimport {\n IndexableDocument,\n SearchEngine,\n SearchQuery,\n SearchResultSet,\n} from '@backstage/search-common';\nimport { Client } from '@elastic/elasticsearch';\nimport esb from 'elastic-builder';\nimport { isEmpty, isNaN as nan, isNumber } from 'lodash';\nimport { Logger } from 'winston';\n\nimport type { ElasticSearchClientOptions } from './ElasticSearchClientOptions';\n\nexport type { ElasticSearchClientOptions };\n\nexport type ConcreteElasticSearchQuery = {\n documentTypes?: string[];\n elasticSearchQuery: Object;\n pageSize: number;\n};\n\ntype ElasticSearchQueryTranslator = (\n query: SearchQuery,\n) => ConcreteElasticSearchQuery;\n\ntype ElasticSearchOptions = {\n logger: Logger;\n config: Config;\n aliasPostfix?: string;\n indexPrefix?: string;\n};\n\ntype ElasticSearchResult = {\n _index: string;\n _type: string;\n _score: number;\n _source: IndexableDocument;\n};\n\nfunction duration(startTimestamp: [number, number]): string {\n const delta = process.hrtime(startTimestamp);\n const seconds = delta[0] + delta[1] / 1e9;\n return `${seconds.toFixed(1)}s`;\n}\n\nfunction isBlank(str: string) {\n return (isEmpty(str) && !isNumber(str)) || nan(str);\n}\n\n/**\n * @public\n */\nexport class ElasticSearchSearchEngine implements SearchEngine {\n private readonly elasticSearchClient: Client;\n\n constructor(\n private readonly elasticSearchClientOptions: ElasticSearchClientOptions,\n private readonly aliasPostfix: string,\n private readonly indexPrefix: string,\n private readonly logger: Logger,\n ) {\n this.elasticSearchClient = this.newClient(options => new Client(options));\n }\n\n static async fromConfig({\n logger,\n config,\n aliasPostfix = `search`,\n indexPrefix = ``,\n }: ElasticSearchOptions) {\n const options = await createElasticSearchClientOptions(\n config.getConfig('search.elasticsearch'),\n );\n if (options.provider === 'elastic') {\n logger.info('Initializing Elastic.co ElasticSearch search engine.');\n } else if (options.provider === 'aws') {\n logger.info('Initializing AWS ElasticSearch search engine.');\n } else {\n logger.info('Initializing ElasticSearch search engine.');\n }\n\n return new ElasticSearchSearchEngine(\n options,\n aliasPostfix,\n indexPrefix,\n logger,\n );\n }\n\n /**\n * Create a custom search client from the derived elastic search\n * configuration. This need not be the same client that the engine uses\n * internally.\n */\n newClient<T>(create: (options: ElasticSearchClientOptions) => T): T {\n return create(this.elasticSearchClientOptions);\n }\n\n protected translator(query: SearchQuery): ConcreteElasticSearchQuery {\n const { term, filters = {}, types, pageCursor } = query;\n\n const filter = Object.entries(filters)\n .filter(([_, value]) => Boolean(value))\n .map(([key, value]: [key: string, value: any]) => {\n if (['string', 'number', 'boolean'].includes(typeof value)) {\n return esb.matchQuery(key, value.toString());\n }\n if (Array.isArray(value)) {\n return esb\n .boolQuery()\n .should(value.map(it => esb.matchQuery(key, it.toString())));\n }\n this.logger.error(\n 'Failed to query, unrecognized filter type',\n key,\n value,\n );\n throw new Error(\n 'Failed to add filters to query. Unrecognized filter type',\n );\n });\n const esbQuery = isBlank(term)\n ? esb.matchAllQuery()\n : esb\n .multiMatchQuery(['*'], term)\n .fuzziness('auto')\n .minimumShouldMatch(1);\n const pageSize = 25;\n const { page } = decodePageCursor(pageCursor);\n\n return {\n elasticSearchQuery: esb\n .requestBodySearch()\n .query(esb.boolQuery().filter(filter).must([esbQuery]))\n .from(page * pageSize)\n .size(pageSize)\n .toJSON(),\n documentTypes: types,\n pageSize,\n };\n }\n\n setTranslator(translator: ElasticSearchQueryTranslator) {\n this.translator = translator;\n }\n\n async index(type: string, documents: IndexableDocument[]): Promise<void> {\n this.logger.info(\n `Started indexing ${documents.length} documents for index ${type}`,\n );\n const startTimestamp = process.hrtime();\n const alias = this.constructSearchAlias(type);\n const index = this.constructIndexName(type, `${Date.now()}`);\n try {\n const aliases = await this.elasticSearchClient.cat.aliases({\n format: 'json',\n name: alias,\n });\n const removableIndices = aliases.body.map(\n (r: Record<string, any>) => r.index,\n );\n\n await this.elasticSearchClient.indices.create({\n index,\n });\n const result = await this.elasticSearchClient.helpers.bulk({\n datasource: documents,\n onDocument() {\n return {\n index: { _index: index },\n };\n },\n refreshOnCompletion: index,\n });\n\n this.logger.info(\n `Indexing completed for index ${type} in ${duration(startTimestamp)}`,\n result,\n );\n await this.elasticSearchClient.indices.updateAliases({\n body: {\n actions: [\n { remove: { index: this.constructIndexName(type, '*'), alias } },\n { add: { index, alias } },\n ],\n },\n });\n\n this.logger.info('Removing stale search indices', removableIndices);\n if (removableIndices.length) {\n await this.elasticSearchClient.indices.delete({\n index: removableIndices,\n });\n }\n } catch (e) {\n this.logger.error(`Failed to index documents for type ${type}`, e);\n const response = await this.elasticSearchClient.indices.exists({\n index,\n });\n const indexCreated = response.body;\n if (indexCreated) {\n this.logger.info(`Removing created index ${index}`);\n await this.elasticSearchClient.indices.delete({\n index,\n });\n }\n }\n }\n\n async query(query: SearchQuery): Promise<SearchResultSet> {\n const { elasticSearchQuery, documentTypes, pageSize } =\n this.translator(query);\n const queryIndices = documentTypes\n ? documentTypes.map(it => this.constructSearchAlias(it))\n : this.constructSearchAlias('*');\n try {\n const result = await this.elasticSearchClient.search({\n index: queryIndices,\n body: elasticSearchQuery,\n });\n const { page } = decodePageCursor(query.pageCursor);\n const hasNextPage = result.body.hits.total.value > page * pageSize;\n const hasPreviousPage = page > 0;\n const nextPageCursor = hasNextPage\n ? encodePageCursor({ page: page + 1 })\n : undefined;\n const previousPageCursor = hasPreviousPage\n ? encodePageCursor({ page: page - 1 })\n : undefined;\n\n return {\n results: result.body.hits.hits.map((d: ElasticSearchResult) => ({\n type: this.getTypeFromIndex(d._index),\n document: d._source,\n })),\n nextPageCursor,\n previousPageCursor,\n };\n } catch (e) {\n this.logger.error(\n `Failed to query documents for indices ${queryIndices}`,\n e,\n );\n return Promise.reject({ results: [] });\n }\n }\n\n private readonly indexSeparator = '-index__';\n\n private constructIndexName(type: string, postFix: string) {\n return `${this.indexPrefix}${type}${this.indexSeparator}${postFix}`;\n }\n\n private getTypeFromIndex(index: string) {\n return index\n .substring(this.indexPrefix.length)\n .split(this.indexSeparator)[0];\n }\n\n private constructSearchAlias(type: string) {\n const postFix = this.aliasPostfix ? `__${this.aliasPostfix}` : '';\n return `${this.indexPrefix}${type}${postFix}`;\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\nasync function createElasticSearchClientOptions(\n config?: Config,\n): Promise<ElasticSearchClientOptions> {\n if (!config) {\n throw new Error('No elastic search config found');\n }\n const clientOptionsConfig = config.getOptionalConfig('clientOptions');\n const sslConfig = clientOptionsConfig?.getOptionalConfig('ssl');\n\n if (config.getOptionalString('provider') === 'elastic') {\n const authConfig = config.getConfig('auth');\n return {\n provider: 'elastic',\n cloud: {\n id: config.getString('cloudId'),\n },\n auth: {\n username: authConfig.getString('username'),\n password: authConfig.getString('password'),\n },\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n };\n }\n if (config.getOptionalString('provider') === 'aws') {\n const awsCredentials = await awsGetCredentials();\n const AWSConnection = createAWSConnection(awsCredentials);\n return {\n provider: 'aws',\n node: config.getString('node'),\n ...AWSConnection,\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n };\n }\n const authConfig = config.getOptionalConfig('auth');\n const auth =\n authConfig &&\n (authConfig.has('apiKey')\n ? {\n apiKey: authConfig.getString('apiKey'),\n }\n : {\n username: authConfig.getString('username'),\n password: authConfig.getString('password'),\n });\n return {\n node: config.getString('node'),\n auth,\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n };\n}\n"],"names":["isEmpty","isNumber","nan","Client","esb","awsGetCredentials","createAWSConnection"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/engines/ElasticSearchSearchEngineIndexer.ts","../src/engines/ElasticSearchSearchEngine.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';\nimport { IndexableDocument } from '@backstage/search-common';\nimport { Client } from '@elastic/elasticsearch';\nimport { Readable } from 'stream';\nimport { Logger } from 'winston';\n\nexport type ElasticSearchSearchEngineIndexerOptions = {\n type: string;\n indexPrefix: string;\n indexSeparator: string;\n alias: string;\n logger: Logger;\n elasticSearchClient: Client;\n};\n\nfunction duration(startTimestamp: [number, number]): string {\n const delta = process.hrtime(startTimestamp);\n const seconds = delta[0] + delta[1] / 1e9;\n return `${seconds.toFixed(1)}s`;\n}\n\nexport class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {\n private received: number = 0;\n private processed: number = 0;\n private removableIndices: string[] = [];\n\n private readonly startTimestamp: [number, number];\n private readonly type: string;\n public readonly indexName: string;\n private readonly indexPrefix: string;\n private readonly indexSeparator: string;\n private readonly alias: string;\n private readonly logger: Logger;\n private readonly sourceStream: Readable;\n private readonly elasticSearchClient: Client;\n private bulkResult: Promise<any>;\n\n constructor(options: ElasticSearchSearchEngineIndexerOptions) {\n super({ batchSize: 100 });\n this.logger = options.logger;\n this.startTimestamp = process.hrtime();\n this.type = options.type;\n this.indexPrefix = options.indexPrefix;\n this.indexSeparator = options.indexSeparator;\n this.indexName = this.constructIndexName(`${Date.now()}`);\n this.alias = options.alias;\n this.elasticSearchClient = options.elasticSearchClient;\n\n // The ES client bulk helper supports stream-based indexing, but we have to\n // supply the stream directly to it at instantiation-time. We can't supply\n // this class itself, so instead, we create this inline stream instead.\n this.sourceStream = new Readable({ objectMode: true });\n this.sourceStream._read = () => {};\n\n // eslint-disable-next-line consistent-this\n const that = this;\n\n // Keep a reference to the ES Bulk helper so that we can know when all\n // documents have been successfully written to ES.\n this.bulkResult = this.elasticSearchClient.helpers.bulk({\n datasource: this.sourceStream,\n onDocument() {\n that.processed++;\n return {\n index: { _index: that.indexName },\n };\n },\n refreshOnCompletion: that.indexName,\n });\n }\n\n async initialize(): Promise<void> {\n this.logger.info(`Started indexing documents for index ${this.type}`);\n\n const aliases = await this.elasticSearchClient.cat.aliases({\n format: 'json',\n name: this.alias,\n });\n\n this.removableIndices = aliases.body.map(\n (r: Record<string, any>) => r.index,\n );\n\n await this.elasticSearchClient.indices.create({\n index: this.indexName,\n });\n }\n\n async index(documents: IndexableDocument[]): Promise<void> {\n await this.isReady();\n documents.forEach(document => {\n this.received++;\n this.sourceStream.push(document);\n });\n }\n\n async finalize(): Promise<void> {\n // Wait for all documents to be processed.\n await this.isReady();\n\n // Close off the underlying stream connected to ES, indicating that no more\n // documents will be written.\n this.sourceStream.push(null);\n\n // Wait for the bulk helper to finish processing.\n const result = await this.bulkResult;\n\n // Rotate aliases upon completion. Allow errors to bubble up so that we can\n // clean up the create index.\n this.logger.info(\n `Indexing completed for index ${this.type} in ${duration(\n this.startTimestamp,\n )}`,\n result,\n );\n await this.elasticSearchClient.indices.updateAliases({\n body: {\n actions: [\n {\n remove: { index: this.constructIndexName('*'), alias: this.alias },\n },\n { add: { index: this.indexName, alias: this.alias } },\n ],\n },\n });\n\n // If any indices are removable, remove them. Do not bubble up this error,\n // as doing so would delete the now aliased index. Log instead.\n if (this.removableIndices.length) {\n this.logger.info('Removing stale search indices', this.removableIndices);\n try {\n await this.elasticSearchClient.indices.delete({\n index: this.removableIndices,\n });\n } catch (e) {\n this.logger.warn(`Failed to remove stale search indices: ${e}`);\n }\n }\n }\n\n /**\n * Ensures that the number of documents sent over the wire to ES matches the\n * number of documents this stream has received so far. This helps manage\n * backpressure in other parts of the indexing pipeline.\n */\n private isReady(): Promise<void> {\n return new Promise(resolve => {\n const interval = setInterval(() => {\n if (this.received === this.processed) {\n clearInterval(interval);\n resolve();\n }\n }, 50);\n });\n }\n\n private constructIndexName(postFix: string) {\n return `${this.indexPrefix}${this.type}${this.indexSeparator}${postFix}`;\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 awsGetCredentials,\n createAWSConnection,\n} from '@acuris/aws-es-connection';\nimport { Config } from '@backstage/config';\nimport {\n IndexableDocument,\n SearchEngine,\n SearchQuery,\n SearchResultSet,\n} from '@backstage/search-common';\nimport { Client } from '@elastic/elasticsearch';\nimport esb from 'elastic-builder';\nimport { isEmpty, isNaN as nan, isNumber } from 'lodash';\nimport { Logger } from 'winston';\nimport type { ElasticSearchClientOptions } from './ElasticSearchClientOptions';\nimport { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer';\n\nexport type { ElasticSearchClientOptions };\n\nexport type ConcreteElasticSearchQuery = {\n documentTypes?: string[];\n elasticSearchQuery: Object;\n pageSize: number;\n};\n\ntype ElasticSearchQueryTranslator = (\n query: SearchQuery,\n) => ConcreteElasticSearchQuery;\n\ntype ElasticSearchOptions = {\n logger: Logger;\n config: Config;\n aliasPostfix?: string;\n indexPrefix?: string;\n};\n\ntype ElasticSearchResult = {\n _index: string;\n _type: string;\n _score: number;\n _source: IndexableDocument;\n};\n\nfunction isBlank(str: string) {\n return (isEmpty(str) && !isNumber(str)) || nan(str);\n}\n\n/**\n * @public\n */\nexport class ElasticSearchSearchEngine implements SearchEngine {\n private readonly elasticSearchClient: Client;\n\n constructor(\n private readonly elasticSearchClientOptions: ElasticSearchClientOptions,\n private readonly aliasPostfix: string,\n private readonly indexPrefix: string,\n private readonly logger: Logger,\n ) {\n this.elasticSearchClient = this.newClient(options => new Client(options));\n }\n\n static async fromConfig({\n logger,\n config,\n aliasPostfix = `search`,\n indexPrefix = ``,\n }: ElasticSearchOptions) {\n const options = await createElasticSearchClientOptions(\n config.getConfig('search.elasticsearch'),\n );\n if (options.provider === 'elastic') {\n logger.info('Initializing Elastic.co ElasticSearch search engine.');\n } else if (options.provider === 'aws') {\n logger.info('Initializing AWS ElasticSearch search engine.');\n } else {\n logger.info('Initializing ElasticSearch search engine.');\n }\n\n return new ElasticSearchSearchEngine(\n options,\n aliasPostfix,\n indexPrefix,\n logger,\n );\n }\n\n /**\n * Create a custom search client from the derived elastic search\n * configuration. This need not be the same client that the engine uses\n * internally.\n */\n newClient<T>(create: (options: ElasticSearchClientOptions) => T): T {\n return create(this.elasticSearchClientOptions);\n }\n\n protected translator(query: SearchQuery): ConcreteElasticSearchQuery {\n const { term, filters = {}, types, pageCursor } = query;\n\n const filter = Object.entries(filters)\n .filter(([_, value]) => Boolean(value))\n .map(([key, value]: [key: string, value: any]) => {\n if (['string', 'number', 'boolean'].includes(typeof value)) {\n return esb.matchQuery(key, value.toString());\n }\n if (Array.isArray(value)) {\n return esb\n .boolQuery()\n .should(value.map(it => esb.matchQuery(key, it.toString())));\n }\n this.logger.error(\n 'Failed to query, unrecognized filter type',\n key,\n value,\n );\n throw new Error(\n 'Failed to add filters to query. Unrecognized filter type',\n );\n });\n const esbQuery = isBlank(term)\n ? esb.matchAllQuery()\n : esb\n .multiMatchQuery(['*'], term)\n .fuzziness('auto')\n .minimumShouldMatch(1);\n const pageSize = 25;\n const { page } = decodePageCursor(pageCursor);\n\n return {\n elasticSearchQuery: esb\n .requestBodySearch()\n .query(esb.boolQuery().filter(filter).must([esbQuery]))\n .from(page * pageSize)\n .size(pageSize)\n .toJSON(),\n documentTypes: types,\n pageSize,\n };\n }\n\n setTranslator(translator: ElasticSearchQueryTranslator) {\n this.translator = translator;\n }\n\n async getIndexer(type: string) {\n const alias = this.constructSearchAlias(type);\n const indexer = new ElasticSearchSearchEngineIndexer({\n type,\n indexPrefix: this.indexPrefix,\n indexSeparator: this.indexSeparator,\n alias,\n elasticSearchClient: this.elasticSearchClient,\n logger: this.logger,\n });\n\n // Attempt cleanup upon failure.\n indexer.on('error', async e => {\n this.logger.error(`Failed to index documents for type ${type}`, e);\n try {\n const response = await this.elasticSearchClient.indices.exists({\n index: indexer.indexName,\n });\n const indexCreated = response.body;\n if (indexCreated) {\n this.logger.info(`Removing created index ${indexer.indexName}`);\n await this.elasticSearchClient.indices.delete({\n index: indexer.indexName,\n });\n }\n } catch (error) {\n this.logger.error(`Unable to clean up elastic index: ${error}`);\n }\n });\n\n return indexer;\n }\n\n async query(query: SearchQuery): Promise<SearchResultSet> {\n const { elasticSearchQuery, documentTypes, pageSize } =\n this.translator(query);\n const queryIndices = documentTypes\n ? documentTypes.map(it => this.constructSearchAlias(it))\n : this.constructSearchAlias('*');\n try {\n const result = await this.elasticSearchClient.search({\n index: queryIndices,\n body: elasticSearchQuery,\n });\n const { page } = decodePageCursor(query.pageCursor);\n const hasNextPage = result.body.hits.total.value > page * pageSize;\n const hasPreviousPage = page > 0;\n const nextPageCursor = hasNextPage\n ? encodePageCursor({ page: page + 1 })\n : undefined;\n const previousPageCursor = hasPreviousPage\n ? encodePageCursor({ page: page - 1 })\n : undefined;\n\n return {\n results: result.body.hits.hits.map((d: ElasticSearchResult) => ({\n type: this.getTypeFromIndex(d._index),\n document: d._source,\n })),\n nextPageCursor,\n previousPageCursor,\n };\n } catch (e) {\n this.logger.error(\n `Failed to query documents for indices ${queryIndices}`,\n e,\n );\n return Promise.reject({ results: [] });\n }\n }\n\n private readonly indexSeparator = '-index__';\n\n private getTypeFromIndex(index: string) {\n return index\n .substring(this.indexPrefix.length)\n .split(this.indexSeparator)[0];\n }\n\n private constructSearchAlias(type: string) {\n const postFix = this.aliasPostfix ? `__${this.aliasPostfix}` : '';\n return `${this.indexPrefix}${type}${postFix}`;\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\nasync function createElasticSearchClientOptions(\n config?: Config,\n): Promise<ElasticSearchClientOptions> {\n if (!config) {\n throw new Error('No elastic search config found');\n }\n const clientOptionsConfig = config.getOptionalConfig('clientOptions');\n const sslConfig = clientOptionsConfig?.getOptionalConfig('ssl');\n\n if (config.getOptionalString('provider') === 'elastic') {\n const authConfig = config.getConfig('auth');\n return {\n provider: 'elastic',\n cloud: {\n id: config.getString('cloudId'),\n },\n auth: {\n username: authConfig.getString('username'),\n password: authConfig.getString('password'),\n },\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n };\n }\n if (config.getOptionalString('provider') === 'aws') {\n const awsCredentials = await awsGetCredentials();\n const AWSConnection = createAWSConnection(awsCredentials);\n return {\n provider: 'aws',\n node: config.getString('node'),\n ...AWSConnection,\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n };\n }\n const authConfig = config.getOptionalConfig('auth');\n const auth =\n authConfig &&\n (authConfig.has('apiKey')\n ? {\n apiKey: authConfig.getString('apiKey'),\n }\n : {\n username: authConfig.getString('username'),\n password: authConfig.getString('password'),\n });\n return {\n node: config.getString('node'),\n auth,\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n };\n}\n"],"names":["BatchSearchEngineIndexer","Readable","isEmpty","isNumber","nan","Client","esb","awsGetCredentials","createAWSConnection"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { Config } from '@backstage/config';
|
|
3
|
-
import { SearchEngine, SearchQuery,
|
|
3
|
+
import { IndexableDocument, SearchEngine, SearchQuery, SearchResultSet } from '@backstage/search-common';
|
|
4
4
|
import { Logger } from 'winston';
|
|
5
5
|
import { ConnectionOptions } from 'tls';
|
|
6
|
+
import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';
|
|
7
|
+
import { Client } from '@elastic/elasticsearch';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Options used to configure the `@elastic/elasticsearch` client and
|
|
@@ -96,6 +98,41 @@ interface ElasticSearchTransportConstructor {
|
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
declare type ElasticSearchSearchEngineIndexerOptions = {
|
|
102
|
+
type: string;
|
|
103
|
+
indexPrefix: string;
|
|
104
|
+
indexSeparator: string;
|
|
105
|
+
alias: string;
|
|
106
|
+
logger: Logger;
|
|
107
|
+
elasticSearchClient: Client;
|
|
108
|
+
};
|
|
109
|
+
declare class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
|
110
|
+
private received;
|
|
111
|
+
private processed;
|
|
112
|
+
private removableIndices;
|
|
113
|
+
private readonly startTimestamp;
|
|
114
|
+
private readonly type;
|
|
115
|
+
readonly indexName: string;
|
|
116
|
+
private readonly indexPrefix;
|
|
117
|
+
private readonly indexSeparator;
|
|
118
|
+
private readonly alias;
|
|
119
|
+
private readonly logger;
|
|
120
|
+
private readonly sourceStream;
|
|
121
|
+
private readonly elasticSearchClient;
|
|
122
|
+
private bulkResult;
|
|
123
|
+
constructor(options: ElasticSearchSearchEngineIndexerOptions);
|
|
124
|
+
initialize(): Promise<void>;
|
|
125
|
+
index(documents: IndexableDocument[]): Promise<void>;
|
|
126
|
+
finalize(): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Ensures that the number of documents sent over the wire to ES matches the
|
|
129
|
+
* number of documents this stream has received so far. This helps manage
|
|
130
|
+
* backpressure in other parts of the indexing pipeline.
|
|
131
|
+
*/
|
|
132
|
+
private isReady;
|
|
133
|
+
private constructIndexName;
|
|
134
|
+
}
|
|
135
|
+
|
|
99
136
|
declare type ConcreteElasticSearchQuery = {
|
|
100
137
|
documentTypes?: string[];
|
|
101
138
|
elasticSearchQuery: Object;
|
|
@@ -127,12 +164,11 @@ declare class ElasticSearchSearchEngine implements SearchEngine {
|
|
|
127
164
|
newClient<T>(create: (options: ElasticSearchClientOptions) => T): T;
|
|
128
165
|
protected translator(query: SearchQuery): ConcreteElasticSearchQuery;
|
|
129
166
|
setTranslator(translator: ElasticSearchQueryTranslator): void;
|
|
130
|
-
|
|
167
|
+
getIndexer(type: string): Promise<ElasticSearchSearchEngineIndexer>;
|
|
131
168
|
query(query: SearchQuery): Promise<SearchResultSet>;
|
|
132
169
|
private readonly indexSeparator;
|
|
133
|
-
private constructIndexName;
|
|
134
170
|
private getTypeFromIndex;
|
|
135
171
|
private constructSearchAlias;
|
|
136
172
|
}
|
|
137
173
|
|
|
138
|
-
export { ElasticSearchClientOptions, ElasticSearchSearchEngine };
|
|
174
|
+
export { ElasticSearchClientOptions, ElasticSearchSearchEngine, ElasticSearchSearchEngineIndexer, ElasticSearchSearchEngineIndexerOptions };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-search-backend-module-elasticsearch",
|
|
3
3
|
"description": "A module for the search backend that implements search using ElasticSearch",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -10,19 +10,23 @@
|
|
|
10
10
|
"main": "dist/index.cjs.js",
|
|
11
11
|
"types": "dist/index.d.ts"
|
|
12
12
|
},
|
|
13
|
+
"backstage": {
|
|
14
|
+
"role": "backend-plugin-module"
|
|
15
|
+
},
|
|
13
16
|
"scripts": {
|
|
14
|
-
"start": "backstage-cli
|
|
15
|
-
"build": "backstage-cli
|
|
16
|
-
"lint": "backstage-cli lint",
|
|
17
|
-
"test": "backstage-cli test",
|
|
18
|
-
"prepack": "backstage-cli prepack",
|
|
19
|
-
"postpack": "backstage-cli postpack",
|
|
20
|
-
"clean": "backstage-cli clean"
|
|
17
|
+
"start": "backstage-cli package start",
|
|
18
|
+
"build": "backstage-cli package build",
|
|
19
|
+
"lint": "backstage-cli package lint",
|
|
20
|
+
"test": "backstage-cli package test",
|
|
21
|
+
"prepack": "backstage-cli package prepack",
|
|
22
|
+
"postpack": "backstage-cli package postpack",
|
|
23
|
+
"clean": "backstage-cli package clean"
|
|
21
24
|
},
|
|
22
25
|
"dependencies": {
|
|
23
26
|
"@acuris/aws-es-connection": "^2.2.0",
|
|
24
|
-
"@backstage/config": "^0.1.
|
|
25
|
-
"@backstage/search-
|
|
27
|
+
"@backstage/config": "^0.1.15",
|
|
28
|
+
"@backstage/plugin-search-backend-node": "^0.5.0",
|
|
29
|
+
"@backstage/search-common": "^0.3.0",
|
|
26
30
|
"@elastic/elasticsearch": "7.13.0",
|
|
27
31
|
"aws-sdk": "^2.948.0",
|
|
28
32
|
"elastic-builder": "^2.16.0",
|
|
@@ -30,9 +34,9 @@
|
|
|
30
34
|
"winston": "^3.2.1"
|
|
31
35
|
},
|
|
32
36
|
"devDependencies": {
|
|
33
|
-
"@backstage/backend-common": "^0.
|
|
34
|
-
"@backstage/cli": "^0.
|
|
35
|
-
"@elastic/elasticsearch-mock": "^0.
|
|
37
|
+
"@backstage/backend-common": "^0.12.0",
|
|
38
|
+
"@backstage/cli": "^0.15.0",
|
|
39
|
+
"@elastic/elasticsearch-mock": "^1.0.0"
|
|
36
40
|
},
|
|
37
41
|
"files": [
|
|
38
42
|
"dist",
|
|
@@ -42,5 +46,5 @@
|
|
|
42
46
|
"testEnvironment": "node"
|
|
43
47
|
},
|
|
44
48
|
"configSchema": "config.d.ts",
|
|
45
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "04bb0dd824b78f6b57dac62c3015e681f094045c"
|
|
46
50
|
}
|