Просмотр исходного кода

Merge pull request #10150 from weseek/feat/168205-elasticsearchclientdelegator-es9-support

feat: Elasticsearch9 support for ElasticsearchClientDelegator
Yuki Takei 9 месяцев назад
Родитель
Сommit
289739b4fd

+ 1 - 1
apps/app/package.json

@@ -71,7 +71,7 @@
     "@cspell/dynamic-import": "^8.15.4",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.4",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.18.2",
-    "@elastic/elasticsearch9": "npm:@elastic/elasticsearch@^9.0.2",
+    "@elastic/elasticsearch9": "npm:@elastic/elasticsearch@^9.0.3",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@growi/core": "workspace:^",

+ 3 - 0
apps/app/src/server/routes/apiv3/search.js

@@ -138,6 +138,7 @@ module.exports = (crowi) => {
       return res.status(200).send({ info });
     }
     catch (err) {
+      logger.error(err);
       return res.apiv3Err(err, 503);
     }
   });
@@ -169,6 +170,7 @@ module.exports = (crowi) => {
       return res.status(200).send();
     }
     catch (err) {
+      logger.error(err);
       return res.apiv3Err(err, 503);
     }
   });
@@ -241,6 +243,7 @@ module.exports = (crowi) => {
       }
     }
     catch (err) {
+      logger.error(err);
       return res.apiv3Err(err, 503);
     }
   });

+ 78 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es7-client-delegator.ts

@@ -0,0 +1,78 @@
+// TODO: https://redmine.weseek.co.jp/issues/168446
+import {
+  Client,
+  type ClientOptions,
+  type ApiResponse,
+  type RequestParams,
+  type estypes,
+} from '@elastic/elasticsearch7';
+
+export class ES7ClientDelegator {
+
+  private client: Client;
+
+  constructor(options: ClientOptions, rejectUnauthorized: boolean) {
+    this.client = new Client({ ...options, ssl: { rejectUnauthorized } });
+  }
+
+  async bulk(params: RequestParams.Bulk): Promise<estypes.BulkResponse> {
+    const res = (await this.client.bulk(params)).body as estypes.BulkResponse;
+    return res;
+  }
+
+  cat = {
+    aliases: (params: RequestParams.CatAliases): Promise<ApiResponse<estypes.CatAliasesResponse>> => this.client.cat.aliases(params),
+    indices: (params: RequestParams.CatIndices): Promise<ApiResponse<estypes.CatIndicesResponse>> => this.client.cat.indices(params),
+  };
+
+  cluster = {
+    health: (): Promise<ApiResponse<estypes.ClusterHealthResponse>> => this.client.cluster.health(),
+  };
+
+  indices = {
+    create: (params: RequestParams.IndicesCreate): Promise<ApiResponse<estypes.IndicesCreateResponse>> => this.client.indices.create(params),
+    delete: (params: RequestParams.IndicesDelete): Promise<ApiResponse<estypes.IndicesDeleteResponse>> => this.client.indices.delete(params),
+    exists: async(params: RequestParams.IndicesExists): Promise<estypes.IndicesExistsResponse> => {
+      const res = (await this.client.indices.exists(params)).body;
+      return res;
+    },
+    existsAlias: async(params: RequestParams.IndicesExistsAlias): Promise<estypes.IndicesExistsAliasResponse> => {
+      const res = (await this.client.indices.existsAlias(params)).body;
+      return res;
+    },
+    putAlias: (params: RequestParams.IndicesPutAlias): Promise<ApiResponse<estypes.IndicesUpdateAliasesResponse>> => this.client.indices.putAlias(params),
+    getAlias: async(params: RequestParams.IndicesGetAlias): Promise<estypes.IndicesGetAliasResponse> => {
+      const res = (await this.client.indices.getAlias(params)).body as estypes.IndicesGetAliasResponse;
+      return res;
+    },
+    updateAliases: (params: RequestParams.IndicesUpdateAliases['body']): Promise<ApiResponse<estypes.IndicesUpdateAliasesResponse>> => {
+      return this.client.indices.updateAliases({ body: params });
+    },
+    validateQuery: async(params:RequestParams.IndicesValidateQuery): Promise<estypes.IndicesValidateQueryResponse> => {
+      const res = (await this.client.indices.validateQuery(params)).body as estypes.IndicesValidateQueryResponse;
+      return res;
+    },
+    stats: async(params: RequestParams.IndicesStats): Promise<estypes.IndicesStatsResponse> => {
+      const res = (await this.client.indices.stats(params)).body as estypes.IndicesStatsResponse;
+      return res;
+    },
+  };
+
+  nodes = {
+    info: (): Promise<ApiResponse<estypes.NodesInfoResponse>> => this.client.nodes.info(),
+  };
+
+  ping(): Promise<ApiResponse<estypes.PingResponse>> {
+    return this.client.ping();
+  }
+
+  reindex(indexName: string, tmpIndexName: string): Promise<ApiResponse<estypes.ReindexResponse>> {
+    return this.client.reindex({ wait_for_completion: false, body: { source: { index: indexName }, dest: { index: tmpIndexName } } });
+  }
+
+  async search(params: RequestParams.Search): Promise<estypes.SearchResponse> {
+    const res = (await this.client.search(params)).body as estypes.SearchResponse;
+    return res;
+  }
+
+}

+ 52 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es8-client-delegator.ts

@@ -0,0 +1,52 @@
+import { Client, type ClientOptions, type estypes } from '@elastic/elasticsearch8';
+
+export class ES8ClientDelegator {
+
+  private client: Client;
+
+  constructor(options: ClientOptions, rejectUnauthorized: boolean) {
+    this.client = new Client({ ...options, tls: { rejectUnauthorized } });
+  }
+
+  bulk(params: estypes.BulkRequest): Promise<estypes.BulkResponse> {
+    return this.client.bulk(params);
+  }
+
+  cat = {
+    aliases: (params: estypes.CatAliasesRequest): Promise<estypes.CatAliasesResponse> => this.client.cat.aliases(params),
+    indices: (params: estypes.CatIndicesRequest): Promise<estypes.CatIndicesResponse> => this.client.cat.indices(params),
+  };
+
+  cluster = {
+    health: (): Promise<estypes.ClusterHealthResponse> => this.client.cluster.health(),
+  };
+
+  indices = {
+    create: (params: estypes.IndicesCreateRequest): Promise<estypes.IndicesCreateResponse> => this.client.indices.create(params),
+    delete: (params: estypes.IndicesDeleteRequest): Promise<estypes.IndicesDeleteResponse> => this.client.indices.delete(params),
+    exists: (params: estypes.IndicesExistsRequest): Promise<estypes.IndicesExistsResponse> => this.client.indices.exists(params),
+    existsAlias: (params: estypes.IndicesExistsAliasRequest): Promise<estypes.IndicesExistsAliasResponse> => this.client.indices.existsAlias(params),
+    putAlias: (params: estypes.IndicesPutAliasRequest): Promise<estypes.IndicesPutAliasResponse> => this.client.indices.putAlias(params),
+    getAlias: (params: estypes.IndicesGetAliasRequest): Promise<estypes.IndicesGetAliasResponse> => this.client.indices.getAlias(params),
+    updateAliases: (params: estypes.IndicesUpdateAliasesRequest): Promise<estypes.IndicesUpdateAliasesResponse> => this.client.indices.updateAliases(params),
+    validateQuery: (params: estypes.IndicesValidateQueryRequest): Promise<estypes.IndicesValidateQueryResponse> => this.client.indices.validateQuery(params),
+    stats: (params: estypes.IndicesStatsRequest): Promise<estypes.IndicesStatsResponse> => this.client.indices.stats(params),
+  };
+
+  nodes = {
+    info: (): Promise<estypes.NodesInfoResponse> => this.client.nodes.info(),
+  };
+
+  ping(): Promise<estypes.PingResponse> {
+    return this.client.ping();
+  }
+
+  reindex(indexName: string, tmpIndexName: string): Promise<estypes.ReindexResponse> {
+    return this.client.reindex({ wait_for_completion: false, source: { index: indexName }, dest: { index: tmpIndexName } });
+  }
+
+  search(params: estypes.SearchRequest): Promise<estypes.SearchResponse> {
+    return this.client.search(params);
+  }
+
+}

+ 52 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es9-client-delegator.ts

