Skip to content

#1141 Set Up SearchClient 🔍#1021

Open
demariadaniel wants to merge 20 commits intomainfrom
1141/feat-setup-opensearch-client
Open

#1141 Set Up SearchClient 🔍#1021
demariadaniel wants to merge 20 commits intomainfrom
1141/feat-setup-opensearch-client

Conversation

@demariadaniel
Copy link
Contributor

@demariadaniel demariadaniel commented Jan 29, 2026

Summary

Base feature branch for supporting multiple Search Client options in Arranger

Issues

Description of Changes

  • Creates a shared SearchClient function for determining configuration and returning appropriate client
  • Adds OpenSearch client library
  • Initial Type structure for SearchClient
  • Prettier & Typescript cleanup across multiple files

Setup

  • Requires Arranger npm install:
cd /arranger
npm i

Successful connection to Arranger server with both OpenSearch and Elastic setups:

OpenSearch:
Screenshot 2026-02-11 at 2 40 03 PM

ElasticSearch:
Screenshot 2026-02-11 at 3 13 08 PM

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

@demariadaniel demariadaniel changed the title WIP: Basic SearchClient, w/ some type & import cleanup WIP: Basic SearchClient Jan 30, 2026
@@ -1,9 +1,12 @@
import { after, before, describe } from 'node:test';

Copy link
Contributor Author

@demariadaniel demariadaniel Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All usage of @elastic Client is replaced by our SearchClientType

@demariadaniel demariadaniel self-assigned this Feb 9, 2026
const SearchClient = async (options: ESClientOptions | OSClientOptions) => {
const { ES_HOST } = ENV_CONFIG;
const searchConfig = await (await fetch(ES_HOST)).json();
const { distribution } = searchConfig.version;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With types cleaned up I found extendMapping is undefined, not sure what this is actually meant to be

@demariadaniel demariadaniel changed the title WIP: Basic SearchClient WIP: #1141 Set Up Basic SearchClient Feb 11, 2026
Copy link
Contributor Author

@demariadaniel demariadaniel Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main changes here in [modules/server/src/searchClient/index.ts]

@demariadaniel demariadaniel force-pushed the 1141/feat-setup-opensearch-client branch from d611c86 to 9ad4669 Compare March 9, 2026 15:28
@demariadaniel demariadaniel changed the title WIP: #1141 Set Up Basic SearchClient #1141 SearchClient Mar 9, 2026
@demariadaniel demariadaniel changed the title #1141 SearchClient #1141 SearchClient 🔍 Mar 9, 2026
@demariadaniel demariadaniel changed the title #1141 SearchClient 🔍 #1141 Set Up SearchClient 🔍 Mar 9, 2026
@demariadaniel demariadaniel marked this pull request as ready for review March 9, 2026 19:29
Copy link
Contributor

@joneubank joneubank left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment on lines +28 to +33
// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check be case insensitive?


export type SearchConfig = {
node: string;
clientType?: SupportedClientTypes | string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = '') => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This typeof check is not doing anything.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +9 to +64
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines +3 to +4
export default (esClient: SearchClient) => async (params: SearchResponse) => {
return await esClient.search(params);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice type improvements here.

Comment on lines +28 to +44
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>>;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants