@backstage/plugin-search-backend-module-elasticsearch 0.0.5 → 0.0.8
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 +27 -0
- package/dist/index.cjs.js +84 -77
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +108 -6
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @backstage/plugin-search-backend-module-elasticsearch
|
|
2
2
|
|
|
3
|
+
## 0.0.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @backstage/config@0.1.13
|
|
9
|
+
|
|
10
|
+
## 0.0.8-next.0
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @backstage/config@0.1.13-next.0
|
|
16
|
+
|
|
17
|
+
## 0.0.7
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 68512f5178: Add `newClient()` method to re-use the configuration of the elastic search
|
|
22
|
+
engine with custom clients
|
|
23
|
+
|
|
24
|
+
## 0.0.6
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly
|
|
29
|
+
|
|
3
30
|
## 0.0.5
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/dist/index.cjs.js
CHANGED
|
@@ -20,12 +20,13 @@ function isBlank(str) {
|
|
|
20
20
|
return lodash.isEmpty(str) && !lodash.isNumber(str) || lodash.isNaN(str);
|
|
21
21
|
}
|
|
22
22
|
class ElasticSearchSearchEngine {
|
|
23
|
-
constructor(
|
|
24
|
-
this.
|
|
23
|
+
constructor(elasticSearchClientOptions, aliasPostfix, indexPrefix, logger) {
|
|
24
|
+
this.elasticSearchClientOptions = elasticSearchClientOptions;
|
|
25
25
|
this.aliasPostfix = aliasPostfix;
|
|
26
26
|
this.indexPrefix = indexPrefix;
|
|
27
27
|
this.logger = logger;
|
|
28
28
|
this.indexSeparator = "-index__";
|
|
29
|
+
this.elasticSearchClient = this.newClient((options) => new elasticsearch.Client(options));
|
|
29
30
|
}
|
|
30
31
|
static async fromConfig({
|
|
31
32
|
logger,
|
|
@@ -33,85 +34,36 @@ class ElasticSearchSearchEngine {
|
|
|
33
34
|
aliasPostfix = `search`,
|
|
34
35
|
indexPrefix = ``
|
|
35
36
|
}) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
static async constructElasticSearchClient(logger, config) {
|
|
39
|
-
if (!config) {
|
|
40
|
-
throw new Error("No elastic search config found");
|
|
41
|
-
}
|
|
42
|
-
const clientOptionsConfig = config.getOptionalConfig("clientOptions");
|
|
43
|
-
const sslConfig = clientOptionsConfig == null ? void 0 : clientOptionsConfig.getOptionalConfig("ssl");
|
|
44
|
-
if (config.getOptionalString("provider") === "elastic") {
|
|
37
|
+
const options = await createElasticSearchClientOptions(config.getConfig("search.elasticsearch"));
|
|
38
|
+
if (options.provider === "elastic") {
|
|
45
39
|
logger.info("Initializing Elastic.co ElasticSearch search engine.");
|
|
46
|
-
|
|
47
|
-
return new elasticsearch.Client({
|
|
48
|
-
cloud: {
|
|
49
|
-
id: config.getString("cloudId")
|
|
50
|
-
},
|
|
51
|
-
auth: {
|
|
52
|
-
username: authConfig2.getString("username"),
|
|
53
|
-
password: authConfig2.getString("password")
|
|
54
|
-
},
|
|
55
|
-
...sslConfig ? {
|
|
56
|
-
ssl: {
|
|
57
|
-
rejectUnauthorized: sslConfig == null ? void 0 : sslConfig.getOptionalBoolean("rejectUnauthorized")
|
|
58
|
-
}
|
|
59
|
-
} : {}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
if (config.getOptionalString("provider") === "aws") {
|
|
40
|
+
} else if (options.provider === "aws") {
|
|
63
41
|
logger.info("Initializing AWS ElasticSearch search engine.");
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return new elasticsearch.Client({
|
|
67
|
-
node: config.getString("node"),
|
|
68
|
-
...AWSConnection,
|
|
69
|
-
...sslConfig ? {
|
|
70
|
-
ssl: {
|
|
71
|
-
rejectUnauthorized: sslConfig == null ? void 0 : sslConfig.getOptionalBoolean("rejectUnauthorized")
|
|
72
|
-
}
|
|
73
|
-
} : {}
|
|
74
|
-
});
|
|
42
|
+
} else {
|
|
43
|
+
logger.info("Initializing ElasticSearch search engine.");
|
|
75
44
|
}
|
|
76
|
-
|
|
77
|
-
const authConfig = config.getOptionalConfig("auth");
|
|
78
|
-
const auth = authConfig && (authConfig.has("apiKey") ? {
|
|
79
|
-
apiKey: authConfig.getString("apiKey")
|
|
80
|
-
} : {
|
|
81
|
-
username: authConfig.getString("username"),
|
|
82
|
-
password: authConfig.getString("password")
|
|
83
|
-
});
|
|
84
|
-
return new elasticsearch.Client({
|
|
85
|
-
node: config.getString("node"),
|
|
86
|
-
auth,
|
|
87
|
-
...sslConfig ? {
|
|
88
|
-
ssl: {
|
|
89
|
-
rejectUnauthorized: sslConfig == null ? void 0 : sslConfig.getOptionalBoolean("rejectUnauthorized")
|
|
90
|
-
}
|
|
91
|
-
} : {}
|
|
92
|
-
});
|
|
45
|
+
return new ElasticSearchSearchEngine(options, aliasPostfix, indexPrefix, logger);
|
|
93
46
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
pageCursor
|
|
99
|
-
}) {
|
|
47
|
+
newClient(create) {
|
|
48
|
+
return create(this.elasticSearchClientOptions);
|
|
49
|
+
}
|
|
50
|
+
translator(query) {
|
|
51
|
+
const { term, filters = {}, types, pageCursor } = query;
|
|
100
52
|
const filter = Object.entries(filters).filter(([_, value]) => Boolean(value)).map(([key, value]) => {
|
|
101
53
|
if (["string", "number", "boolean"].includes(typeof value)) {
|
|
102
|
-
return esb__default[
|
|
54
|
+
return esb__default["default"].matchQuery(key, value.toString());
|
|
103
55
|
}
|
|
104
56
|
if (Array.isArray(value)) {
|
|
105
|
-
return esb__default[
|
|
57
|
+
return esb__default["default"].boolQuery().should(value.map((it) => esb__default["default"].matchQuery(key, it.toString())));
|
|
106
58
|
}
|
|
107
59
|
this.logger.error("Failed to query, unrecognized filter type", key, value);
|
|
108
60
|
throw new Error("Failed to add filters to query. Unrecognized filter type");
|
|
109
61
|
});
|
|
110
|
-
const
|
|
62
|
+
const esbQuery = isBlank(term) ? esb__default["default"].matchAllQuery() : esb__default["default"].multiMatchQuery(["*"], term).fuzziness("auto").minimumShouldMatch(1);
|
|
111
63
|
const pageSize = 25;
|
|
112
|
-
const {page} = decodePageCursor(pageCursor);
|
|
64
|
+
const { page } = decodePageCursor(pageCursor);
|
|
113
65
|
return {
|
|
114
|
-
elasticSearchQuery: esb__default[
|
|
66
|
+
elasticSearchQuery: esb__default["default"].requestBodySearch().query(esb__default["default"].boolQuery().filter(filter).must([esbQuery])).from(page * pageSize).size(pageSize).toJSON(),
|
|
115
67
|
documentTypes: types,
|
|
116
68
|
pageSize
|
|
117
69
|
};
|
|
@@ -137,7 +89,7 @@ class ElasticSearchSearchEngine {
|
|
|
137
89
|
datasource: documents,
|
|
138
90
|
onDocument() {
|
|
139
91
|
return {
|
|
140
|
-
index: {_index: index}
|
|
92
|
+
index: { _index: index }
|
|
141
93
|
};
|
|
142
94
|
},
|
|
143
95
|
refreshOnCompletion: index
|
|
@@ -146,8 +98,8 @@ class ElasticSearchSearchEngine {
|
|
|
146
98
|
await this.elasticSearchClient.indices.updateAliases({
|
|
147
99
|
body: {
|
|
148
100
|
actions: [
|
|
149
|
-
{remove: {index: this.constructIndexName(type, "*"), alias}},
|
|
150
|
-
{add: {index, alias}}
|
|
101
|
+
{ remove: { index: this.constructIndexName(type, "*"), alias } },
|
|
102
|
+
{ add: { index, alias } }
|
|
151
103
|
]
|
|
152
104
|
}
|
|
153
105
|
});
|
|
@@ -172,18 +124,18 @@ class ElasticSearchSearchEngine {
|
|
|
172
124
|
}
|
|
173
125
|
}
|
|
174
126
|
async query(query) {
|
|
175
|
-
const {elasticSearchQuery, documentTypes, pageSize} = this.translator(query);
|
|
127
|
+
const { elasticSearchQuery, documentTypes, pageSize } = this.translator(query);
|
|
176
128
|
const queryIndices = documentTypes ? documentTypes.map((it) => this.constructSearchAlias(it)) : this.constructSearchAlias("*");
|
|
177
129
|
try {
|
|
178
130
|
const result = await this.elasticSearchClient.search({
|
|
179
131
|
index: queryIndices,
|
|
180
132
|
body: elasticSearchQuery
|
|
181
133
|
});
|
|
182
|
-
const {page} = decodePageCursor(query.pageCursor);
|
|
134
|
+
const { page } = decodePageCursor(query.pageCursor);
|
|
183
135
|
const hasNextPage = result.body.hits.total.value > page * pageSize;
|
|
184
136
|
const hasPreviousPage = page > 0;
|
|
185
|
-
const nextPageCursor = hasNextPage ? encodePageCursor({page: page + 1}) : void 0;
|
|
186
|
-
const previousPageCursor = hasPreviousPage ? encodePageCursor({page: page - 1}) : void 0;
|
|
137
|
+
const nextPageCursor = hasNextPage ? encodePageCursor({ page: page + 1 }) : void 0;
|
|
138
|
+
const previousPageCursor = hasPreviousPage ? encodePageCursor({ page: page - 1 }) : void 0;
|
|
187
139
|
return {
|
|
188
140
|
results: result.body.hits.hits.map((d) => ({
|
|
189
141
|
type: this.getTypeFromIndex(d._index),
|
|
@@ -194,7 +146,7 @@ class ElasticSearchSearchEngine {
|
|
|
194
146
|
};
|
|
195
147
|
} catch (e) {
|
|
196
148
|
this.logger.error(`Failed to query documents for indices ${queryIndices}`, e);
|
|
197
|
-
return Promise.reject({results: []});
|
|
149
|
+
return Promise.reject({ results: [] });
|
|
198
150
|
}
|
|
199
151
|
}
|
|
200
152
|
constructIndexName(type, postFix) {
|
|
@@ -210,15 +162,70 @@ class ElasticSearchSearchEngine {
|
|
|
210
162
|
}
|
|
211
163
|
function decodePageCursor(pageCursor) {
|
|
212
164
|
if (!pageCursor) {
|
|
213
|
-
return {page: 0};
|
|
165
|
+
return { page: 0 };
|
|
214
166
|
}
|
|
215
167
|
return {
|
|
216
168
|
page: Number(Buffer.from(pageCursor, "base64").toString("utf-8"))
|
|
217
169
|
};
|
|
218
170
|
}
|
|
219
|
-
function encodePageCursor({page}) {
|
|
171
|
+
function encodePageCursor({ page }) {
|
|
220
172
|
return Buffer.from(`${page}`, "utf-8").toString("base64");
|
|
221
173
|
}
|
|
174
|
+
async function createElasticSearchClientOptions(config) {
|
|
175
|
+
if (!config) {
|
|
176
|
+
throw new Error("No elastic search config found");
|
|
177
|
+
}
|
|
178
|
+
const clientOptionsConfig = config.getOptionalConfig("clientOptions");
|
|
179
|
+
const sslConfig = clientOptionsConfig == null ? void 0 : clientOptionsConfig.getOptionalConfig("ssl");
|
|
180
|
+
if (config.getOptionalString("provider") === "elastic") {
|
|
181
|
+
const authConfig2 = config.getConfig("auth");
|
|
182
|
+
return {
|
|
183
|
+
provider: "elastic",
|
|
184
|
+
cloud: {
|
|
185
|
+
id: config.getString("cloudId")
|
|
186
|
+
},
|
|
187
|
+
auth: {
|
|
188
|
+
username: authConfig2.getString("username"),
|
|
189
|
+
password: authConfig2.getString("password")
|
|
190
|
+
},
|
|
191
|
+
...sslConfig ? {
|
|
192
|
+
ssl: {
|
|
193
|
+
rejectUnauthorized: sslConfig == null ? void 0 : sslConfig.getOptionalBoolean("rejectUnauthorized")
|
|
194
|
+
}
|
|
195
|
+
} : {}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (config.getOptionalString("provider") === "aws") {
|
|
199
|
+
const awsCredentials = await awsEsConnection.awsGetCredentials();
|
|
200
|
+
const AWSConnection = awsEsConnection.createAWSConnection(awsCredentials);
|
|
201
|
+
return {
|
|
202
|
+
provider: "aws",
|
|
203
|
+
node: config.getString("node"),
|
|
204
|
+
...AWSConnection,
|
|
205
|
+
...sslConfig ? {
|
|
206
|
+
ssl: {
|
|
207
|
+
rejectUnauthorized: sslConfig == null ? void 0 : sslConfig.getOptionalBoolean("rejectUnauthorized")
|
|
208
|
+
}
|
|
209
|
+
} : {}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const authConfig = config.getOptionalConfig("auth");
|
|
213
|
+
const auth = authConfig && (authConfig.has("apiKey") ? {
|
|
214
|
+
apiKey: authConfig.getString("apiKey")
|
|
215
|
+
} : {
|
|
216
|
+
username: authConfig.getString("username"),
|
|
217
|
+
password: authConfig.getString("password")
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
node: config.getString("node"),
|
|
221
|
+
auth,
|
|
222
|
+
...sslConfig ? {
|
|
223
|
+
ssl: {
|
|
224
|
+
rejectUnauthorized: sslConfig == null ? void 0 : sslConfig.getOptionalBoolean("rejectUnauthorized")
|
|
225
|
+
}
|
|
226
|
+
} : {}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
222
229
|
|
|
223
230
|
exports.ElasticSearchSearchEngine = ElasticSearchSearchEngine;
|
|
224
231
|
//# sourceMappingURL=index.cjs.js.map
|
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\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\nexport class ElasticSearchSearchEngine implements SearchEngine {\n constructor(\n private readonly elasticSearchClient: Client,\n private readonly aliasPostfix: string,\n private readonly indexPrefix: string,\n private readonly logger: Logger,\n ) {}\n\n static async fromConfig({\n logger,\n config,\n aliasPostfix = `search`,\n indexPrefix = ``,\n }: ElasticSearchOptions) {\n return new ElasticSearchSearchEngine(\n await ElasticSearchSearchEngine.constructElasticSearchClient(\n logger,\n config.getConfig('search.elasticsearch'),\n ),\n aliasPostfix,\n indexPrefix,\n logger,\n );\n }\n\n private static async constructElasticSearchClient(\n logger: Logger,\n config?: Config,\n ) {\n if (!config) {\n throw new Error('No elastic search config found');\n }\n\n const clientOptionsConfig = config.getOptionalConfig('clientOptions');\n const sslConfig = clientOptionsConfig?.getOptionalConfig('ssl');\n\n if (config.getOptionalString('provider') === 'elastic') {\n logger.info('Initializing Elastic.co ElasticSearch search engine.');\n const authConfig = config.getConfig('auth');\n return new Client({\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 logger.info('Initializing AWS ElasticSearch search engine.');\n const awsCredentials = await awsGetCredentials();\n const AWSConnection = createAWSConnection(awsCredentials);\n return new Client({\n node: config.getString('node'),\n ...AWSConnection,\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n });\n }\n logger.info('Initializing ElasticSearch search engine.');\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 new Client({\n node: config.getString('node'),\n auth,\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n });\n }\n\n protected translator({\n term,\n filters = {},\n types,\n pageCursor,\n }: SearchQuery): ConcreteElasticSearchQuery {\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 query = 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([query]))\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"],"names":["isEmpty","isNumber","nan","Client","awsGetCredentials","createAWSConnection","esb"],"mappings":"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"}
|
|
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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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,100 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { Config } from '@backstage/config';
|
|
2
3
|
import { SearchEngine, SearchQuery, IndexableDocument, SearchResultSet } from '@backstage/search-common';
|
|
3
|
-
import { Client } from '@elastic/elasticsearch';
|
|
4
4
|
import { Logger } from 'winston';
|
|
5
|
+
import { ConnectionOptions } from 'tls';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options used to configure the `@elastic/elasticsearch` client and
|
|
9
|
+
* are what will be passed as an argument to the
|
|
10
|
+
* {@link ElasticSearchEngine.newClient} method
|
|
11
|
+
*
|
|
12
|
+
* They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`,
|
|
13
|
+
* but are maintained separately so that this interface is not coupled to
|
|
14
|
+
*/
|
|
15
|
+
interface ElasticSearchClientOptions {
|
|
16
|
+
provider?: 'aws' | 'elastic';
|
|
17
|
+
node?: string | string[] | ElasticSearchNodeOptions | ElasticSearchNodeOptions[];
|
|
18
|
+
nodes?: string | string[] | ElasticSearchNodeOptions | ElasticSearchNodeOptions[];
|
|
19
|
+
Transport?: ElasticSearchTransportConstructor;
|
|
20
|
+
Connection?: ElasticSearchConnectionConstructor;
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
requestTimeout?: number;
|
|
23
|
+
pingTimeout?: number;
|
|
24
|
+
sniffInterval?: number | boolean;
|
|
25
|
+
sniffOnStart?: boolean;
|
|
26
|
+
sniffEndpoint?: string;
|
|
27
|
+
sniffOnConnectionFault?: boolean;
|
|
28
|
+
resurrectStrategy?: 'ping' | 'optimistic' | 'none';
|
|
29
|
+
suggestCompression?: boolean;
|
|
30
|
+
compression?: 'gzip';
|
|
31
|
+
ssl?: ConnectionOptions;
|
|
32
|
+
agent?: ElasticSearchAgentOptions | ((opts?: any) => unknown) | false;
|
|
33
|
+
nodeFilter?: (connection: any) => boolean;
|
|
34
|
+
nodeSelector?: ((connections: any[]) => any) | string;
|
|
35
|
+
headers?: Record<string, any>;
|
|
36
|
+
opaqueIdPrefix?: string;
|
|
37
|
+
name?: string | symbol;
|
|
38
|
+
auth?: ElasticSearchAuth;
|
|
39
|
+
proxy?: string | URL;
|
|
40
|
+
enableMetaHeader?: boolean;
|
|
41
|
+
cloud?: {
|
|
42
|
+
id: string;
|
|
43
|
+
username?: string;
|
|
44
|
+
password?: string;
|
|
45
|
+
};
|
|
46
|
+
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
|
|
47
|
+
}
|
|
48
|
+
declare type ElasticSearchAuth = {
|
|
49
|
+
username: string;
|
|
50
|
+
password: string;
|
|
51
|
+
} | {
|
|
52
|
+
apiKey: string | {
|
|
53
|
+
id: string;
|
|
54
|
+
api_key: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
interface ElasticSearchNodeOptions {
|
|
58
|
+
url: URL;
|
|
59
|
+
id?: string;
|
|
60
|
+
agent?: ElasticSearchAgentOptions;
|
|
61
|
+
ssl?: ConnectionOptions;
|
|
62
|
+
headers?: Record<string, any>;
|
|
63
|
+
roles?: {
|
|
64
|
+
master: boolean;
|
|
65
|
+
data: boolean;
|
|
66
|
+
ingest: boolean;
|
|
67
|
+
ml: boolean;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
interface ElasticSearchAgentOptions {
|
|
71
|
+
keepAlive?: boolean;
|
|
72
|
+
keepAliveMsecs?: number;
|
|
73
|
+
maxSockets?: number;
|
|
74
|
+
maxFreeSockets?: number;
|
|
75
|
+
}
|
|
76
|
+
interface ElasticSearchConnectionConstructor {
|
|
77
|
+
new (opts?: any): any;
|
|
78
|
+
statuses: {
|
|
79
|
+
ALIVE: string;
|
|
80
|
+
DEAD: string;
|
|
81
|
+
};
|
|
82
|
+
roles: {
|
|
83
|
+
MASTER: string;
|
|
84
|
+
DATA: string;
|
|
85
|
+
INGEST: string;
|
|
86
|
+
ML: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
interface ElasticSearchTransportConstructor {
|
|
90
|
+
new (opts?: any): any;
|
|
91
|
+
sniffReasons: {
|
|
92
|
+
SNIFF_ON_START: string;
|
|
93
|
+
SNIFF_INTERVAL: string;
|
|
94
|
+
SNIFF_ON_CONNECTION_FAULT: string;
|
|
95
|
+
DEFAULT: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
5
98
|
|
|
6
99
|
declare type ConcreteElasticSearchQuery = {
|
|
7
100
|
documentTypes?: string[];
|
|
@@ -15,15 +108,24 @@ declare type ElasticSearchOptions = {
|
|
|
15
108
|
aliasPostfix?: string;
|
|
16
109
|
indexPrefix?: string;
|
|
17
110
|
};
|
|
111
|
+
/**
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
18
114
|
declare class ElasticSearchSearchEngine implements SearchEngine {
|
|
19
|
-
private readonly
|
|
115
|
+
private readonly elasticSearchClientOptions;
|
|
20
116
|
private readonly aliasPostfix;
|
|
21
117
|
private readonly indexPrefix;
|
|
22
118
|
private readonly logger;
|
|
23
|
-
|
|
119
|
+
private readonly elasticSearchClient;
|
|
120
|
+
constructor(elasticSearchClientOptions: ElasticSearchClientOptions, aliasPostfix: string, indexPrefix: string, logger: Logger);
|
|
24
121
|
static fromConfig({ logger, config, aliasPostfix, indexPrefix, }: ElasticSearchOptions): Promise<ElasticSearchSearchEngine>;
|
|
25
|
-
|
|
26
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Create a custom search client from the derived elastic search
|
|
124
|
+
* configuration. This need not be the same client that the engine uses
|
|
125
|
+
* internally.
|
|
126
|
+
*/
|
|
127
|
+
newClient<T>(create: (options: ElasticSearchClientOptions) => T): T;
|
|
128
|
+
protected translator(query: SearchQuery): ConcreteElasticSearchQuery;
|
|
27
129
|
setTranslator(translator: ElasticSearchQueryTranslator): void;
|
|
28
130
|
index(type: string, documents: IndexableDocument[]): Promise<void>;
|
|
29
131
|
query(query: SearchQuery): Promise<SearchResultSet>;
|
|
@@ -33,4 +135,4 @@ declare class ElasticSearchSearchEngine implements SearchEngine {
|
|
|
33
135
|
private constructSearchAlias;
|
|
34
136
|
}
|
|
35
137
|
|
|
36
|
-
export { ElasticSearchSearchEngine };
|
|
138
|
+
export { ElasticSearchClientOptions, ElasticSearchSearchEngine };
|
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.0.8",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@acuris/aws-es-connection": "^2.2.0",
|
|
24
|
-
"@backstage/config": "^0.1.
|
|
24
|
+
"@backstage/config": "^0.1.13",
|
|
25
25
|
"@backstage/search-common": "^0.2.0",
|
|
26
26
|
"@elastic/elasticsearch": "7.13.0",
|
|
27
27
|
"aws-sdk": "^2.948.0",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"winston": "^3.2.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@backstage/backend-common": "^0.
|
|
34
|
-
"@backstage/cli": "^0.
|
|
33
|
+
"@backstage/backend-common": "^0.10.4",
|
|
34
|
+
"@backstage/cli": "^0.12.0",
|
|
35
35
|
"@elastic/elasticsearch-mock": "^0.3.0"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"testEnvironment": "node"
|
|
43
43
|
},
|
|
44
44
|
"configSchema": "config.d.ts",
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "600d6e3c854bbfb12a0078ca6f726d1c0d1fea0b"
|
|
46
46
|
}
|