@@ -0,0 +1,52 @@
+import { Client, type ClientOptions, type estypes } from '@elastic/elasticsearch9';
+
+export class ES9ClientDelegator {
+
+  private client: Client;
+
+  constructor(options: ClientOptions, rejectUnauthorized: boolean) {
+    this.client = new Client({ ...options, tls: { rejectUnauthorized } });
+  }
+
+  bulk(params: estypes.BulkRequest): Promise<estypes.BulkResponse> {
+    return this.client.bulk(params);
+  }
+
+  cat = {
+    aliases: (params: estypes.CatAliasesRequest): Promise<estypes.CatAliasesResponse> => this.client.cat.aliases(params),
+    indices: (params: estypes.CatIndicesRequest): Promise<estypes.CatIndicesResponse> => this.client.cat.indices(params),
+  };
+
+  cluster = {
+    health: (): Promise<estypes.ClusterHealthResponse> => this.client.cluster.health(),
+  };
+
+  indices = {
+    create: (params: estypes.IndicesCreateRequest): Promise<estypes.IndicesCreateResponse> => this.client.indices.create(params),
+    delete: (params: estypes.IndicesDeleteRequest): Promise<estypes.IndicesDeleteResponse> => this.client.indices.delete(params),
+    exists: (params: estypes.IndicesExistsRequest): Promise<estypes.IndicesExistsResponse> => this.client.indices.exists(params),
+    existsAlias: (params: estypes.IndicesExistsAliasRequest): Promise<estypes.IndicesExistsAliasResponse> => this.client.indices.existsAlias(params),
+    putAlias: (params: estypes.IndicesPutAliasRequest): Promise<estypes.IndicesPutAliasResponse> => this.client.indices.putAlias(params),
+    getAlias: (params: estypes.IndicesGetAliasRequest): Promise<estypes.IndicesGetAliasResponse> => this.client.indices.getAlias(params),
+    updateAliases: (params: estypes.IndicesUpdateAliasesRequest): Promise<estypes.IndicesUpdateAliasesResponse> => this.client.indices.updateAliases(params),
+    validateQuery: (params: estypes.IndicesValidateQueryRequest): Promise<estypes.IndicesValidateQueryResponse> => this.client.indices.validateQuery(params),
+    stats: (params: estypes.IndicesStatsRequest): Promise<estypes.IndicesStatsResponse> => this.client.indices.stats(params),
+  };
+
+  nodes = {
+    info: (): Promise<estypes.NodesInfoResponse> => this.client.nodes.info(),
+  };
+
+  ping(): Promise<estypes.PingResponse> {
+    return this.client.ping();
+  }
+
+  reindex(indexName: string, tmpIndexName: string): Promise<estypes.ReindexResponse> {
+    return this.client.reindex({ wait_for_completion: false, source: { index: indexName }, dest: { index: tmpIndexName } });
+  }
+
+  search(params: estypes.SearchRequest): Promise<estypes.SearchResponse> {
+    return this.client.search(params);
+  }
+
+}

+ 60 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/get-client.ts

@@ -0,0 +1,60 @@
+import type { ClientOptions as ES7ClientOptions } from '@elastic/elasticsearch7';
+import type { ClientOptions as ES8ClientOptions } from '@elastic/elasticsearch8';
+import type { ClientOptions as ES9ClientOptions } from '@elastic/elasticsearch9';
+
+import { type ES7ClientDelegator } from './es7-client-delegator';
+import { type ES8ClientDelegator } from './es8-client-delegator';
+import { type ES9ClientDelegator } from './es9-client-delegator';
+import type { ElasticSEarchClientDeletegator } from './interfaces';
+
+type GetDelegatorOptions = {
+  version: 7;
+  options: ES7ClientOptions;
+  rejectUnauthorized: boolean;
+} | {
+  version: 8;
+  options: ES8ClientOptions;
+  rejectUnauthorized: boolean;
+} | {
+  version: 9;
+  options: ES9ClientOptions;
+  rejectUnauthorized: boolean;
+}
+
+type IsAny<T> = 'dummy' extends (T & 'dummy') ? true : false;
+type Delegator<Opts extends GetDelegatorOptions> =
+  IsAny<Opts> extends true
+    ? ElasticSEarchClientDeletegator
+    : Opts extends { version: 7 }
+      ? ES7ClientDelegator
+      : Opts extends { version: 8 }
+        ? ES8ClientDelegator
+        : Opts extends { version: 9 }
+          ? ES9ClientDelegator
+          : ElasticSEarchClientDeletegator
+
+let instance: ElasticSEarchClientDeletegator | null = null;
+export const getClient = async<Opts extends GetDelegatorOptions>(opts: Opts): Promise<Delegator<Opts>> => {
+  if (instance == null) {
+    if (opts.version === 7) {
+      await import('./es7-client-delegator').then(({ ES7ClientDelegator }) => {
+        instance = new ES7ClientDelegator(opts.options, opts.rejectUnauthorized);
+        return instance;
+      });
+    }
+    if (opts.version === 8) {
+      await import('./es8-client-delegator').then(({ ES8ClientDelegator }) => {
+        instance = new ES8ClientDelegator(opts.options, opts.rejectUnauthorized);
+        return instance;
+      });
+    }
+    if (opts.version === 9) {
+      await import('./es9-client-delegator').then(({ ES9ClientDelegator }) => {
+        instance = new ES9ClientDelegator(opts.options, opts.rejectUnauthorized);
+        return instance;
+      });
+    }
+  }
+
+  return instance as Delegator<Opts>;
+};

+ 2 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/index.ts

@@ -0,0 +1,2 @@
+export * from './get-client';
+export * from './interfaces';

+ 5 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/interfaces.ts

@@ -0,0 +1,5 @@
+import type { ES7ClientDelegator } from './es7-client-delegator';
+import type { ES8ClientDelegator } from './es8-client-delegator';
+import type { ES9ClientDelegator } from './es9-client-delegator';
+
+export type ElasticSEarchClientDeletegator = ES7ClientDelegator | ES8ClientDelegator | ES9ClientDelegator;

