@backstage/plugin-search-backend-module-elasticsearch 0.0.9 → 0.1.1-next.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 CHANGED
@@ -1,5 +1,42 @@
1
1
  # @backstage/plugin-search-backend-module-elasticsearch
2
2
 
3
+ ## 0.1.1-next.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 3e54f6c436: Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`.
8
+ - Updated dependencies
9
+ - @backstage/plugin-search-common@0.3.1-next.0
10
+ - @backstage/plugin-search-backend-node@0.5.1-next.0
11
+
12
+ ## 0.1.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 022507c860: **BREAKING**
17
+
18
+ The `ElasticSearchSearchEngine` implements the new stream-based indexing
19
+ process expected by the latest `@backstage/search-backend-node`.
20
+
21
+ When updating to this version, you must also update to the latest version of
22
+ `@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)
23
+ for further details.
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies
28
+ - @backstage/plugin-search-backend-node@0.5.0
29
+ - @backstage/search-common@0.3.0
30
+
31
+ ## 0.0.10
32
+
33
+ ### Patch Changes
34
+
35
+ - Fix for the previous release with missing type declarations.
36
+ - Updated dependencies
37
+ - @backstage/config@0.1.15
38
+ - @backstage/search-common@0.2.4
39
+
3
40
  ## 0.0.9
4
41
 
5
42
  ### 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 index(type, documents) {
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 index = this.constructIndexName(type, `${Date.now()}`);
79
- try {
80
- const aliases = await this.elasticSearchClient.cat.aliases({
81
- format: "json",
82
- name: alias
83
- });
84
- const removableIndices = aliases.body.map((r) => r.index);
85
- await this.elasticSearchClient.indices.create({
86
- index
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
- const response = await this.elasticSearchClient.indices.exists({
115
- index
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
  }
@@ -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":";;;;;;;;;;;;;AA4DA,kBAAkB,gBAA0C;AAC1D,QAAM,QAAQ,QAAQ,OAAO;AAC7B,QAAM,UAAU,MAAM,KAAK,MAAM,KAAK;AACtC,SAAO,GAAG,QAAQ,QAAQ;AAAA;AAG5B,iBAAiB,KAAa;AAC5B,SAAQA,eAAQ,QAAQ,CAACC,gBAAS,QAASC,aAAI;AAAA;gCAMc;AAAA,EAG7D,YACmB,4BACA,cACA,aACA,QACjB;AAJiB;AACA;AACA;AACA;AA4LF,0BAAiB;AA1LhC,SAAK,sBAAsB,KAAK,UAAU,aAAW,IAAIC,qBAAO;AAAA;AAAA,eAGrD,WAAW;AAAA,IACtB;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,cAAc;AAAA,KACS;AACvB,UAAM,UAAU,MAAM,iCACpB,OAAO,UAAU;AAEnB,QAAI,QAAQ,aAAa,WAAW;AAClC,aAAO,KAAK;AAAA,eACH,QAAQ,aAAa,OAAO;AACrC,aAAO,KAAK;AAAA,WACP;AACL,aAAO,KAAK;AAAA;AAGd,WAAO,IAAI,0BACT,SACA,cACA,aACA;AAAA;AAAA,EASJ,UAAa,QAAuD;AAClE,WAAO,OAAO,KAAK;AAAA;AAAA,EAGX,WAAW,OAAgD;AACnE,UAAM,EAAE,MAAM,UAAU,IAAI,OAAO,eAAe;AAElD,UAAM,SAAS,OAAO,QAAQ,SAC3B,OAAO,CAAC,CAAC,GAAG,WAAW,QAAQ,QAC/B,IAAI,CAAC,CAAC,KAAK,WAAsC;AAChD,UAAI,CAAC,UAAU,UAAU,WAAW,SAAS,OAAO,QAAQ;AAC1D,eAAOC,wBAAI,WAAW,KAAK,MAAM;AAAA;AAEnC,UAAI,MAAM,QAAQ,QAAQ;AACxB,eAAOA,wBACJ,YACA,OAAO,MAAM,IAAI,QAAMA,wBAAI,WAAW,KAAK,GAAG;AAAA;AAEnD,WAAK,OAAO,MACV,6CACA,KACA;AAEF,YAAM,IAAI,MACR;AAAA;AAGN,UAAM,WAAW,QAAQ,QACrBA,wBAAI,kBACJA,wBACG,gBAAgB,CAAC,MAAM,MACvB,UAAU,QACV,mBAAmB;AAC1B,UAAM,WAAW;AACjB,UAAM,EAAE,SAAS,iBAAiB;AAElC,WAAO;AAAA,MACL,oBAAoBA,wBACjB,oBACA,MAAMA,wBAAI,YAAY,OAAO,QAAQ,KAAK,CAAC,YAC3C,KAAK,OAAO,UACZ,KAAK,UACL;AAAA,MACH,eAAe;AAAA,MACf;AAAA;AAAA;AAAA,EAIJ,cAAc,YAA0C;AACtD,SAAK,aAAa;AAAA;AAAA,QAGd,MAAM,MAAc,WAA+C;AACvE,SAAK,OAAO,KACV,oBAAoB,UAAU,8BAA8B;AAE9D,UAAM,iBAAiB,QAAQ;AAC/B,UAAM,QAAQ,KAAK,qBAAqB;AACxC,UAAM,QAAQ,KAAK,mBAAmB,MAAM,GAAG,KAAK;AACpD,QAAI;AACF,YAAM,UAAU,MAAM,KAAK,oBAAoB,IAAI,QAAQ;AAAA,QACzD,QAAQ;AAAA,QACR,MAAM;AAAA;AAER,YAAM,mBAAmB,QAAQ,KAAK,IACpC,CAAC,MAA2B,EAAE;AAGhC,YAAM,KAAK,oBAAoB,QAAQ,OAAO;AAAA,QAC5C;AAAA;AAEF,YAAM,SAAS,MAAM,KAAK,oBAAoB,QAAQ,KAAK;AAAA,QACzD,YAAY;AAAA,QACZ,aAAa;AACX,iBAAO;AAAA,YACL,OAAO,EAAE,QAAQ;AAAA;AAAA;AAAA,QAGrB,qBAAqB;AAAA;AAGvB,WAAK,OAAO,KACV,gCAAgC,WAAW,SAAS,mBACpD;AAEF,YAAM,KAAK,oBAAoB,QAAQ,cAAc;AAAA,QACnD,MAAM;AAAA,UACJ,SAAS;AAAA,YACP,EAAE,QAAQ,EAAE,OAAO,KAAK,mBAAmB,MAAM,MAAM;AAAA,YACvD,EAAE,KAAK,EAAE,OAAO;AAAA;AAAA;AAAA;AAKtB,WAAK,OAAO,KAAK,iCAAiC;AAClD,UAAI,iBAAiB,QAAQ;AAC3B,cAAM,KAAK,oBAAoB,QAAQ,OAAO;AAAA,UAC5C,OAAO;AAAA;AAAA;AAAA,aAGJ,GAAP;AACA,WAAK,OAAO,MAAM,sCAAsC,QAAQ;AAChE,YAAM,WAAW,MAAM,KAAK,oBAAoB,QAAQ,OAAO;AAAA,QAC7D;AAAA;AAEF,YAAM,eAAe,SAAS;AAC9B,UAAI,cAAc;AAChB,aAAK,OAAO,KAAK,0BAA0B;AAC3C,cAAM,KAAK,oBAAoB,QAAQ,OAAO;AAAA,UAC5C;AAAA;AAAA;AAAA;AAAA;AAAA,QAMF,MAAM,OAA8C;AACxD,UAAM,EAAE,oBAAoB,eAAe,aACzC,KAAK,WAAW;AAClB,UAAM,eAAe,gBACjB,cAAc,IAAI,QAAM,KAAK,qBAAqB,OAClD,KAAK,qBAAqB;AAC9B,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,oBAAoB,OAAO;AAAA,QACnD,OAAO;AAAA,QACP,MAAM;AAAA;AAER,YAAM,EAAE,SAAS,iBAAiB,MAAM;AACxC,YAAM,cAAc,OAAO,KAAK,KAAK,MAAM,QAAQ,OAAO;AAC1D,YAAM,kBAAkB,OAAO;AAC/B,YAAM,iBAAiB,cACnB,iBAAiB,EAAE,MAAM,OAAO,OAChC;AACJ,YAAM,qBAAqB,kBACvB,iBAAiB,EAAE,MAAM,OAAO,OAChC;AAEJ,aAAO;AAAA,QACL,SAAS,OAAO,KAAK,KAAK,KAAK,IAAI,CAAC;AAA4B,UAC9D,MAAM,KAAK,iBAAiB,EAAE;AAAA,UAC9B,UAAU,EAAE;AAAA;AAAA,QAEd;AAAA,QACA;AAAA;AAAA,aAEK,GAAP;AACA,WAAK,OAAO,MACV,yCAAyC,gBACzC;AAEF,aAAO,QAAQ,OAAO,EAAE,SAAS;AAAA;AAAA;AAAA,EAM7B,mBAAmB,MAAc,SAAiB;AACxD,WAAO,GAAG,KAAK,cAAc,OAAO,KAAK,iBAAiB;AAAA;AAAA,EAGpD,iBAAiB,OAAe;AACtC,WAAO,MACJ,UAAU,KAAK,YAAY,QAC3B,MAAM,KAAK,gBAAgB;AAAA;AAAA,EAGxB,qBAAqB,MAAc;AACzC,UAAM,UAAU,KAAK,eAAe,KAAK,KAAK,iBAAiB;AAC/D,WAAO,GAAG,KAAK,cAAc,OAAO;AAAA;AAAA;0BAIP,YAAuC;AACtE,MAAI,CAAC,YAAY;AACf,WAAO,EAAE,MAAM;AAAA;AAGjB,SAAO;AAAA,IACL,MAAM,OAAO,OAAO,KAAK,YAAY,UAAU,SAAS;AAAA;AAAA;0BAI3B,EAAE,QAAkC;AACnE,SAAO,OAAO,KAAK,GAAG,QAAQ,SAAS,SAAS;AAAA;AAGlD,gDACE,QACqC;AACrC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM;AAAA;AAElB,QAAM,sBAAsB,OAAO,kBAAkB;AACrD,QAAM,YAAY,2DAAqB,kBAAkB;AAEzD,MAAI,OAAO,kBAAkB,gBAAgB,WAAW;AACtD,UAAM,cAAa,OAAO,UAAU;AACpC,WAAO;AAAA,MACL,UAAU;AAAA,MACV,OAAO;AAAA,QACL,IAAI,OAAO,UAAU;AAAA;AAAA,MAEvB,MAAM;AAAA,QACJ,UAAU,YAAW,UAAU;AAAA,QAC/B,UAAU,YAAW,UAAU;AAAA;AAAA,SAE7B,YACA;AAAA,QACE,KAAK;AAAA,UACH,oBACE,uCAAW,mBAAmB;AAAA;AAAA,UAGpC;AAAA;AAAA;AAGR,MAAI,OAAO,kBAAkB,gBAAgB,OAAO;AAClD,UAAM,iBAAiB,MAAMC;AAC7B,UAAM,gBAAgBC,oCAAoB;AAC1C,WAAO;AAAA,MACL,UAAU;AAAA,MACV,MAAM,OAAO,UAAU;AAAA,SACpB;AAAA,SACC,YACA;AAAA,QACE,KAAK;AAAA,UACH,oBACE,uCAAW,mBAAmB;AAAA;AAAA,UAGpC;AAAA;AAAA;AAGR,QAAM,aAAa,OAAO,kBAAkB;AAC5C,QAAM,OACJ,0BACY,IAAI,YACZ;AAAA,IACE,QAAQ,WAAW,UAAU;AAAA,MAE/B;AAAA,IACE,UAAU,WAAW,UAAU;AAAA,IAC/B,UAAU,WAAW,UAAU;AAAA;AAEvC,SAAO;AAAA,IACL,MAAM,OAAO,UAAU;AAAA,IACvB;AAAA,OACI,YACA;AAAA,MACE,KAAK;AAAA,QACH,oBACE,uCAAW,mBAAmB;AAAA;AAAA,QAGpC;AAAA;AAAA;;;;"}
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/plugin-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/plugin-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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"}
@@ -0,0 +1,174 @@
1
+ /// <reference types="node" />
2
+ import { Config } from '@backstage/config';
3
+ import { IndexableDocument, SearchEngine, SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
4
+ import { Logger } from 'winston';
5
+ import { ConnectionOptions } from 'tls';
6
+ import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';
7
+ import { Client } from '@elastic/elasticsearch';
8
+
9
+ /**
10
+ * Options used to configure the `@elastic/elasticsearch` client and
11
+ * are what will be passed as an argument to the
12
+ * {@link ElasticSearchEngine.newClient} method
13
+ *
14
+ * They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`,
15
+ * but are maintained separately so that this interface is not coupled to
16
+ */
17
+ interface ElasticSearchClientOptions {
18
+ provider?: 'aws' | 'elastic';
19
+ node?: string | string[] | ElasticSearchNodeOptions | ElasticSearchNodeOptions[];
20
+ nodes?: string | string[] | ElasticSearchNodeOptions | ElasticSearchNodeOptions[];
21
+ Transport?: ElasticSearchTransportConstructor;
22
+ Connection?: ElasticSearchConnectionConstructor;
23
+ maxRetries?: number;
24
+ requestTimeout?: number;
25
+ pingTimeout?: number;
26
+ sniffInterval?: number | boolean;
27
+ sniffOnStart?: boolean;
28
+ sniffEndpoint?: string;
29
+ sniffOnConnectionFault?: boolean;
30
+ resurrectStrategy?: 'ping' | 'optimistic' | 'none';
31
+ suggestCompression?: boolean;
32
+ compression?: 'gzip';
33
+ ssl?: ConnectionOptions;
34
+ agent?: ElasticSearchAgentOptions | ((opts?: any) => unknown) | false;
35
+ nodeFilter?: (connection: any) => boolean;
36
+ nodeSelector?: ((connections: any[]) => any) | string;
37
+ headers?: Record<string, any>;
38
+ opaqueIdPrefix?: string;
39
+ name?: string | symbol;
40
+ auth?: ElasticSearchAuth;
41
+ proxy?: string | URL;
42
+ enableMetaHeader?: boolean;
43
+ cloud?: {
44
+ id: string;
45
+ username?: string;
46
+ password?: string;
47
+ };
48
+ disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
49
+ }
50
+ declare type ElasticSearchAuth = {
51
+ username: string;
52
+ password: string;
53
+ } | {
54
+ apiKey: string | {
55
+ id: string;
56
+ api_key: string;
57
+ };
58
+ };
59
+ interface ElasticSearchNodeOptions {
60
+ url: URL;
61
+ id?: string;
62
+ agent?: ElasticSearchAgentOptions;
63
+ ssl?: ConnectionOptions;
64
+ headers?: Record<string, any>;
65
+ roles?: {
66
+ master: boolean;
67
+ data: boolean;
68
+ ingest: boolean;
69
+ ml: boolean;
70
+ };
71
+ }
72
+ interface ElasticSearchAgentOptions {
73
+ keepAlive?: boolean;
74
+ keepAliveMsecs?: number;
75
+ maxSockets?: number;
76
+ maxFreeSockets?: number;
77
+ }
78
+ interface ElasticSearchConnectionConstructor {
79
+ new (opts?: any): any;
80
+ statuses: {
81
+ ALIVE: string;
82
+ DEAD: string;
83
+ };
84
+ roles: {
85
+ MASTER: string;
86
+ DATA: string;
87
+ INGEST: string;
88
+ ML: string;
89
+ };
90
+ }
91
+ interface ElasticSearchTransportConstructor {
92
+ new (opts?: any): any;
93
+ sniffReasons: {
94
+ SNIFF_ON_START: string;
95
+ SNIFF_INTERVAL: string;
96
+ SNIFF_ON_CONNECTION_FAULT: string;
97
+ DEFAULT: string;
98
+ };
99
+ }
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
+
136
+ declare type ConcreteElasticSearchQuery = {
137
+ documentTypes?: string[];
138
+ elasticSearchQuery: Object;
139
+ pageSize: number;
140
+ };
141
+ declare type ElasticSearchQueryTranslator = (query: SearchQuery) => ConcreteElasticSearchQuery;
142
+ declare type ElasticSearchOptions = {
143
+ logger: Logger;
144
+ config: Config;
145
+ aliasPostfix?: string;
146
+ indexPrefix?: string;
147
+ };
148
+ /**
149
+ * @public
150
+ */
151
+ declare class ElasticSearchSearchEngine implements SearchEngine {
152
+ private readonly elasticSearchClientOptions;
153
+ private readonly aliasPostfix;
154
+ private readonly indexPrefix;
155
+ private readonly logger;
156
+ private readonly elasticSearchClient;
157
+ constructor(elasticSearchClientOptions: ElasticSearchClientOptions, aliasPostfix: string, indexPrefix: string, logger: Logger);
158
+ static fromConfig({ logger, config, aliasPostfix, indexPrefix, }: ElasticSearchOptions): Promise<ElasticSearchSearchEngine>;
159
+ /**
160
+ * Create a custom search client from the derived elastic search
161
+ * configuration. This need not be the same client that the engine uses
162
+ * internally.
163
+ */
164
+ newClient<T>(create: (options: ElasticSearchClientOptions) => T): T;
165
+ protected translator(query: SearchQuery): ConcreteElasticSearchQuery;
166
+ setTranslator(translator: ElasticSearchQueryTranslator): void;
167
+ getIndexer(type: string): Promise<ElasticSearchSearchEngineIndexer>;
168
+ query(query: SearchQuery): Promise<SearchResultSet>;
169
+ private readonly indexSeparator;
170
+ private getTypeFromIndex;
171
+ private constructSearchAlias;
172
+ }
173
+
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.9",
4
+ "version": "0.1.1-next.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "Apache-2.0",
@@ -24,8 +24,9 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@acuris/aws-es-connection": "^2.2.0",
27
- "@backstage/config": "^0.1.14",
28
- "@backstage/search-common": "^0.2.3",
27
+ "@backstage/config": "^0.1.15",
28
+ "@backstage/plugin-search-backend-node": "^0.5.1-next.0",
29
+ "@backstage/plugin-search-common": "^0.3.1-next.0",
29
30
  "@elastic/elasticsearch": "7.13.0",
30
31
  "aws-sdk": "^2.948.0",
31
32
  "elastic-builder": "^2.16.0",
@@ -33,8 +34,8 @@
33
34
  "winston": "^3.2.1"
34
35
  },
35
36
  "devDependencies": {
36
- "@backstage/backend-common": "^0.10.8",
37
- "@backstage/cli": "^0.14.0",
37
+ "@backstage/backend-common": "^0.13.0-next.0",
38
+ "@backstage/cli": "^0.15.2-next.0",
38
39
  "@elastic/elasticsearch-mock": "^1.0.0"
39
40
  },
40
41
  "files": [
@@ -45,5 +46,5 @@
45
46
  "testEnvironment": "node"
46
47
  },
47
48
  "configSchema": "config.d.ts",
48
- "gitHead": "4805c3d13ce9bfc369e53c271b1b95e722b3b4dc"
49
+ "gitHead": "e90d3ed129ebfce978f1adfa40c1dc2cef3f7e65"
49
50
  }