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

Enhance Elasticsearch client with type-safe search queries and version checks

Shun Miyazawa 8 месяцев назад
Родитель
Сommit
a3b7a994b2

+ 10 - 14
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es7-client-delegator.ts

@@ -7,6 +7,8 @@ import {
   type estypes,
 } from '@elastic/elasticsearch7';
 
+import type { ES7SearchQuery } from './interfaces';
+
 export class ES7ClientDelegator {
 
   private client: Client;
@@ -35,28 +37,23 @@ export class ES7ClientDelegator {
     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;
+      return (await this.client.indices.exists(params)).body;
     },
     existsAlias: async(params: RequestParams.IndicesExistsAlias): Promise<estypes.IndicesExistsAliasResponse> => {
-      const res = (await this.client.indices.existsAlias(params)).body;
-      return res;
+      return (await this.client.indices.existsAlias(params)).body;
     },
     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;
+      return (await this.client.indices.getAlias<estypes.IndicesGetAliasResponse>(params)).body;
     },
     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;
+    validateQuery: async(params:RequestParams.IndicesValidateQuery<ES7SearchQuery>): Promise<estypes.IndicesValidateQueryResponse> => {
+      return (await this.client.indices.validateQuery<estypes.IndicesValidateQueryResponse>(params)).body;
     },
     stats: async(params: RequestParams.IndicesStats): Promise<estypes.IndicesStatsResponse> => {
-      const res = (await this.client.indices.stats(params)).body as estypes.IndicesStatsResponse;
-      return res;
+      return (await this.client.indices.stats<estypes.IndicesStatsResponse>(params)).body;
     },
   };
 
@@ -72,9 +69,8 @@ export class ES7ClientDelegator {
     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;
+  async search(params: ES7SearchQuery): Promise<estypes.SearchResponse> {
+    return (await this.client.search<estypes.SearchResponse>(params)).body;
   }
 
 }

+ 17 - 3
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/interfaces.ts