+ 0 - 113
apps/app/src/server/service/search-delegator/elasticsearch-client-types.ts

@@ -1,113 +0,0 @@
-/* eslint-disable camelcase */
-export type NodesInfoResponse = {
-  nodes: Record<
-    string,
-    {
-      version: string
-      plugins: Plugin[]
-    }
-  >
-}
-
-export type CatIndicesResponse = {
-  index: string
-}[]
-
-export type IndicesExistsResponse = boolean
-
-export type IndicesExistsAliasResponse = boolean
-
-export type CatAliasesResponse = {
-  alias: string
-  index: string
-  filter: string
-}[]
-
-export type BulkResponse = {
-  took: number
-  errors: boolean
-  items: Record<string, any>[]
-}
-
-export type SearchResponse = {
-  took: number
-  timed_out: boolean
-  _shards: {
-    total: number
-    successful: number
-    skipped: number
-    failed: number
-  }
-  hits: {
-    total: number | {
-      value: number
-      relation: string
-    } // 6.x.x | 7.x.x
-    max_score: number | null
-    hits: Record<string, {
-      _index: string
-      _type: string
-      _id: string
-      _score: number
-      _source: any
-    }>[]
-  }
-}
-
-export type ValidateQueryResponse = {
-  valid: boolean,
-  _shards: {
-    total: number,
-    successful: number,
-    failed: number
-  },
-  explanations: Record<string, any>[]
-}
-
-export type ClusterHealthResponse = {
-  cluster_name: string,
-  status: string,
-  timed_out: boolean,
-  number_of_nodes: number,
-  number_of_data_nodes: number,
-  active_primary_shards: number,
-  active_shards: number,
-  relocating_shards: number,
-  initializing_shards: number,
-  unassigned_shards: number,
-  delayed_unassigned_shards: number,
-  number_of_pending_tasks: number,
-  number_of_in_flight_fetch: number,
-  task_max_waiting_in_queue_millis: number,
-  active_shards_percent_as_number: number
-}
-
-export type IndicesStatsResponse = {
-  _shards: {
-    total: number,
-    successful: number,
-    failed: number
-  },
-  _all: {
-    primaries: any,
-    total: any
-  },
-  indices: any
-}
-
-export type ReindexResponse = {
-  took: number,
-  timed_out: boolean,
-  total: number,
-  updated: number,
-  created: number,
-  deleted: number,
-  batches: number,
-  noops: number,
-  version_conflicts: number,
-  retries: number,
-  throttled_millis: number,
-  requests_per_second: number,
-  throttled_until_millis: number,
-  failures: any | null
-}

+ 0 - 125
apps/app/src/server/service/search-delegator/elasticsearch-client.ts

