Refactor Elasticsearch client calls into client wrapper and break out types
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -5,60 +5,27 @@
|
||||
```ts
|
||||
/// <reference types="node" />
|
||||
|
||||
import { ApiResponse } from '@opensearch-project/opensearch';
|
||||
import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import { BulkHelper } from '@opensearch-project/opensearch/lib/Helpers';
|
||||
import { BulkStats } from '@opensearch-project/opensearch/lib/Helpers';
|
||||
import { Config } from '@backstage/config';
|
||||
import type { ConnectionOptions } from 'tls';
|
||||
import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
import { IndexableResultSet } from '@backstage/plugin-search-common';
|
||||
import { Logger } from 'winston';
|
||||
import { Readable } from 'stream';
|
||||
import { SearchEngine } from '@backstage/plugin-search-common';
|
||||
import { SearchQuery } from '@backstage/plugin-search-common';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchAgentOptions {
|
||||
// (undocumented)
|
||||
keepAlive?: boolean;
|
||||
// (undocumented)
|
||||
keepAliveMsecs?: number;
|
||||
// (undocumented)
|
||||
maxFreeSockets?: number;
|
||||
// (undocumented)
|
||||
maxSockets?: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchAuth =
|
||||
| {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
| {
|
||||
apiKey:
|
||||
| string
|
||||
| {
|
||||
id: string;
|
||||
api_key: string;
|
||||
};
|
||||
};
|
||||
import { TransportRequestPromise } from '@opensearch-project/opensearch/lib/Transport';
|
||||
|
||||
// @public
|
||||
export interface ElasticSearchClientOptions {
|
||||
export interface BaseElasticSearchClientOptions {
|
||||
// (undocumented)
|
||||
agent?: ElasticSearchAgentOptions | ((opts?: any) => unknown) | false;
|
||||
// (undocumented)
|
||||
auth?: ElasticSearchAuth;
|
||||
// (undocumented)
|
||||
cloud?: {
|
||||
id: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
// (undocumented)
|
||||
compression?: 'gzip';
|
||||
// (undocumented)
|
||||
Connection?: ElasticSearchConnectionConstructor;
|
||||
// (undocumented)
|
||||
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
|
||||
// (undocumented)
|
||||
enableMetaHeader?: boolean;
|
||||
@@ -69,28 +36,14 @@ export interface ElasticSearchClientOptions {
|
||||
// (undocumented)
|
||||
name?: string | symbol;
|
||||
// (undocumented)
|
||||
node?:
|
||||
| string
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
nodeFilter?: (connection: any) => boolean;
|
||||
// (undocumented)
|
||||
nodes?:
|
||||
| string
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
nodeSelector?: ((connections: any[]) => any) | string;
|
||||
// (undocumented)
|
||||
opaqueIdPrefix?: string;
|
||||
// (undocumented)
|
||||
pingTimeout?: number;
|
||||
// (undocumented)
|
||||
provider?: 'aws' | 'elastic';
|
||||
// (undocumented)
|
||||
proxy?: string | URL;
|
||||
// (undocumented)
|
||||
requestTimeout?: number;
|
||||
@@ -112,6 +65,118 @@ export interface ElasticSearchClientOptions {
|
||||
Transport?: ElasticSearchTransportConstructor;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchAgentOptions {
|
||||
// (undocumented)
|
||||
keepAlive?: boolean;
|
||||
// (undocumented)
|
||||
keepAliveMsecs?: number;
|
||||
// (undocumented)
|
||||
maxFreeSockets?: number;
|
||||
// (undocumented)
|
||||
maxSockets?: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchAliasAction =
|
||||
| {
|
||||
remove: {
|
||||
index: any;
|
||||
alias: any;
|
||||
};
|
||||
add?: undefined;
|
||||
}
|
||||
| {
|
||||
add: {
|
||||
indices: any;
|
||||
alias: any;
|
||||
index?: undefined;
|
||||
};
|
||||
remove?: undefined;
|
||||
}
|
||||
| {
|
||||
add: {
|
||||
index: any;
|
||||
alias: any;
|
||||
indices?: undefined;
|
||||
};
|
||||
remove?: undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchAuth =
|
||||
| OpenSearchAuth
|
||||
| {
|
||||
apiKey:
|
||||
| string
|
||||
| {
|
||||
id: string;
|
||||
api_key: string;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ElasticSearchClientOptions =
|
||||
| ElasticSearchElasticSearchClientOptions
|
||||
| OpenSearchElasticSearchClientOptions;
|
||||
|
||||
// @public
|
||||
export class ElasticSearchClientWrapper {
|
||||
// (undocumented)
|
||||
bulk(bulkOptions: {
|
||||
datasource: Readable;
|
||||
onDocument: () => ElasticSearchIndexAction;
|
||||
refreshOnCompletion?: string | boolean;
|
||||
}): BulkHelper<BulkStats>;
|
||||
// (undocumented)
|
||||
createIndex({
|
||||
index,
|
||||
}: {
|
||||
index: string;
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
deleteIndex({
|
||||
index,
|
||||
}: {
|
||||
index: string | string[];
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
static fromClientOptions(
|
||||
options: ElasticSearchClientOptions,
|
||||
): ElasticSearchClientWrapper;
|
||||
// (undocumented)
|
||||
getAliases({
|
||||
aliases,
|
||||
}: {
|
||||
aliases: string[];
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
indexExists({
|
||||
index,
|
||||
}: {
|
||||
index: string | string[];
|
||||
}): TransportRequestPromise<ApiResponse<boolean, unknown>>;
|
||||
// (undocumented)
|
||||
putIndexTemplate(
|
||||
template: ElasticSearchCustomIndexTemplate,
|
||||
): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
search({
|
||||
index,
|
||||
body,
|
||||
}: {
|
||||
index: string | string[];
|
||||
body: Object;
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
updateAliases({
|
||||
actions,
|
||||
}: {
|
||||
actions: ElasticSearchAliasAction[];
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ElasticSearchConcreteQuery = {
|
||||
documentTypes?: string[];
|
||||
@@ -150,6 +215,35 @@ export type ElasticSearchCustomIndexTemplateBody = {
|
||||
template?: Record<string, any>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ElasticSearchElasticSearchClientOptions
|
||||
extends BaseElasticSearchClientOptions {
|
||||
// (undocumented)
|
||||
auth?: ElasticSearchAuth;
|
||||
// (undocumented)
|
||||
cloud?: {
|
||||
id: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
// (undocumented)
|
||||
Connection?: ElasticSearchConnectionConstructor;
|
||||
// (undocumented)
|
||||
node?:
|
||||
| string
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
nodes?:
|
||||
| string
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
provider?: 'elastic';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchHighlightConfig = {
|
||||
fragmentDelimiter: string;
|
||||
@@ -166,6 +260,14 @@ export type ElasticSearchHighlightOptions = {
|
||||
numFragments?: number;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchIndexAction = {
|
||||
index: {
|
||||
_index: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchNodeOptions {
|
||||
// (undocumented)
|
||||
@@ -258,7 +360,7 @@ export type ElasticSearchSearchEngineIndexerOptions = {
|
||||
indexSeparator: string;
|
||||
alias: string;
|
||||
logger: Logger;
|
||||
elasticSearchClient: Client;
|
||||
elasticSearchClientWrapper: ElasticSearchClientWrapper;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -273,4 +375,67 @@ export interface ElasticSearchTransportConstructor {
|
||||
DEFAULT: string;
|
||||
};
|
||||
}
|
||||
|
||||
// @public
|
||||
export const isOpenSearchCompatible: (
|
||||
opts: ElasticSearchClientOptions,
|
||||
) => opts is OpenSearchElasticSearchClientOptions;
|
||||
|
||||
// @public (undocumented)
|
||||
export type OpenSearchAuth = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface OpenSearchConnectionConstructor {
|
||||
// (undocumented)
|
||||
new (opts?: any): any;
|
||||
// (undocumented)
|
||||
roles: {
|
||||
MASTER: string;
|
||||
DATA: string;
|
||||
INGEST: string;
|
||||
};
|
||||
// (undocumented)
|
||||
statuses: {
|
||||
ALIVE: string;
|
||||
DEAD: string;
|
||||
};
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface OpenSearchElasticSearchClientOptions
|
||||
extends BaseElasticSearchClientOptions {
|
||||
// (undocumented)
|
||||
auth?: OpenSearchAuth;
|
||||
// (undocumented)
|
||||
connection?: OpenSearchConnectionConstructor;
|
||||
// (undocumented)
|
||||
node?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
nodes?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
provider?: 'aws';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface OpenSearchNodeOptions {
|
||||
// (undocumented)
|
||||
agent?: ElasticSearchAgentOptions;
|
||||
// (undocumented)
|
||||
headers?: Record<string, any>;
|
||||
// (undocumented)
|
||||
id?: string;
|
||||
// (undocumented)
|
||||
roles?: {
|
||||
master: boolean;
|
||||
data: boolean;
|
||||
ingest: boolean;
|
||||
};
|
||||
// (undocumented)
|
||||
ssl?: ConnectionOptions;
|
||||
// (undocumented)
|
||||
url: URL;
|
||||
}
|
||||
```
|
||||
|
||||
+109
-19
@@ -16,17 +16,60 @@
|
||||
import type { ConnectionOptions as TLSConnectionOptions } from 'tls';
|
||||
|
||||
/**
|
||||
* Options used to configure the `@elastic/elasticsearch` client and
|
||||
* are what will be passed as an argument to the
|
||||
* {@link ElasticSearchSearchEngine.newClient} method
|
||||
*
|
||||
* They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`,
|
||||
* but are maintained separately so that this interface is not coupled to
|
||||
* Typeguard to differentiate ElasticSearch client options which are compatible
|
||||
* with OpenSearch vs. ElasticSearch clients. Useful when calling the
|
||||
* {@link ElasticSearchSearchEngine.newClient} method.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ElasticSearchClientOptions {
|
||||
provider?: 'aws' | 'elastic';
|
||||
export const isOpenSearchCompatible = (
|
||||
opts: ElasticSearchClientOptions,
|
||||
): opts is OpenSearchElasticSearchClientOptions => {
|
||||
return opts?.provider === 'aws';
|
||||
};
|
||||
|
||||
/**
|
||||
* Options used to configure the `@elastic/elasticsearch` client or the
|
||||
* `@opensearch-project/opensearch` client, depending on the given config. It
|
||||
* will be passed as an argument to the
|
||||
* {@link ElasticSearchSearchEngine.newClient} method.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchClientOptions =
|
||||
| ElasticSearchElasticSearchClientOptions
|
||||
| OpenSearchElasticSearchClientOptions;
|
||||
|
||||
/**
|
||||
* Options used to configure the `@opensearch-project/opensearch` client.
|
||||
*
|
||||
* They are drawn from the `ClientOptions` class of `@opensearch-project/opensearch`,
|
||||
* but are maintained separately so that this interface is not coupled to it.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface OpenSearchElasticSearchClientOptions
|
||||
extends BaseElasticSearchClientOptions {
|
||||
provider?: 'aws';
|
||||
auth?: OpenSearchAuth;
|
||||
connection?: OpenSearchConnectionConstructor;
|
||||
node?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[];
|
||||
nodes?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to configure the `@elastic/elasticsearch` client.
|
||||
*
|
||||
* They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`,
|
||||
* but are maintained separately so that this interface is not coupled to it.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ElasticSearchElasticSearchClientOptions
|
||||
extends BaseElasticSearchClientOptions {
|
||||
provider?: 'elastic';
|
||||
auth?: ElasticSearchAuth;
|
||||
Connection?: ElasticSearchConnectionConstructor;
|
||||
node?:
|
||||
| string
|
||||
| string[]
|
||||
@@ -37,8 +80,21 @@ export interface ElasticSearchClientOptions {
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
cloud?: {
|
||||
id: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Base client options that are shared across `@opensearch-project/opensearch`
|
||||
* and `@elastic/elasticsearch` clients.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface BaseElasticSearchClientOptions {
|
||||
Transport?: ElasticSearchTransportConstructor;
|
||||
Connection?: ElasticSearchConnectionConstructor;
|
||||
maxRetries?: number;
|
||||
requestTimeout?: number;
|
||||
pingTimeout?: number;
|
||||
@@ -56,25 +112,24 @@ export interface ElasticSearchClientOptions {
|
||||
headers?: Record<string, any>;
|
||||
opaqueIdPrefix?: string;
|
||||
name?: string | symbol;
|
||||
auth?: ElasticSearchAuth;
|
||||
proxy?: string | URL;
|
||||
enableMetaHeader?: boolean;
|
||||
cloud?: {
|
||||
id: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type OpenSearchAuth = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchAuth =
|
||||
| {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
| OpenSearchAuth
|
||||
| {
|
||||
apiKey:
|
||||
| string
|
||||
@@ -101,6 +156,22 @@ export interface ElasticSearchNodeOptions {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface OpenSearchNodeOptions {
|
||||
url: URL;
|
||||
id?: string;
|
||||
agent?: ElasticSearchAgentOptions;
|
||||
ssl?: TLSConnectionOptions;
|
||||
headers?: Record<string, any>;
|
||||
roles?: {
|
||||
master: boolean;
|
||||
data: boolean;
|
||||
ingest: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -127,6 +198,23 @@ export interface ElasticSearchConnectionConstructor {
|
||||
ML: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface OpenSearchConnectionConstructor {
|
||||
new (opts?: any): any;
|
||||
statuses: {
|
||||
ALIVE: string;
|
||||
DEAD: string;
|
||||
};
|
||||
roles: {
|
||||
MASTER: string;
|
||||
DATA: string;
|
||||
INGEST: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -139,3 +227,5 @@ export interface ElasticSearchTransportConstructor {
|
||||
DEFAULT: string;
|
||||
};
|
||||
}
|
||||
|
||||
// todo(iamEAP) implement canary types to ensure we remain compatible through upgrades.
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Client as ElasticSearchClient } from '@elastic/elasticsearch';
|
||||
import { Client as OpenSearchClient } from '@opensearch-project/opensearch';
|
||||
import { Readable } from 'stream';
|
||||
import {
|
||||
ElasticSearchClientOptions,
|
||||
isOpenSearchCompatible,
|
||||
} from './ElasticSearchClientOptions';
|
||||
import { ElasticSearchCustomIndexTemplate } from './types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchAliasAction =
|
||||
| {
|
||||
remove: { index: any; alias: any };
|
||||
add?: undefined;
|
||||
}
|
||||
| {
|
||||
add: { indices: any; alias: any; index?: undefined };
|
||||
remove?: undefined;
|
||||
}
|
||||
| {
|
||||
add: { index: any; alias: any; indices?: undefined };
|
||||
remove?: undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchIndexAction = {
|
||||
index: {
|
||||
_index: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper class that exposes logical methods that are conditionally fired
|
||||
* against either a configured Elasticsearch client or a configured Opensearch
|
||||
* client.
|
||||
*
|
||||
* This is necessary because, despite its intention to be API-compatible, the
|
||||
* opensearch client does not support API key-based authentication. This is
|
||||
* also the sanest way to accomplish this while making typescript happy.
|
||||
*
|
||||
* In the future, if the differences between implementations become
|
||||
* unmaintainably divergent, we should split out the Opensearch and
|
||||
* Elasticsearch search engine implementations.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class ElasticSearchClientWrapper {
|
||||
private readonly elasticSearchClient: ElasticSearchClient | undefined;
|
||||
private readonly openSearchClient: OpenSearchClient | undefined;
|
||||
|
||||
private constructor({
|
||||
openSearchClient,
|
||||
elasticSearchClient,
|
||||
}: {
|
||||
openSearchClient?: OpenSearchClient;
|
||||
elasticSearchClient?: ElasticSearchClient;
|
||||
}) {
|
||||
this.openSearchClient = openSearchClient;
|
||||
this.elasticSearchClient = elasticSearchClient;
|
||||
}
|
||||
|
||||
static fromClientOptions(options: ElasticSearchClientOptions) {
|
||||
if (isOpenSearchCompatible(options)) {
|
||||
return new ElasticSearchClientWrapper({
|
||||
openSearchClient: new OpenSearchClient(options),
|
||||
});
|
||||
}
|
||||
|
||||
return new ElasticSearchClientWrapper({
|
||||
elasticSearchClient: new ElasticSearchClient(options),
|
||||
});
|
||||
}
|
||||
|
||||
search({ index, body }: { index: string | string[]; body: Object }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.search({ index, body });
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.search({ index, body });
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
bulk(bulkOptions: {
|
||||
datasource: Readable;
|
||||
onDocument: () => ElasticSearchIndexAction;
|
||||
refreshOnCompletion?: string | boolean;
|
||||
}) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.helpers.bulk(bulkOptions);
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.helpers.bulk(bulkOptions);
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
putIndexTemplate(template: ElasticSearchCustomIndexTemplate) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.putIndexTemplate(template);
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.putIndexTemplate(template);
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
indexExists({ index }: { index: string | string[] }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.exists({ index });
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.exists({ index });
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
deleteIndex({ index }: { index: string | string[] }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.delete({ index });
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.delete({ index });
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
createIndex({ index }: { index: string }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.create({ index });
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.create({ index });
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
getAliases({ aliases }: { aliases: string[] }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.cat.aliases({
|
||||
format: 'json',
|
||||
name: aliases,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.cat.aliases({
|
||||
format: 'json',
|
||||
name: aliases,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
updateAliases({ actions }: { actions: ElasticSearchAliasAction[] }) {
|
||||
const filteredActions = actions.filter(Boolean);
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.updateAliases({
|
||||
body: {
|
||||
actions: filteredActions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.updateAliases({
|
||||
body: {
|
||||
actions: filteredActions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
}
|
||||
+8
-7
@@ -16,8 +16,9 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Client, errors } from '@elastic/elasticsearch';
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import Mock from '@elastic/elasticsearch-mock';
|
||||
import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper';
|
||||
import {
|
||||
ElasticSearchConcreteQuery,
|
||||
decodePageCursor,
|
||||
@@ -70,7 +71,7 @@ const customIndexTemplate = {
|
||||
describe('ElasticSearchSearchEngine', () => {
|
||||
let testSearchEngine: ElasticSearchSearchEngine;
|
||||
let inspectableSearchEngine: ElasticSearchSearchEngineForTranslatorTests;
|
||||
let client: Client;
|
||||
let clientWrapper: ElasticSearchClientWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
testSearchEngine = new ElasticSearchSearchEngine(
|
||||
@@ -86,7 +87,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
getVoidLogger(),
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
client = testSearchEngine['elasticSearchClient'];
|
||||
clientWrapper = testSearchEngine['elasticSearchClientWrapper'];
|
||||
});
|
||||
|
||||
describe('custom index template', () => {
|
||||
@@ -686,7 +687,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
});
|
||||
|
||||
it('should handle index/search type filtering correctly', async () => {
|
||||
const elasticSearchQuerySpy = jest.spyOn(client, 'search');
|
||||
const elasticSearchQuerySpy = jest.spyOn(clientWrapper, 'search');
|
||||
await testSearchEngine.query({
|
||||
term: 'testTerm',
|
||||
filters: {},
|
||||
@@ -725,7 +726,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
});
|
||||
|
||||
it('should create matchAll query if no term defined', async () => {
|
||||
const elasticSearchQuerySpy = jest.spyOn(client, 'search');
|
||||
const elasticSearchQuerySpy = jest.spyOn(clientWrapper, 'search');
|
||||
await testSearchEngine.query({
|
||||
term: '',
|
||||
filters: {},
|
||||
@@ -759,7 +760,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
});
|
||||
|
||||
it('should query only specified indices if defined', async () => {
|
||||
const elasticSearchQuerySpy = jest.spyOn(client, 'search');
|
||||
const elasticSearchQuerySpy = jest.spyOn(clientWrapper, 'search');
|
||||
await testSearchEngine.query({
|
||||
term: '',
|
||||
filters: {},
|
||||
@@ -809,7 +810,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
type: 'test-index',
|
||||
indexPrefix: '',
|
||||
indexSeparator: '-index__',
|
||||
elasticSearchClient: client,
|
||||
elasticSearchClientWrapper: clientWrapper,
|
||||
}),
|
||||
);
|
||||
expect(indexerMock.on).toHaveBeenCalledWith(
|
||||
|
||||
+13
-45
@@ -14,10 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
awsGetCredentials,
|
||||
createAWSConnection,
|
||||
} from '@acuris/aws-es-connection';
|
||||
import { awsGetCredentials, createAWSConnection } from 'aws-os-connection';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
IndexableDocument,
|
||||
@@ -26,47 +23,17 @@ import {
|
||||
SearchEngine,
|
||||
SearchQuery,
|
||||
} from '@backstage/plugin-search-common';
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import esb from 'elastic-builder';
|
||||
import { isEmpty, isNaN as nan, isNumber } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import type { ElasticSearchClientOptions } from './ElasticSearchClientOptions';
|
||||
import { ElasticSearchClientOptions } from './ElasticSearchClientOptions';
|
||||
import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer';
|
||||
import { ElasticSearchCustomIndexTemplate } from './types';
|
||||
import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper';
|
||||
|
||||
export type { ElasticSearchClientOptions };
|
||||
|
||||
/**
|
||||
* Elasticsearch specific index template
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchCustomIndexTemplate = {
|
||||
name: string;
|
||||
body: ElasticSearchCustomIndexTemplateBody;
|
||||
};
|
||||
|
||||
/**
|
||||
* Elasticsearch specific index template body
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchCustomIndexTemplateBody = {
|
||||
/**
|
||||
* Array of wildcard (*) expressions used to match the names of data streams and indices during creation.
|
||||
*/
|
||||
index_patterns: string[];
|
||||
/**
|
||||
* An ordered list of component template names.
|
||||
* Component templates are merged in the order specified,
|
||||
* meaning that the last component template specified has the highest precedence.
|
||||
*/
|
||||
composed_of?: string[];
|
||||
/**
|
||||
* See available properties of template
|
||||
* https://www.elastic.co/guide/en/elasticsearch/reference/7.15/indices-put-template.html#put-index-template-api-request-body
|
||||
*/
|
||||
template?: Record<string, any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Search query that the elasticsearch engine understands.
|
||||
* @public
|
||||
@@ -143,7 +110,7 @@ function isBlank(str: string) {
|
||||
* @public
|
||||
*/
|
||||
export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
private readonly elasticSearchClient: Client;
|
||||
private readonly elasticSearchClientWrapper: ElasticSearchClientWrapper;
|
||||
private readonly highlightOptions: ElasticSearchHighlightConfig;
|
||||
|
||||
constructor(
|
||||
@@ -153,7 +120,8 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
private readonly logger: Logger,
|
||||
highlightOptions?: ElasticSearchHighlightOptions,
|
||||
) {
|
||||
this.elasticSearchClient = this.newClient(options => new Client(options));
|
||||
this.elasticSearchClientWrapper =
|
||||
ElasticSearchClientWrapper.fromClientOptions(elasticSearchClientOptions);
|
||||
const uuidTag = uuid();
|
||||
this.highlightOptions = {
|
||||
preTag: `<${uuidTag}>`,
|
||||
@@ -177,7 +145,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
if (options.provider === 'elastic') {
|
||||
logger.info('Initializing Elastic.co ElasticSearch search engine.');
|
||||
} else if (options.provider === 'aws') {
|
||||
logger.info('Initializing AWS ElasticSearch search engine.');
|
||||
logger.info('Initializing AWS OpenSearch search engine.');
|
||||
} else {
|
||||
logger.info('Initializing ElasticSearch search engine.');
|
||||
}
|
||||
@@ -269,7 +237,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
|
||||
async setIndexTemplate(template: ElasticSearchCustomIndexTemplate) {
|
||||
try {
|
||||
await this.elasticSearchClient.indices.putIndexTemplate(template);
|
||||
await this.elasticSearchClientWrapper.putIndexTemplate(template);
|
||||
this.logger.info('Custom index template set');
|
||||
} catch (error) {
|
||||
this.logger.error(`Unable to set custom index template: ${error}`);
|
||||
@@ -284,7 +252,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
indexPrefix: this.indexPrefix,
|
||||
indexSeparator: this.indexSeparator,
|
||||
alias,
|
||||
elasticSearchClient: this.elasticSearchClient,
|
||||
elasticSearchClientWrapper: this.elasticSearchClientWrapper,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
@@ -292,13 +260,13 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
indexer.on('error', async e => {
|
||||
this.logger.error(`Failed to index documents for type ${type}`, e);
|
||||
try {
|
||||
const response = await this.elasticSearchClient.indices.exists({
|
||||
const response = await this.elasticSearchClientWrapper.indexExists({
|
||||
index: indexer.indexName,
|
||||
});
|
||||
const indexCreated = response.body;
|
||||
if (indexCreated) {
|
||||
this.logger.info(`Removing created index ${indexer.indexName}`);
|
||||
await this.elasticSearchClient.indices.delete({
|
||||
await this.elasticSearchClientWrapper.deleteIndex({
|
||||
index: indexer.indexName,
|
||||
});
|
||||
}
|
||||
@@ -319,7 +287,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
? documentTypes.map(it => this.constructSearchAlias(it))
|
||||
: this.constructSearchAlias('*');
|
||||
try {
|
||||
const result = await this.elasticSearchClient.search({
|
||||
const result = await this.elasticSearchClientWrapper.search({
|
||||
index: queryIndices,
|
||||
body: elasticSearchQuery,
|
||||
});
|
||||
|
||||
+3
-3
@@ -16,13 +16,13 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { TestPipeline } from '@backstage/plugin-search-backend-node';
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import Mock from '@elastic/elasticsearch-mock';
|
||||
import { range } from 'lodash';
|
||||
import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper';
|
||||
import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer';
|
||||
|
||||
const mock = new Mock();
|
||||
const client = new Client({
|
||||
const clientWrapper = ElasticSearchClientWrapper.fromClientOptions({
|
||||
node: 'http://localhost:9200',
|
||||
Connection: mock.getConnection(),
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe('ElasticSearchSearchEngineIndexer', () => {
|
||||
indexSeparator: '-index__',
|
||||
alias: 'some-type-index__search',
|
||||
logger: getVoidLogger(),
|
||||
elasticSearchClient: client,
|
||||
elasticSearchClientWrapper: clientWrapper,
|
||||
});
|
||||
|
||||
// Set up all requisite Elastic mocks.
|
||||
|
||||
+26
-29
@@ -16,9 +16,9 @@
|
||||
|
||||
import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';
|
||||
import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import { Readable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper';
|
||||
|
||||
/**
|
||||
* Options for instansiate ElasticSearchSearchEngineIndexer
|
||||
@@ -30,7 +30,7 @@ export type ElasticSearchSearchEngineIndexerOptions = {
|
||||
indexSeparator: string;
|
||||
alias: string;
|
||||
logger: Logger;
|
||||
elasticSearchClient: Client;
|
||||
elasticSearchClientWrapper: ElasticSearchClientWrapper;
|
||||
};
|
||||
|
||||
function duration(startTimestamp: [number, number]): string {
|
||||
@@ -57,7 +57,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
private readonly removableAlias: string;
|
||||
private readonly logger: Logger;
|
||||
private readonly sourceStream: Readable;
|
||||
private readonly elasticSearchClient: Client;
|
||||
private readonly elasticSearchClientWrapper: ElasticSearchClientWrapper;
|
||||
private bulkResult: Promise<any>;
|
||||
|
||||
constructor(options: ElasticSearchSearchEngineIndexerOptions) {
|
||||
@@ -70,7 +70,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
this.indexName = this.constructIndexName(`${Date.now()}`);
|
||||
this.alias = options.alias;
|
||||
this.removableAlias = `${this.alias}_removable`;
|
||||
this.elasticSearchClient = options.elasticSearchClient;
|
||||
this.elasticSearchClientWrapper = options.elasticSearchClientWrapper;
|
||||
|
||||
// The ES client bulk helper supports stream-based indexing, but we have to
|
||||
// supply the stream directly to it at instantiation-time. We can't supply
|
||||
@@ -83,7 +83,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
|
||||
// Keep a reference to the ES Bulk helper so that we can know when all
|
||||
// documents have been successfully written to ES.
|
||||
this.bulkResult = this.elasticSearchClient.helpers.bulk({
|
||||
this.bulkResult = this.elasticSearchClientWrapper.bulk({
|
||||
datasource: this.sourceStream,
|
||||
onDocument() {
|
||||
that.processed++;
|
||||
@@ -98,16 +98,15 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
async initialize(): Promise<void> {
|
||||
this.logger.info(`Started indexing documents for index ${this.type}`);
|
||||
|
||||
const aliases = await this.elasticSearchClient.cat.aliases({
|
||||
format: 'json',
|
||||
name: [this.alias, this.removableAlias],
|
||||
const aliases = await this.elasticSearchClientWrapper.getAliases({
|
||||
aliases: [this.alias, this.removableAlias],
|
||||
});
|
||||
|
||||
this.removableIndices = [
|
||||
...new Set(aliases.body.map((r: Record<string, any>) => r.index)),
|
||||
] as string[];
|
||||
|
||||
await this.elasticSearchClient.indices.create({
|
||||
await this.elasticSearchClientWrapper.createIndex({
|
||||
index: this.indexName,
|
||||
});
|
||||
}
|
||||
@@ -140,25 +139,23 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
)}`,
|
||||
result,
|
||||
);
|
||||
await this.elasticSearchClient.indices.updateAliases({
|
||||
body: {
|
||||
actions: [
|
||||
{
|
||||
remove: { index: this.constructIndexName('*'), alias: this.alias },
|
||||
},
|
||||
this.removableIndices.length
|
||||
? {
|
||||
add: {
|
||||
indices: this.removableIndices,
|
||||
alias: this.removableAlias,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
add: { index: this.indexName, alias: this.alias },
|
||||
},
|
||||
].filter(Boolean),
|
||||
},
|
||||
await this.elasticSearchClientWrapper.updateAliases({
|
||||
actions: [
|
||||
{
|
||||
remove: { index: this.constructIndexName('*'), alias: this.alias },
|
||||
},
|
||||
this.removableIndices.length
|
||||
? {
|
||||
add: {
|
||||
indices: this.removableIndices,
|
||||
alias: this.removableAlias,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
add: { index: this.indexName, alias: this.alias },
|
||||
},
|
||||
].filter(Boolean),
|
||||
});
|
||||
|
||||
// If any indices are removable, remove them. Do not bubble up this error,
|
||||
@@ -166,7 +163,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
if (this.removableIndices.length) {
|
||||
this.logger.info('Removing stale search indices', this.removableIndices);
|
||||
try {
|
||||
await this.elasticSearchClient.indices.delete({
|
||||
await this.elasticSearchClientWrapper.deleteIndex({
|
||||
index: this.removableIndices,
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -15,13 +15,25 @@
|
||||
*/
|
||||
|
||||
export { ElasticSearchSearchEngine } from './ElasticSearchSearchEngine';
|
||||
export { isOpenSearchCompatible } from './ElasticSearchClientOptions';
|
||||
export type {
|
||||
BaseElasticSearchClientOptions,
|
||||
ElasticSearchAgentOptions,
|
||||
ElasticSearchConnectionConstructor,
|
||||
ElasticSearchElasticSearchClientOptions,
|
||||
ElasticSearchTransportConstructor,
|
||||
ElasticSearchNodeOptions,
|
||||
ElasticSearchAuth,
|
||||
OpenSearchAuth,
|
||||
OpenSearchConnectionConstructor,
|
||||
OpenSearchElasticSearchClientOptions,
|
||||
OpenSearchNodeOptions,
|
||||
} from './ElasticSearchClientOptions';
|
||||
export type {
|
||||
ElasticSearchAliasAction,
|
||||
ElasticSearchClientWrapper,
|
||||
ElasticSearchIndexAction,
|
||||
} from './ElasticSearchClientWrapper';
|
||||
export type {
|
||||
ElasticSearchConcreteQuery,
|
||||
ElasticSearchClientOptions,
|
||||
@@ -30,10 +42,12 @@ export type {
|
||||
ElasticSearchQueryTranslator,
|
||||
ElasticSearchQueryTranslatorOptions,
|
||||
ElasticSearchOptions,
|
||||
ElasticSearchCustomIndexTemplate,
|
||||
ElasticSearchCustomIndexTemplateBody,
|
||||
} from './ElasticSearchSearchEngine';
|
||||
export type {
|
||||
ElasticSearchSearchEngineIndexer,
|
||||
ElasticSearchSearchEngineIndexerOptions,
|
||||
} from './ElasticSearchSearchEngineIndexer';
|
||||
export type {
|
||||
ElasticSearchCustomIndexTemplate,
|
||||
ElasticSearchCustomIndexTemplateBody,
|
||||
} from './types';
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Elasticsearch specific index template
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchCustomIndexTemplate = {
|
||||
name: string;
|
||||
body: ElasticSearchCustomIndexTemplateBody;
|
||||
};
|
||||
|
||||
/**
|
||||
* Elasticsearch specific index template body
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchCustomIndexTemplateBody = {
|
||||
/**
|
||||
* Array of wildcard (*) expressions used to match the names of data streams and indices during creation.
|
||||
*/
|
||||
index_patterns: string[];
|
||||
/**
|
||||
* An ordered list of component template names.
|
||||
* Component templates are merged in the order specified,
|
||||
* meaning that the last component template specified has the highest precedence.
|
||||
*/
|
||||
composed_of?: string[];
|
||||
/**
|
||||
* See available properties of template
|
||||
* https://www.elastic.co/guide/en/elasticsearch/reference/7.15/indices-put-template.html#put-index-template-api-request-body
|
||||
*/
|
||||
template?: Record<string, any>;
|
||||
};
|
||||
@@ -20,13 +20,18 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { ElasticSearchSearchEngine } from './engines';
|
||||
export { ElasticSearchSearchEngine, isOpenSearchCompatible } from './engines';
|
||||
export type {
|
||||
BaseElasticSearchClientOptions,
|
||||
ElasticSearchAgentOptions,
|
||||
ElasticSearchAliasAction,
|
||||
ElasticSearchClientWrapper,
|
||||
ElasticSearchConcreteQuery,
|
||||
ElasticSearchClientOptions,
|
||||
ElasticSearchElasticSearchClientOptions,
|
||||
ElasticSearchHighlightConfig,
|
||||
ElasticSearchHighlightOptions,
|
||||
ElasticSearchIndexAction,
|
||||
ElasticSearchQueryTranslator,
|
||||
ElasticSearchQueryTranslatorOptions,
|
||||
ElasticSearchConnectionConstructor,
|
||||
@@ -38,4 +43,8 @@ export type {
|
||||
ElasticSearchSearchEngineIndexerOptions,
|
||||
ElasticSearchCustomIndexTemplate,
|
||||
ElasticSearchCustomIndexTemplateBody,
|
||||
OpenSearchAuth,
|
||||
OpenSearchConnectionConstructor,
|
||||
OpenSearchElasticSearchClientOptions,
|
||||
OpenSearchNodeOptions,
|
||||
} from './engines';
|
||||
|
||||
Reference in New Issue
Block a user