@@ -63,7 +63,7 @@ export type ES7SearchQuery = RequestParams.Search<{
     indices_boost?: Record<ES7types.IndexName, ES7types.double>[]
     docvalue_fields?: (ES7types.QueryDslFieldAndFormat | ES7types.Field)[]
     min_score?: ES7types.double
-    post_filter?: QueryDslQueryContainer
+    post_filter?: ES7types.QueryDslQueryContainer
     profile?: boolean
     query?: ES7types.QueryDslQueryContainer
     rescore?: ES7types.SearchRescore | ES7types.SearchRescore[]
@@ -86,6 +86,8 @@ export type ES7SearchQuery = RequestParams.Search<{
     stats?: string[]
 }>
 
+// export type ES7SearchQuery = RequestParams.Search<ES7types.QueryDslQueryContainer>
+
 export interface ES8SearchQuery {
   index: ES8types.IndexName
   _source: ES8types.Fields
@@ -112,8 +114,20 @@ export interface ES9SearchQuery {
 
 export type SearchQuery = ES7SearchQuery | ES8SearchQuery | ES9SearchQuery;
 
-export type QueryDslMultiMatchQuery = ES7types.QueryDslMultiMatchQuery | ES8types.QueryDslMultiMatchQuery | ES9types.QueryDslMultiMatchQuery;
-export type QueryDslQueryContainer = ES7types.QueryDslQueryContainer | ES8types.QueryDslQueryContainer | ES9types.QueryDslQueryContainer;
+export const isES7SearchQuery = (clientDelegator: ElasticsearchClientDelegator, query: SearchQuery): query is ES7SearchQuery => {
+  return clientDelegator.delegatorVersion === 7;
+};
+
+export const isES8SearchQuery = (clientDelegator: ElasticsearchClientDelegator, query: SearchQuery): query is ES8SearchQuery => {
+  return clientDelegator.delegatorVersion === 8;
+};
+
+export const isES9SearchQuery = (clientDelegator: ElasticsearchClientDelegator, query: SearchQuery): query is ES9SearchQuery => {
+  return clientDelegator.delegatorVersion === 9;
+};
+
+// export type QueryDslMultiMatchQuery = ES7types.QueryDslMultiMatchQuery | ES8types.QueryDslMultiMatchQuery | ES9types.QueryDslMultiMatchQuery;
+// export type QueryDslQueryContainer = ES7types.QueryDslQueryContainer | ES8types.QueryDslQueryContainer | ES9types.QueryDslQueryContainer;
 
 // export type QueryDslMultiMatchQuery = ES9types.QueryDslMultiMatchQuery;
 // export type QueryDslQueryContainer = ES9types.QueryDslQueryContainer;

+ 63 - 13
apps/app/src/server/service/search-delegator/elasticsearch.ts

@@ -24,12 +24,15 @@ import type { UpdateOrInsertPagesOpts } from '../interfaces/search';
 
 import { aggregatePipelineToIndex } from './aggregate-to-index';
 import type { AggregatedPage, BulkWriteBody, BulkWriteCommand } from './bulk-write';
-import type { SearchQuery } from './elasticsearch-client-delegator';
 import {
   getClient,
   isES7ClientDelegator,
   isES8ClientDelegator,
   isES9ClientDelegator,
+  isES7SearchQuery,
+  isES8SearchQuery,
+  isES9SearchQuery,
+  type SearchQuery,
   type ElasticsearchClientDelegator,
 } from './elasticsearch-client-delegator';
 
@@ -556,26 +559,73 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
    *   data: [ pages ...],
    * }
    */
-  async searchKeyword(query): Promise<ISearchResult<ISearchResultData>> {
+  async searchKeyword(query: SearchQuery): Promise<ISearchResult<ISearchResultData>> {
 
     // for debug
     if (process.env.NODE_ENV === 'development') {
       logger.debug('query: ', JSON.stringify(query, null, 2));
 
-      const validateQueryResponse = await this.client.indices.validateQuery({
-        index: query.index,
-        type: query.type,
-        explain: true,
-        body: {
-          query: query.body.query,
-        },
-      });
+
+      const validateQueryResponse = await (async() => {
+        if (isES7ClientDelegator(this.client) && isES7SearchQuery(this.client, query)) {
+          const { body } = query;
+          return this.client.indices.validateQuery({
+            index: query.index,
+            type: query.type,
+            explain: true,
+            ...body,
+          });
+        }
+
+        if (isES8ClientDelegator(this.client) && isES8SearchQuery(this.client, query)) {
+          const { body } = query;
+          return this.client.indices.validateQuery({
+            index: query.index,
+            explain: true,
+            ...body,
+          });
+        }
+
+        if (isES9SearchQuery(this.client, query) && isES9ClientDelegator(this.client)) {
+          const { body } = query;
+          return this.client.indices.validateQuery({
+            index: query.index,
+            explain: true,
+            ...body,
+          });
+        }
+
+        throw new Error('Unsupported Elasticsearch version');
+      })();
+
 
       // for debug
       logger.debug('ES result: ', validateQueryResponse);
     }
 
-    const searchResponse = await this.client.search(query);
+    const searchResponse = await (async() => {
+      if (isES7ClientDelegator(this.client) && isES7SearchQuery(this.client, query)) {
+        return this.client.search(query);
+      }
+
+      if (isES8ClientDelegator(this.client) && isES8SearchQuery(this.client, query)) {
+        return this.client.search(query);
+      }
+
+      if (isES9ClientDelegator(this.client) && isES9SearchQuery(this.client, query)) {
+        const { body, ...rest } = query;
+        return this.client.search({
+          ...rest,
+          // Elimination of the body property since ES9
+          // https://raw.githubusercontent.com/elastic/elasticsearch-js/2f6200eb397df0e54d23848d769a93614ee1fb45/docs/release-notes/breaking-changes.md
+          query: body.query,
+          sort: body.sort,
+          highlight: body.highlight,
+        });
+      }
+
+      throw new Error('Unsupported Elasticsearch version');
+    })();
 
     const _total = searchResponse?.hits?.total;
     let total = 0;
@@ -714,7 +764,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
             ],
           },
         };
-        query.body.query.bool.must.push(phraseQuery); // TypeScript can track this better
+        query.body.query.bool.must.push(phraseQuery);
       }
     }
 
@@ -723,7 +773,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
         const notPhraseQuery = {
           multi_match: {
             query: phrase, // each phrase is quoteted words
-            type: 'phrase' as const, // Add 'as const' to fix type error
+            type: 'phrase' as const,
             fields: [
               // Not use "*.ja" fields here, because we want to analyze (parse) search words
               'path.raw^2',