@@ -1,125 +0,0 @@
-/* eslint-disable implicit-arrow-linebreak */
-/* eslint-disable no-confusing-arrow */
-import type {
-  ClientOptions as ES7ClientOptions,
-  ApiResponse as ES7ApiResponse,
-  RequestParams as ES7RequestParams,
-} from '@elastic/elasticsearch7';
-import {
-  Client as ES7Client,
-} from '@elastic/elasticsearch7';
-import type { ClientOptions as ES8ClientOptions, estypes } from '@elastic/elasticsearch8';
-import { Client as ES8Client } from '@elastic/elasticsearch8';
-
-import type {
-  BulkResponse,
-  CatAliasesResponse,
-  CatIndicesResponse,
-  IndicesExistsResponse,
-  IndicesExistsAliasResponse,
-  NodesInfoResponse,
-  SearchResponse,
-  ValidateQueryResponse,
-  ClusterHealthResponse,
-  IndicesStatsResponse,
-  ReindexResponse,
-} from './elasticsearch-client-types';
-
-
-type ElasticsearchClientParams =
-  | [ isES7: true, options: ES7ClientOptions, rejectUnauthorized: boolean ]
-  | [ isES7: false, options: ES8ClientOptions, rejectUnauthorized: boolean ]
-
-export default class ElasticsearchClient {
-
-  private client: ES7Client | ES8Client;
-
-  constructor(...params: ElasticsearchClientParams) {
-    const [isES7, options, rejectUnauthorized] = params;
-
-    this.client = isES7
-      ? new ES7Client({ ...options, ssl: { rejectUnauthorized } })
-      : new ES8Client({ ...options, tls: { rejectUnauthorized } });
-  }
-
-  async bulk(params: ES7RequestParams.Bulk & estypes.BulkRequest): Promise<BulkResponse | estypes.BulkResponse> {
-    return this.client instanceof ES7Client ? (await this.client.bulk(params)).body as BulkResponse : this.client.bulk(params);
-  }
-
-  // TODO: cat is not used in current Implementation, remove cat?
-  cat = {
-    aliases: (params: ES7RequestParams.CatAliases & estypes.CatAliasesRequest): Promise<ES7ApiResponse<CatAliasesResponse> | estypes.CatAliasesResponse> =>
-      this.client instanceof ES7Client ? this.client.cat.aliases(params) : this.client.cat.aliases(params),
-
-    indices: (params: ES7RequestParams.CatIndices & estypes.CatIndicesRequest): Promise<ES7ApiResponse<CatIndicesResponse> | estypes.CatAliasesResponse> =>
-      this.client instanceof ES7Client ? this.client.cat.indices(params) : this.client.cat.indices(params),
-  };
-
-  cluster = {
-    health: ()
-    : Promise<ES7ApiResponse<ClusterHealthResponse> | estypes.ClusterHealthResponse> =>
-      this.client instanceof ES7Client ? this.client.cluster.health() : this.client.cluster.health(),
-  };
-
-  indices = {
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    create: (params: ES7RequestParams.IndicesCreate & estypes.IndicesCreateRequest) =>
-      this.client instanceof ES7Client ? this.client.indices.create(params) : this.client.indices.create(params),
-
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    delete: (params: ES7RequestParams.IndicesDelete & estypes.IndicesDeleteRequest) =>
-      this.client instanceof ES7Client ? this.client.indices.delete(params) : this.client.indices.delete(params),
-
-    exists: async(params: ES7RequestParams.IndicesExists & estypes.IndicesExistsRequest)
-    : Promise<IndicesExistsResponse | estypes.IndicesExistsResponse> =>
-      this.client instanceof ES7Client ? (await this.client.indices.exists(params)).body as IndicesExistsResponse : this.client.indices.exists(params),
-
-    existsAlias: async(params: ES7RequestParams.IndicesExistsAlias & estypes.IndicesExistsAliasRequest)
-    : Promise<IndicesExistsAliasResponse | estypes.IndicesExistsAliasResponse> =>
-      this.client instanceof ES7Client
-        ? (await this.client.indices.existsAlias(params)).body as IndicesExistsAliasResponse
-        : this.client.indices.existsAlias(params),
-
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    putAlias: (params: ES7RequestParams.IndicesPutAlias & estypes.IndicesPutAliasRequest) =>
-      this.client instanceof ES7Client ? this.client.indices.putAlias(params) : this.client.indices.putAlias(params),
-
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    getAlias: async(params: ES7RequestParams.IndicesGetAlias & estypes.IndicesGetAliasRequest) =>
-      this.client instanceof ES7Client ? (await this.client.indices.getAlias(params)).body : this.client.indices.getAlias(params),
-
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    updateAliases: (params: ES7RequestParams.IndicesUpdateAliases & estypes.IndicesUpdateAliasesRequest) =>
-      this.client instanceof ES7Client ? this.client.indices.updateAliases(params) : this.client.indices.updateAliases(params),
-
-    validateQuery: async(params: ES7RequestParams.IndicesValidateQuery & estypes.IndicesValidateQueryRequest)
-    : Promise<ValidateQueryResponse | estypes.IndicesValidateQueryResponse> =>
-      // eslint-disable-next-line max-len
-      this.client instanceof ES7Client ? (await this.client.indices.validateQuery(params)).body as ValidateQueryResponse : this.client.indices.validateQuery(params),
-
-    stats: async(params: ES7RequestParams.IndicesStats & estypes.IndicesStatsRequest)
-    : Promise<IndicesStatsResponse | estypes.IndicesStatsResponse> =>
-      this.client instanceof ES7Client ? (await this.client.indices.stats(params)).body as IndicesStatsResponse : this.client.indices.stats(params),
-  };
-
-  nodes = {
-    info: (): Promise<ES7ApiResponse<NodesInfoResponse> | estypes.NodesInfoResponse> =>
-      (this.client instanceof ES7Client ? this.client.nodes.info() : this.client.nodes.info()),
-  };
-
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  ping() {
-    return this.client instanceof ES7Client ? this.client.ping() : this.client.ping();
-  }
-
-  reindex(indexName: string, tmpIndexName: string): Promise<ES7ApiResponse<ReindexResponse> | estypes.ReindexResponse> {
-    return this.client instanceof ES7Client
-      ? this.client.reindex({ wait_for_completion: false, body: { source: { index: indexName }, dest: { index: tmpIndexName } } })
-      : this.client.reindex({ wait_for_completion: false, source: { index: indexName }, dest: { index: tmpIndexName } });
-  }
-
-  async search(params: ES7RequestParams.Search & estypes.SearchRequest): Promise<SearchResponse | estypes.SearchResponse> {
-    return this.client instanceof ES7Client ? (await this.client.search(params)).body as SearchResponse : this.client.search(params);
-  }
-
-}

