Conversation
| @@ -1,9 +1,12 @@ | |||
| import { after, before, describe } from 'node:test'; | |||
|
|
|||
There was a problem hiding this comment.
Heads up that multiple files include simple prettier cleanup changes to clear up the repo (mostly order of imports, whitespace, & falsy handling) or Typescript updates to declutter the repo.
The main change is the creation of a SearchClient which replaces instances of ElasticSearch Client.
| const aggsState: I_AggsState[] = mappingToAggsState(mapping); | ||
| return { timestamp: timestamp(), state: aggsState }; | ||
| }; | ||
| (es: SearchClientType) => |
There was a problem hiding this comment.
All usage of @elastic Client is replaced by our SearchClientType
| const SearchClient = async (options: ESClientOptions | OSClientOptions) => { | ||
| const { ES_HOST } = ENV_CONFIG; | ||
| const searchConfig = await (await fetch(ES_HOST)).json(); | ||
| const { distribution } = searchConfig.version; |
There was a problem hiding this comment.
See:
https://docs.opensearch.org/latest/api-reference/cluster-api/info/
https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-info
Cluster Info provides a distribution field which lets us clearly identify when an OpenSearch instance is running.
This will need to be expanded when investigating ElasticSearch 8. But for OpenSearch hopefully the distribution field will suffice.
| import { getProjectStorageMetadata, updateProjectIndexMetadata } from '../IndexSchema/utils'; | ||
| import { type AllClients } from '#searchClient/index.js'; | ||
|
|
||
| import { extendMapping } from '../../../mapping/index.js'; |
There was a problem hiding this comment.
With types cleaned up I found extendMapping is undefined, not sure what this is actually meant to be
There was a problem hiding this comment.
Main changes here in [modules/server/src/searchClient/index.ts]
d611c86 to
9ad4669
Compare
joneubank
left a comment
There was a problem hiding this comment.
So great seeing this working end to end. Thanks for the big effort.
I saw quite a few places for improvement in how we are handling the configuration and setup for arranger server now that it supports multiple search services, so that's where this feedback focuses.
We also require some updates to documentation for arranger to communicate to operators about the search supports and how to configure it. @b-f-chan this may fit better into a separate ticket. @demariadaniel for this review, let's just make sure we document somewhere the valid options for the SEARCH_SERVICE_TYPE env var.
There is future work needed to make our SearchClient type strongly typed without all the anys. That will take some dedicated time and effort to get right so I couldn't fit it into this round of feedback comments.
Thanks again Dan!
| // Determine which search client is being used | ||
| // Distribution field is specific to OpenSearch | ||
| // Else, if number field is a valid string, default to 'elasticSearch' as client type | ||
| const { distribution, number } = response.version; | ||
| const version = | ||
| typeof distribution === 'string' ? distribution : typeof number === 'string' ? 'elasticsearch' : undefined; |
There was a problem hiding this comment.
Should this check be case insensitive?
|
|
||
| export type SearchConfig = { | ||
| node: string; | ||
| clientType?: SupportedClientTypes | string; |
There was a problem hiding this comment.
Why can this type be string?
SupportedClientTypes are a subset of string so this union type is equivalent to string.
I believe this should just be typed as SupportedClientTypes.
| export const buildEsClient = (esHost = '', esUser = '', esPass = '') => { | ||
| if (!esHost) { | ||
| console.error('no elasticsearch host was provided'); | ||
| export const createSearchConfig = (host = '', username = '', password = '', clientType = '') => { |
There was a problem hiding this comment.
Don't default clientType to an empty string. It should be undefined if not defined. The value is meant to be of type SupportedClientType
| export default async function getSearchClient(config: SearchConfig) { | ||
| try { | ||
| const configClientType = !config.clientType ? await getClientVersion(config) : config.clientType; | ||
| const clientType = supportedClientValues.find((key) => typeof key === 'string' && key === configClientType); |
There was a problem hiding this comment.
This typeof check is not doing anything.
There was a problem hiding this comment.
I think we can ignore this change, because the client type should be checked and returned by getClientVersion().
| const searchConfig: SearchConfig = { | ||
| node: host, | ||
| auth, | ||
| clientType, |
There was a problem hiding this comment.
SEARCH_CLIENT is type string | undefined. If it is not undefined we should be confirming in this file that it is a valid client type, and therefore allows us to type this config as SupportedClientType and not the union with string, or we reject the user's configuration. Take a look at when host is not defined, we throw an error with logs indicating why their config is invalid, and it would be best to do a similar thing here. This prevents the server starting to let the user know that their config is invalid.
| export function createElasticSearchClient(options: ESClientOptions) { | ||
| const elasticSearchClient = new Client(options); | ||
|
|
||
| const searchClient: SearchClient = { | ||
| indices: { | ||
| create: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.indices.create(input, options); | ||
| return output; | ||
| }, | ||
| delete: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.indices.delete(input, options); | ||
| return output; | ||
| }, | ||
| exists: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.indices.exists(input, options); | ||
| return output; | ||
| }, | ||
| getMapping: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.indices.getMapping(input, options); | ||
| return output; | ||
| }, | ||
| }, | ||
| cat: { | ||
| aliases: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.cat.aliases(input, options); | ||
| return output; | ||
| }, | ||
| }, | ||
| bulk: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.bulk(input, options); | ||
| return output; | ||
| }, | ||
| index: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.index(input, options); | ||
| return output; | ||
| }, | ||
| search: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.search(input, options); | ||
| return output; | ||
| }, | ||
| update: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.update(input, options); | ||
| return output; | ||
| }, | ||
| create: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.create(input, options); | ||
| return output; | ||
| }, | ||
| delete: async (input: any, options?: any) => { | ||
| const output = await elasticSearchClient.delete(input, options); | ||
| return output; | ||
| }, | ||
| }; | ||
|
|
||
| return searchClient; | ||
| } |
There was a problem hiding this comment.
Because our shared Client definition is so broad, this can currently be simplified to:
export function createElasticSearchClient(options: ESClientOptions) {
const elasticSearchClient = new Client(options);
return elasticSearchClient;
}This will compile without issue and still pass tests. This should not be surprising since all the functions here did no work.
Before updating like this though, the structure is still useful for when we actually enforce structure to the input and output arguments of our SearchClient interface.
Basically, this is an indication that the any usage in our SearchClient type is too broad.
| clientType: 'opensearch'; | ||
| }; | ||
|
|
||
| export function createOpenSearchClient(options: OSClientOptions): SearchClient { |
There was a problem hiding this comment.
Same comment as for the ESClient, the shape of the SearchClient is compatible with the openSearchClient and with the current type we don't need all the boilerplate.
| */ | ||
| type DefaultRoot = Root; | ||
| export type Resolver<Root = DefaultRoot, QueryArgs = Object, ReturnValue = undefined> = ( | ||
| export type Resolver<Root = DefaultRoot, QueryArgs = object, ReturnValue = undefined> = ( |
| export default (esClient: SearchClient) => async (params: SearchResponse) => { | ||
| return await esClient.search(params); |
There was a problem hiding this comment.
Nice type improvements here.
| export type SearchClient = { | ||
| indices: { | ||
| create: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| delete: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| exists: (input: any, options?: any) => SearchClientResponseHandler<boolean>; | ||
| getMapping: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| }; | ||
| cat: { | ||
| aliases: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| }; | ||
| bulk: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| index: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| search: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| update: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| create: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| delete: (input: any, options?: any) => SearchClientResponseHandler<Record<string, any>>; | ||
| }; |
There was a problem hiding this comment.
The crux of this work is this type definition, and the use of any here is not sufficient for us to publish this. However, due to the similarity of the ES and OS libraries this works fine with our current code base, especially since a lot of the code relies on untyped JS files.
We need to get a clear view of what we want out inputs and outputs to look like, and then do work to make sure all clients adhere to that shared interface. Let's followup on this after addressing the other comments around the search service type configuration.
Summary
Base feature branch for supporting multiple Search Client options in Arranger
Issues
Description of Changes
Setup
Requires running ElasticSearch / OpenSearch instances
Basic OpenSearch setup:
https://docs.opensearch.org/latest/getting-started/quickstart/#option-1-try-opensearch-in-one-command
Requires mock Arranger configs
... then run
npm run serverSuccessful connection to Arranger server with both OpenSearch and Elastic setups:
OpenSearch:

ElasticSearch:

Readiness Checklist
.env.schemafile and documented in the README