+ 38 - 127
apps/app/src/server/service/search-delegator/elasticsearch.ts

@@ -11,6 +11,7 @@ import type { ISearchResult, ISearchResultData } from '~/interfaces/search';
 import { SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 import { SocketEventName } from '~/interfaces/websocket';
 import PageTagRelation from '~/server/models/page-tag-relation';
+import type { SocketIoService } from '~/server/service/socket-io';
 import loggerFactory from '~/utils/logger';
 
 import type {
@@ -20,12 +21,10 @@ import type { PageModel } from '../../models/page';
 import { createBatchStream } from '../../util/batch-stream';
 import { configManager } from '../config-manager';
 import type { UpdateOrInsertPagesOpts } from '../interfaces/search';
-// // import { embed, openaiClient, fileUpload } from '../openai';
-// import { getOrCreateSearchAssistant } from '../openai/assistant';
 
 import { aggregatePipelineToIndex } from './aggregate-to-index';
 import type { AggregatedPage, BulkWriteBody, BulkWriteCommand } from './bulk-write';
-import ElasticsearchClient from './elasticsearch-client';
+import { getClient, type ElasticSEarchClientDeletegator } from './elasticsearch-client-delegator';
 
 const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
 
@@ -53,62 +52,41 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
   name!: SearchDelegatorName.DEFAULT;
 
-  socketIoService!: any;
+  private socketIoService!: SocketIoService;
 
-  isElasticsearchV7: boolean;
+  // TODO: https://redmine.weseek.co.jp/issues/168446
+  private isElasticsearchV7: boolean;
 
-  isElasticsearchReindexOnBoot: boolean;
+  private isElasticsearchReindexOnBoot: boolean;
 
-  client: ElasticsearchClient;
+  private elasticsearchVersion: 7 | 8 | 9;
 
-  queries: any;
+  private client: ElasticSEarchClientDeletegator;
 
-  indexName: string;
+  private indexName: string;
 
-  esUri: string | undefined;
-
-  constructor(socketIoService) {
+  constructor(socketIoService: SocketIoService) {
     this.name = SearchDelegatorName.DEFAULT;
     this.socketIoService = socketIoService;
 
     const elasticsearchVersion = configManager.getConfig('app:elasticsearchVersion');
 
-    if (elasticsearchVersion !== 7 && elasticsearchVersion !== 8) {
+    if (elasticsearchVersion !== 7 && elasticsearchVersion !== 8 && elasticsearchVersion !== 9) {
       throw new Error('Unsupported Elasticsearch version. Please specify a valid number to \'ELASTICSEARCH_VERSION\'');
     }
 
     this.isElasticsearchV7 = elasticsearchVersion === 7;
 
-    this.isElasticsearchReindexOnBoot = configManager.getConfig('app:elasticsearchReindexOnBoot');
+    this.elasticsearchVersion = elasticsearchVersion;
 
-    // In Elasticsearch RegExp, we don't need to used ^ and $.
-    // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-regexp-query.html#_standard_operators
-    this.queries = {
-      PORTAL: {
-        regexp: {
-          'path.raw': '.*/',
-        },
-      },
-      PUBLIC: {
-        regexp: {
-          'path.raw': '.*[^/]',
-        },
-      },
-      USER: {
-        prefix: {
-          'path.raw': '/user/',
-        },
-      },
-    };
-
-    this.initClient();
+    this.isElasticsearchReindexOnBoot = configManager.getConfig('app:elasticsearchReindexOnBoot');
   }
 
-  get aliasName() {
+  get aliasName(): string {
     return `${this.indexName}-alias`;
   }
 
-  initClient() {
+  async initClient(): Promise<void> {
     const { host, auth, indexName } = this.getConnectionInfo();
 
     const rejectUnauthorized = configManager.getConfig('app:elasticsearchRejectUnauthorized');
@@ -119,7 +97,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
       requestTimeout: configManager.getConfig('app:elasticsearchRequestTimeout'),
     };
 
-    this.client = new ElasticsearchClient(this.isElasticsearchV7, options, rejectUnauthorized);
+    this.client = await getClient({ version: this.elasticsearchVersion, options, rejectUnauthorized });
     this.indexName = indexName;
   }
 
@@ -133,7 +111,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
    */
   getConnectionInfo() {
     let indexName = 'crowi';
-    let host = this.esUri;
+    let host: string | undefined;
     let auth;
 
     const elasticsearchUri = configManager.getConfig('app:elasticsearchUri');
@@ -159,6 +137,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   }
 
   async init(): Promise<void> {
+    await this.initClient();
     const normalizeIndices = await this.normalizeIndices();
     if (this.isElasticsearchReindexOnBoot) {
       try {
@@ -188,7 +167,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     let esVersion = 'unknown';
     const esNodeInfos = {};
 
-    for (const [nodeName, nodeInfo] of Object.entries<any>(info)) {
+    for (const [nodeName, nodeInfo] of Object.entries(info)) {
       esVersion = nodeInfo.version;
 
       const filteredInfo = {
@@ -267,7 +246,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   /**
    * rebuild index
    */
-  async rebuildIndex() {
+  async rebuildIndex(): Promise<void> {
     const { client, indexName, aliasName } = this;
 
     const tmpIndexName = `${indexName}-tmp`;
@@ -279,12 +258,10 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
       // update alias
       await client.indices.updateAliases({
-        body: {
-          actions: [
-            { add: { alias: aliasName, index: tmpIndexName } },
-            { remove: { alias: aliasName, index: indexName } },
-          ],
-        },
+        actions: [
+          { add: { alias: aliasName, index: tmpIndexName } },
+          { remove: { alias: aliasName, index: indexName } },
+        ],
       });
 
       // flush index
@@ -310,7 +287,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
   }
 
-  async normalizeIndices() {
+  async normalizeIndices(): Promise<void> {
     const { client, indexName, aliasName } = this;
 
     const tmpIndexName = `${indexName}-tmp`;
@@ -395,7 +372,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     return [command, document];
   }
 
-  prepareBodyForDelete(body, page) {
+  prepareBodyForDelete(body, page): void {
     if (!Array.isArray(body)) {
       throw new Error('Body must be an array.');
     }
@@ -432,7 +409,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   /**
    * @param {function} queryFactory factory method to generate a Mongoose Query instance
    */
-  async updateOrInsertPages(queryFactory, option: UpdateOrInsertPagesOpts = {}) {
+  async updateOrInsertPages(queryFactory, option: UpdateOrInsertPagesOpts = {}): Promise<void> {
     const { shouldEmitProgress = false, invokeGarbageCollection = false } = option;
 
     const Page = mongoose.model<IPage, PageModel>('Page');
@@ -479,28 +456,6 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
       },
     });
 
-    // const appendEmbeddingStream = new Transform({
-    //   objectMode: true,
-    //   async transform(chunk: AggregatedPage[], encoding, callback) {
-    //     // append embedding
-    //     for await (const doc of chunk) {
-    //       doc.revisionBodyEmbedded = (await embed(doc.revision.body, doc.creator?.username))[0].embedding;
-    //     }
-
-    //     this.push(chunk);
-    //     callback();
-    //   },
-    // });
-
-    // const appendFileUploadedStream = new Transform({
-    //   objectMode: true,
-    //   async transform(chunk, encoding, callback) {
-    //     await fileUpload(chunk);
-    //     this.push(chunk);
-    //     callback();
-    //   },
-    // });
-
     let count = 0;
     const writeStream = new Writable({
       objectMode: true,
@@ -556,8 +511,6 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
       readStream,
       batchStream,
       appendTagNamesStream,
-      // appendEmbeddingStream,
-      // appendFileUploadedStream,
       writeStream,
     );
   }
@@ -636,8 +589,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     }
 
     // sort by score
-    // eslint-disable-next-line prefer-const
-    let query = {
+    const query = {
       index: this.aliasName,
       _source: fields,
       body: {
@@ -648,12 +600,12 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     return query;
   }
 
-  appendResultSize(query, from?, size?) {
+  appendResultSize(query, from?, size?): void {
     query.from = from || DEFAULT_OFFSET;
     query.size = size || DEFAULT_LIMIT;
   }
 
-  appendSortOrder(query, sortAxis: SORT_AXIS, sortOrder: SORT_ORDER) {
+  appendSortOrder(query, sortAxis: SORT_AXIS, sortOrder: SORT_ORDER): void {
     // default sort order is score descending
     const sort = ES_SORT_AXIS[sortAxis] || ES_SORT_AXIS[RELATION_SCORE];
     const order = ES_SORT_ORDER[sortOrder] || ES_SORT_ORDER[DESC];
@@ -769,7 +721,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     }
   }
 
-  async filterPagesByViewer(query, user, userGroups) {
+  async filterPagesByViewer(query, user, userGroups): Promise<void> {
     const showPagesRestrictedByOwner = !configManager.getConfig('security:list-policy:hideRestrictedByOwner');
     const showPagesRestrictedByGroup = !configManager.getConfig('security:list-policy:hideRestrictedByGroup');
 
@@ -833,7 +785,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     query.body.query.bool.filter.push({ bool: { should: grantConditions } });
   }
 
-  async appendFunctionScore(query, queryString) {
+  async appendFunctionScore(query, queryString): Promise<void> {
     const User = mongoose.model('User');
     const count = await User.count({}) || 1;
 
@@ -857,43 +809,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     };
   }
 
-  // async appendVectorScore(query, queryString: string, username?: string): Promise<void> {
-
-  //   const searchAssistant = await getOrCreateSearchAssistant();
-
-  //   // generate keywords for vector
-  //   const run = await openaiClient.beta.threads.createAndRunPoll({
-  //     assistant_id: searchAssistant.id,
-  //     thread: {
-  //       messages: [
-  //         { role: 'user', content: 'globalLang: "en_US", userLang: "ja_JP", user_input: "武井さんがジョインしたのはいつですか?"' },
-  //         { role: 'assistant', content: '武井さん 武井 takei yuki ジョイン join 入社 加入 雇用開始 年月日 start date join employee' },
-  //         { role: 'user', content: `globalLang: "en_US", userLang: "ja_JP", user_input: "${queryString}"` },
-  //       ],
-  //     },
-  //   });
-  //   const messages = await openaiClient.beta.threads.messages.list(run.thread_id, {
-  //     limit: 1,
-  //   });
-  //   const content = messages.data[0].content[0];
-  //   const keywordsForVector = content.type === 'text' ? content.text.value : queryString;
-
-  //   logger.debug('keywordsFor: ', keywordsForVector);
-
-  //   const queryVector = (await embed(queryString, username))[0].embedding;
-
-  //   query.body.query = {
-  //     script_score: {
-  //       query: { ...query.body.query },
-  //       script: {
-  //         source: "cosineSimilarity(params.query_vector, 'body_embedded') + 1.0",
-  //         params: { query_vector: queryVector },
-  //       },
-  //     },
-  //   };
-  // }
-
-  appendHighlight(query) {
+  appendHighlight(query): void {
     query.body.highlight = {
       fragmenter: 'simple',
       pre_tags: ["<em class='highlighted-keyword'>"],
@@ -926,15 +842,10 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
     const query = this.createSearchQuery();
 
-    if (option?.vector) {
-      // await this.filterPagesByViewer(query, user, userGroups);
-      // await this.appendVectorScore(query, queryString, user?.username);
-    }
-    else {
-      this.appendCriteriaForQueryString(query, terms);
-      await this.filterPagesByViewer(query, user, userGroups);
-      await this.appendFunctionScore(query, queryString);
-    }
+    this.appendCriteriaForQueryString(query, terms);
+    await this.filterPagesByViewer(query, user, userGroups);
+    await this.appendFunctionScore(query, queryString);
+
 
     this.appendResultSize(query, from, size);
 
@@ -965,7 +876,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   }
 
   // remove pages whitch should nod Indexed
-  async syncPagesUpdated(pages, user) {
+  async syncPagesUpdated(pages, user): Promise<void> {
     const shoudDeletePages: any[] = [];
 
     // delete if page should not indexed

+ 5 - 5
pnpm-lock.yaml

@@ -227,8 +227,8 @@ importers:
         specifier: npm:@elastic/elasticsearch@^8.18.2
         version: '@elastic/elasticsearch@8.18.2'
       '@elastic/elasticsearch9':
-        specifier: npm:@elastic/elasticsearch@^9.0.2
-        version: '@elastic/elasticsearch@9.0.2'
+        specifier: npm:@elastic/elasticsearch@^9.0.3
+        version: '@elastic/elasticsearch@9.0.3'
       '@godaddy/terminus':
         specifier: ^4.9.0
         version: 4.12.1
@@ -2745,8 +2745,8 @@ packages:
     resolution: {integrity: sha512-2pOc/hGdxkbaDavfAlnUfjJdVsFRCGqg7fpsWJfJ2UzpgViIyojdViHg8zOCT1J14lAwvDgb9CNETWa3SBZRfw==}
     engines: {node: '>=18'}
 
-  '@elastic/elasticsearch@9.0.2':
-    resolution: {integrity: sha512-uKA0PuPSND3OhHH9UFqnKZfxifAg/8mQW4VnrQ+sUtusTbPhGuErs5NeWCPyd/RLgruBWBmLSv1zzEv5GS+UnA==}
+  '@elastic/elasticsearch@9.0.3':
+    resolution: {integrity: sha512-aagnssrVQi538wExO0Au169amtq68sXSwQMyzblQVAsqcmbqRTtzmGhKOjnDP0LK3ml0Mtje1uX+Vda7RhqDsA==}
     engines: {node: '>=18'}
 
   '@elastic/transport@8.9.7':
@@ -16902,7 +16902,7 @@ snapshots:
       - '@75lb/nature'
       - supports-color
 
-  '@elastic/elasticsearch@9.0.2':
+  '@elastic/elasticsearch@9.0.3':
     dependencies:
       '@elastic/transport': 9.0.2
       apache-arrow: 19.0.1