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

Merge pull request #10127 from weseek/support/support-es9

support: Elasticsearch9
mergify[bot] 8 месяцев назад
Родитель
Сommit
07bda268e5
21 измененных файлов с 1193 добавлено и 790 удалено
  1. 3 3
      .devcontainer/compose.yml
  2. 3 2
      apps/app/package.json
  3. 0 119
      apps/app/resource/search/mappings-es7.json
  4. 129 0
      apps/app/resource/search/mappings-es7.ts
  5. 0 118
      apps/app/resource/search/mappings-es8-for-ci.json
  6. 0 119
      apps/app/resource/search/mappings-es8.json
  7. 128 0
      apps/app/resource/search/mappings-es8.ts
  8. 127 0
      apps/app/resource/search/mappings-es9-for-ci.ts
  9. 128 0
      apps/app/resource/search/mappings-es9.ts
  10. 3 1
      apps/app/src/server/routes/apiv3/search.js
  11. 2 2
      apps/app/src/server/service/config-manager/config-definition.ts
  12. 77 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es7-client-delegator.ts
  13. 54 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es8-client-delegator.ts
  14. 54 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es9-client-delegator.ts
  15. 60 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/get-client.ts
  16. 2 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/index.ts
  17. 59 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/interfaces.ts
  18. 0 113
      apps/app/src/server/service/search-delegator/elasticsearch-client-types.ts
  19. 0 125
      apps/app/src/server/service/search-delegator/elasticsearch-client.ts
  20. 186 173
      apps/app/src/server/service/search-delegator/elasticsearch.ts
  21. 178 15
      pnpm-lock.yaml

+ 3 - 3
.devcontainer/compose.yml

@@ -26,10 +26,10 @@ services:
   #   cloned from https://github.com/weseek/growi-docker-compose.git
   elasticsearch:
     build:
-      context: ../../growi-docker-compose/elasticsearch/v8
+      context: ../../growi-docker-compose/elasticsearch/v9
       dockerfile: ./Dockerfile
       args:
-        - version=8.7.0
+        - version=9.0.3
     restart: unless-stopped
     ports:
       - 9200
@@ -43,7 +43,7 @@ services:
         hard: -1
     volumes:
       - /usr/share/elasticsearch/data
-      - ../../growi-docker-compose/elasticsearch/v8/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
+      - ../../growi-docker-compose/elasticsearch/v9/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
 
 volumes:
   pnpm-store:

+ 3 - 2
apps/app/package.json

@@ -70,8 +70,9 @@
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@cspell/dynamic-import": "^8.15.4",
-    "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
-    "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
+    "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.4",
+    "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.18.2",
+    "@elastic/elasticsearch9": "npm:@elastic/elasticsearch@^9.0.3",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@growi/core": "workspace:^",

+ 0 - 119
apps/app/resource/search/mappings-es7.json

@@ -1,119 +0,0 @@
-{
-  "settings": {
-    "analysis": {
-      "filter": {
-        "english_stop": {
-          "type":       "stop",
-          "stopwords":  "_english_"
-        }
-      },
-      "tokenizer": {
-        "edge_ngram_tokenizer": {
-          "type": "edge_ngram",
-          "min_gram": 2,
-          "max_gram": 20,
-          "token_chars": ["letter", "digit"]
-        }
-      },
-      "analyzer": {
-        "japanese": {
-          "tokenizer": "kuromoji_tokenizer",
-          "char_filter" : ["icu_normalizer"]
-        },
-        "english_edge_ngram": {
-          "tokenizer": "edge_ngram_tokenizer",
-          "filter": [
-            "lowercase",
-            "english_stop"
-          ]
-        }
-      }
-    }
-  },
-  "mappings": {
-    "properties" : {
-      "path": {
-        "type": "text",
-        "fields": {
-          "raw": {
-            "type": "text",
-            "analyzer": "keyword"
-          },
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body_embedded": {
-        "type": "dense_vector",
-        "dims": 768
-      },
-      "comments": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "username": {
-        "type": "keyword"
-      },
-      "comment_count": {
-        "type": "integer"
-      },
-      "bookmark_count": {
-        "type": "integer"
-      },
-      "like_count": {
-        "type": "integer"
-      },
-      "grant": {
-        "type": "integer"
-      },
-      "granted_users": {
-        "type": "keyword"
-      },
-      "granted_groups": {
-        "type": "keyword"
-      },
-      "created_at": {
-        "type": "date",
-        "format": "dateOptionalTime"
-      },
-      "updated_at": {
-        "type": "date",
-        "format": "dateOptionalTime"
-      },
-      "tag_names": {
-        "type": "keyword"
-      }
-    }
-  }
-}

+ 129 - 0
apps/app/resource/search/mappings-es7.ts

@@ -0,0 +1,129 @@
+// TODO: https://redmine.weseek.co.jp/issues/168446
+import type { estypes } from '@elastic/elasticsearch7';
+
+type Mappings = {
+  settings: NonNullable<estypes.IndicesCreateRequest['body']>['settings'];
+  mappings: NonNullable<estypes.IndicesCreateRequest['body']>['mappings'];
+}
+
+export const mappings: Mappings = {
+  settings: {
+    analysis: {
+      filter: {
+        english_stop: {
+          type:       'stop',
+          stopwords:  '_english_',
+        },
+      },
+      tokenizer: {
+        edge_ngram_tokenizer: {
+          type: 'edge_ngram',
+          min_gram: 2,
+          max_gram: 20,
+          token_chars: ['letter', 'digit'],
+        },
+      },
+      analyzer: {
+        japanese: {
+          type: 'custom',
+          tokenizer: 'kuromoji_tokenizer',
+          char_filter: ['icu_normalizer'],
+        },
+        english_edge_ngram: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+      },
+    },
+  },
+  mappings: {
+    properties: {
+      path: {
+        type: 'text',
+        fields: {
+          raw: {
+            type: 'text',
+            analyzer: 'keyword',
+          },
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body_embedded: {
+        type: 'dense_vector',
+        dims: 768,
+      },
+      comments: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      username: {
+        type: 'keyword',
+      },
+      comment_count: {
+        type: 'integer',
+      },
+      bookmark_count: {
+        type: 'integer',
+      },
+      like_count: {
+        type: 'integer',
+      },
+      grant: {
+        type: 'integer',
+      },
+      granted_users: {
+        type: 'keyword',
+      },
+      granted_groups: {
+        type: 'keyword',
+      },
+      created_at: {
+        type: 'date',
+        format: 'dateOptionalTime',
+      },
+      updated_at: {
+        type: 'date',
+        format: 'dateOptionalTime',
+      },
+      tag_names: {
+        type: 'keyword',
+      },
+    },
+  },
+};

+ 0 - 118
apps/app/resource/search/mappings-es8-for-ci.json

@@ -1,118 +0,0 @@
-{
-  "settings": {
-    "analysis": {
-      "filter": {
-        "english_stop": {
-          "type":       "stop",
-          "stopwords":  "_english_"
-        }
-      },
-      "tokenizer": {
-        "edge_ngram_tokenizer": {
-          "type": "edge_ngram",
-          "min_gram": 2,
-          "max_gram": 20,
-          "token_chars": ["letter", "digit"]
-        }
-      },
-      "analyzer": {
-        "japanese": {
-          "tokenizer": "edge_ngram_tokenizer",
-          "filter": [
-            "lowercase",
-            "english_stop"
-          ]
-        },
-        "english_edge_ngram": {
-          "tokenizer": "edge_ngram_tokenizer",
-          "filter": [
-            "lowercase",
-            "english_stop"
-          ]
-        }
-      }
-    }
-  },
-  "mappings": {
-    "properties" : {
-      "path": {
-        "type": "text",
-        "fields": {
-          "raw": {
-            "type": "text",
-            "analyzer": "keyword"
-          },
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "comments": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "username": {
-        "type": "keyword"
-      },
-      "comment_count": {
-        "type": "integer"
-      },
-      "bookmark_count": {
-        "type": "integer"
-      },
-      "like_count": {
-        "type": "integer"
-      },
-      "grant": {
-        "type": "integer"
-      },
-      "granted_users": {
-        "type": "keyword"
-      },
-      "granted_groups": {
-        "type": "keyword"
-      },
-      "created_at": {
-        "type": "date",
-        "format": "date_optional_time"
-      },
-      "updated_at": {
-        "type": "date",
-        "format": "date_optional_time"
-      },
-      "tag_names": {
-        "type": "keyword"
-      }
-    }
-  }
-}

+ 0 - 119
apps/app/resource/search/mappings-es8.json

@@ -1,119 +0,0 @@
-{
-  "settings": {
-    "analysis": {
-      "filter": {
-        "english_stop": {
-          "type":       "stop",
-          "stopwords":  "_english_"
-        }
-      },
-      "tokenizer": {
-        "edge_ngram_tokenizer": {
-          "type": "edge_ngram",
-          "min_gram": 2,
-          "max_gram": 20,
-          "token_chars": ["letter", "digit"]
-        }
-      },
-      "analyzer": {
-        "japanese": {
-          "tokenizer": "kuromoji_tokenizer",
-          "char_filter" : ["icu_normalizer"]
-        },
-        "english_edge_ngram": {
-          "tokenizer": "edge_ngram_tokenizer",
-          "filter": [
-            "lowercase",
-            "english_stop"
-          ]
-        }
-      }
-    }
-  },
-  "mappings": {
-    "properties" : {
-      "path": {
-        "type": "text",
-        "fields": {
-          "raw": {
-            "type": "text",
-            "analyzer": "keyword"
-          },
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body_embedded": {
-        "type": "dense_vector",
-        "dims": 768
-      },
-      "comments": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "username": {
-        "type": "keyword"
-      },
-      "comment_count": {
-        "type": "integer"
-      },
-      "bookmark_count": {
-        "type": "integer"
-      },
-      "like_count": {
-        "type": "integer"
-      },
-      "grant": {
-        "type": "integer"
-      },
-      "granted_users": {
-        "type": "keyword"
-      },
-      "granted_groups": {
-        "type": "keyword"
-      },
-      "created_at": {
-        "type": "date",
-        "format": "date_optional_time"
-      },
-      "updated_at": {
-        "type": "date",
-        "format": "date_optional_time"
-      },
-      "tag_names": {
-        "type": "keyword"
-      }
-    }
-  }
-}

+ 128 - 0
apps/app/resource/search/mappings-es8.ts

@@ -0,0 +1,128 @@
+import type { estypes } from '@elastic/elasticsearch8';
+
+type Mappings = {
+  settings: estypes.IndicesCreateRequest['settings'];
+  mappings: estypes.IndicesCreateRequest['mappings'];
+}
+
+export const mappings: Mappings = {
+  settings: {
+    analysis: {
+      filter: {
+        english_stop: {
+          type:       'stop',
+          stopwords:  '_english_',
+        },
+      },
+      tokenizer: {
+        edge_ngram_tokenizer: {
+          type: 'edge_ngram',
+          min_gram: 2,
+          max_gram: 20,
+          token_chars: ['letter', 'digit'],
+        },
+      },
+      analyzer: {
+        japanese: {
+          type: 'custom',
+          tokenizer: 'kuromoji_tokenizer',
+          char_filter: ['icu_normalizer'],
+        },
+        english_edge_ngram: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+      },
+    },
+  },
+  mappings: {
+    properties: {
+      path: {
+        type: 'text',
+        fields: {
+          raw: {
+            type: 'text',
+            analyzer: 'keyword',
+          },
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body_embedded: {
+        type: 'dense_vector',
+        dims: 768,
+      },
+      comments: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      username: {
+        type: 'keyword',
+      },
+      comment_count: {
+        type: 'integer',
+      },
+      bookmark_count: {
+        type: 'integer',
+      },
+      like_count: {
+        type: 'integer',
+      },
+      grant: {
+        type: 'integer',
+      },
+      granted_users: {
+        type: 'keyword',
+      },
+      granted_groups: {
+        type: 'keyword',
+      },
+      created_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      updated_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      tag_names: {
+        type: 'keyword',
+      },
+    },
+  },
+};

+ 127 - 0
apps/app/resource/search/mappings-es9-for-ci.ts

@@ -0,0 +1,127 @@
+import type { estypes } from '@elastic/elasticsearch9';
+
+type Mappings = {
+  settings: estypes.IndicesCreateRequest['settings'];
+  mappings: estypes.IndicesCreateRequest['mappings'];
+}
+
+export const mappings: Mappings = {
+  settings: {
+    analysis: {
+      filter: {
+        english_stop: {
+          type:       'stop',
+          stopwords:  '_english_',
+        },
+      },
+      tokenizer: {
+        edge_ngram_tokenizer: {
+          type: 'edge_ngram',
+          min_gram: 2,
+          max_gram: 20,
+          token_chars: ['letter', 'digit'],
+        },
+      },
+      analyzer: {
+        japanese: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+        english_edge_ngram: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+      },
+    },
+  },
+  mappings: {
+    properties: {
+      path: {
+        type: 'text',
+        fields: {
+          raw: {
+            type: 'text',
+            analyzer: 'keyword',
+          },
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      comments: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      username: {
+        type: 'keyword',
+      },
+      comment_count: {
+        type: 'integer',
+      },
+      bookmark_count: {
+        type: 'integer',
+      },
+      like_count: {
+        type: 'integer',
+      },
+      grant: {
+        type: 'integer',
+      },
+      granted_users: {
+        type: 'keyword',
+      },
+      granted_groups: {
+        type: 'keyword',
+      },
+      created_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      updated_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      tag_names: {
+        type: 'keyword',
+      },
+    },
+  },
+};

+ 128 - 0
apps/app/resource/search/mappings-es9.ts

@@ -0,0 +1,128 @@
+import type { estypes } from '@elastic/elasticsearch9';
+
+type Mappings = {
+  settings: estypes.IndicesCreateRequest['settings'];
+  mappings: estypes.IndicesCreateRequest['mappings'];
+}
+
+export const mappings: Mappings = {
+  settings: {
+    analysis: {
+      filter: {
+        english_stop: {
+          type:       'stop',
+          stopwords:  '_english_',
+        },
+      },
+      tokenizer: {
+        edge_ngram_tokenizer: {
+          type: 'edge_ngram',
+          min_gram: 2,
+          max_gram: 20,
+          token_chars: ['letter', 'digit'],
+        },
+      },
+      analyzer: {
+        japanese: {
+          type: 'custom',
+          tokenizer: 'kuromoji_tokenizer',
+          char_filter: ['icu_normalizer'],
+        },
+        english_edge_ngram: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+      },
+    },
+  },
+  mappings: {
+    properties: {
+      path: {
+        type: 'text',
+        fields: {
+          raw: {
+            type: 'text',
+            analyzer: 'keyword',
+          },
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body_embedded: {
+        type: 'dense_vector',
+        dims: 768,
+      },
+      comments: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      username: {
+        type: 'keyword',
+      },
+      comment_count: {
+        type: 'integer',
+      },
+      bookmark_count: {
+        type: 'integer',
+      },
+      like_count: {
+        type: 'integer',
+      },
+      grant: {
+        type: 'integer',
+      },
+      granted_users: {
+        type: 'keyword',
+      },
+      granted_groups: {
+        type: 'keyword',
+      },
+      created_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      updated_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      tag_names: {
+        type: 'keyword',
+      },
+    },
+  },
+};

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

@@ -1,7 +1,7 @@
+import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 
 import { SupportedAction } from '~/interfaces/activity';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import loggerFactory from '~/utils/logger';
 
@@ -139,6 +139,7 @@ module.exports = (crowi) => {
         return res.status(200).send({ info });
       }
       catch (err) {
+        logger.error(err);
         return res.apiv3Err(err, 503);
       }
     });
@@ -171,6 +172,7 @@ module.exports = (crowi) => {
         return res.status(200).send();
       }
       catch (err) {
+        logger.error(err);
         return res.apiv3Err(err, 503);
       }
     });

+ 2 - 2
apps/app/src/server/service/config-manager/config-definition.ts

@@ -433,9 +433,9 @@ export const CONFIG_DEFINITIONS = {
     envVarName: 'FILE_UPLOAD_TOTAL_LIMIT',
     defaultValue: Infinity,
   }),
-  'app:elasticsearchVersion': defineConfig<number>({
+  'app:elasticsearchVersion': defineConfig<7|8|9>({
     envVarName: 'ELASTICSEARCH_VERSION',
-    defaultValue: 8,
+    defaultValue: 9,
   }),
   'app:elasticsearchUri': defineConfig<string | undefined>({
     envVarName: 'ELASTICSEARCH_URI',

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

@@ -0,0 +1,77 @@
+// TODO: https://redmine.weseek.co.jp/issues/168446
+import {
+  Client,
+  type ClientOptions,
+  type ApiResponse,
+  type RequestParams,
+  type estypes,
+} from '@elastic/elasticsearch7';
+
+import type { ES7SearchQuery } from './interfaces';
+
+export class ES7ClientDelegator {
+
+  private client: Client;
+
+  delegatorVersion = 7 as const;
+
+  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> => {
+      return (await this.client.indices.exists(params)).body;
+    },
+    existsAlias: async(params: RequestParams.IndicesExistsAlias): Promise<estypes.IndicesExistsAliasResponse> => {
+      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> => {
+      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<{ query?: estypes.QueryDslQueryContainer }>)
+      : Promise<estypes.IndicesValidateQueryResponse> => {
+      return (await this.client.indices.validateQuery<estypes.IndicesValidateQueryResponse>(params)).body;
+    },
+    stats: async(params: RequestParams.IndicesStats): Promise<estypes.IndicesStatsResponse> => {
+      return (await this.client.indices.stats<estypes.IndicesStatsResponse>(params)).body;
+    },
+  };
+
+  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: ES7SearchQuery): Promise<estypes.SearchResponse> {
+    return (await this.client.search<estypes.SearchResponse>(params)).body;
+  }
+
+}

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

@@ -0,0 +1,54 @@
+import { Client, type ClientOptions, type estypes } from '@elastic/elasticsearch8';
+
+export class ES8ClientDelegator {
+
+  private client: Client;
+
+  delegatorVersion = 8 as const;
+
+  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);
+  }
+
+}

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

@@ -0,0 +1,54 @@
+import { Client, type ClientOptions, type estypes } from '@elastic/elasticsearch9';
+
+export class ES9ClientDelegator {
+
+  private client: Client;
+
+  delegatorVersion = 9 as const;
+
+  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 { ElasticsearchClientDelegator } 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
+    ? ElasticsearchClientDelegator
+    : Opts extends { version: 7 }
+      ? ES7ClientDelegator
+      : Opts extends { version: 8 }
+        ? ES8ClientDelegator
+        : Opts extends { version: 9 }
+          ? ES9ClientDelegator
+          : ElasticsearchClientDelegator
+
+let instance: ElasticsearchClientDelegator | 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';

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

@@ -0,0 +1,59 @@
+import type { estypes as ES7types, RequestParams } from '@elastic/elasticsearch7';
+import type { estypes as ES8types } from '@elastic/elasticsearch8';
+import type { estypes as ES9types } from '@elastic/elasticsearch9';
+
+import type { ES7ClientDelegator } from './es7-client-delegator';
+import type { ES8ClientDelegator } from './es8-client-delegator';
+import type { ES9ClientDelegator } from './es9-client-delegator';
+
+export type ElasticsearchClientDelegator = ES7ClientDelegator | ES8ClientDelegator | ES9ClientDelegator;
+
+
+// type guard
+// TODO: https://redmine.weseek.co.jp/issues/168446
+export const isES7ClientDelegator = (delegator: ElasticsearchClientDelegator): delegator is ES7ClientDelegator => {
+  return delegator.delegatorVersion === 7;
+};
+
+export const isES8ClientDelegator = (delegator: ElasticsearchClientDelegator): delegator is ES8ClientDelegator => {
+  return delegator.delegatorVersion === 8;
+};
+
+export const isES9ClientDelegator = (delegator: ElasticsearchClientDelegator): delegator is ES9ClientDelegator => {
+  return delegator.delegatorVersion === 9;
+};
+
+
+// Official library-derived interface
+// TODO: https://redmine.weseek.co.jp/issues/168446
+export type ES7SearchQuery = RequestParams.Search<{
+  query: ES7types.QueryDslQueryContainer
+  sort?: ES7types.Sort
+  highlight?: ES7types.SearchHighlight
+}>
+
+export interface ES8SearchQuery {
+  index: ES8types.IndexName
+  _source: ES8types.Fields
+  from?: number;
+  size?: number;
+  body: {
+    query: ES8types.QueryDslQueryContainer;
+    sort?: ES8types.Sort
+    highlight?: ES8types.SearchHighlight;
+  };
+}
+
+export interface ES9SearchQuery {
+  index: ES9types.IndexName
+  _source: ES9types.Fields
+  from?: number;
+  size?: number;
+  body: {
+    query: ES9types.QueryDslQueryContainer;
+    sort?: ES9types.Sort
+    highlight?: ES9types.SearchHighlight;
+  };
+}
+
+export type SearchQuery = ES7SearchQuery | ES8SearchQuery | ES9SearchQuery;

+ 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);
-  }
-
-}

+ 186 - 173
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,20 @@ 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,
+  isES7ClientDelegator,
+  isES8ClientDelegator,
+  isES9ClientDelegator,
+  type SearchQuery,
+  type ES7SearchQuery,
+  type ES8SearchQuery,
+  type ES9SearchQuery,
+  type ElasticsearchClientDelegator,
+} from './elasticsearch-client-delegator';
 
 const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
 
@@ -43,7 +52,7 @@ const ES_SORT_AXIS = {
 const ES_SORT_ORDER = {
   [DESC]: 'desc',
   [ASC]: 'asc',
-};
+} as const;
 
 const AVAILABLE_KEYS = ['match', 'not_match', 'phrase', 'not_phrase', 'prefix', 'not_prefix', 'tag', 'not_tag'];
 
@@ -53,64 +62,41 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
   name!: SearchDelegatorName.DEFAULT;
 
-  socketIoService!: any;
-
-  isElasticsearchV7: boolean;
-
-  isElasticsearchReindexOnBoot: boolean;
+  private socketIoService!: SocketIoService;
 
-  elasticsearch: any;
+  // TODO: https://redmine.weseek.co.jp/issues/168446
+  private isElasticsearchV7: boolean;
 
-  client: ElasticsearchClient;
+  private isElasticsearchReindexOnBoot: boolean;
 
-  queries: any;
+  private elasticsearchVersion: 7 | 8 | 9;
 
-  indexName: string;
+  private client: ElasticsearchClientDelegator;
 
-  esUri: string | undefined;
+  private indexName: string;
 
-  constructor(socketIoService) {
+  constructor(socketIoService: SocketIoService) {
     this.name = SearchDelegatorName.DEFAULT;
     this.socketIoService = socketIoService;
 
-    const elasticsearchVersion: number = configManager.getConfig('app:elasticsearchVersion');
+    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');
@@ -121,7 +107,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;
   }
 
@@ -135,7 +121,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');
@@ -161,6 +147,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   }
 
   async init(): Promise<void> {
+    await this.initClient();
     const normalizeIndices = await this.normalizeIndices();
     if (this.isElasticsearchReindexOnBoot) {
       try {
@@ -190,7 +177,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 = {
@@ -269,7 +256,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`;
@@ -281,12 +268,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
@@ -312,7 +297,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
   }
 
-  async normalizeIndices() {
+  async normalizeIndices(): Promise<void> {
     const { client, indexName, aliasName } = this;
 
     const tmpIndexName = `${indexName}-tmp`;
@@ -339,19 +324,36 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     }
   }
 
-  async createIndex(index) {
-    let mappings = this.isElasticsearchV7
-      ? require('^/resource/search/mappings-es7.json')
-      : require('^/resource/search/mappings-es8.json');
+  async createIndex(index: string) {
+    // TODO: https://redmine.weseek.co.jp/issues/168446
+    if (isES7ClientDelegator(this.client)) {
+      const { mappings } = await import('^/resource/search/mappings-es7');
+      return this.client.indices.create({
+        index,
+        body: {
+          ...mappings,
+        },
+      });
+    }
 
-    if (process.env.CI) {
-      mappings = require('^/resource/search/mappings-es8-for-ci.json');
+    if (isES8ClientDelegator(this.client)) {
+      const { mappings } = await import('^/resource/search/mappings-es8');
+      return this.client.indices.create({
+        index,
+        ...mappings,
+      });
     }
 
-    return this.client.indices.create({
-      index,
-      body: mappings,
-    });
+    if (isES9ClientDelegator(this.client)) {
+      const { mappings } = process.env.CI == null
+        ? await import('^/resource/search/mappings-es9')
+        : await import('^/resource/search/mappings-es9-for-ci');
+
+      return this.client.indices.create({
+        index,
+        ...mappings,
+      });
+    }
   }
 
   /**
@@ -397,7 +399,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.');
     }
@@ -434,7 +436,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');
@@ -481,28 +483,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,
@@ -558,8 +538,6 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
       readStream,
       batchStream,
       appendTagNamesStream,
-      // appendEmbeddingStream,
-      // appendFileUploadedStream,
       writeStream,
     );
   }
@@ -581,26 +559,74 @@ 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)) {
+          const es7SearchQuery = query as ES7SearchQuery;
+          return this.client.indices.validateQuery({
+            explain: true,
+            index: es7SearchQuery.index,
+            body: {
+              query: es7SearchQuery.body?.query,
+            },
+          });
+        }
+
+        if (isES8ClientDelegator(this.client)) {
+          const es8SearchQuery = query as ES8SearchQuery;
+          return this.client.indices.validateQuery({
+            explain: true,
+            index: es8SearchQuery.index,
+            query: es8SearchQuery.body.query,
+          });
+        }
+
+        if (isES9ClientDelegator(this.client)) {
+          const es9SearchQuery = query as ES9SearchQuery;
+          return this.client.indices.validateQuery({
+            explain: true,
+            index: es9SearchQuery.index,
+            query: es9SearchQuery.body.query,
+          });
+        }
+
+        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)) {
+        return this.client.search(query as ES7SearchQuery);
+      }
+
+      if (isES8ClientDelegator(this.client)) {
+        return this.client.search(query as ES8SearchQuery);
+      }
+
+      if (isES9ClientDelegator(this.client)) {
+        const { body, ...rest } = query as ES9SearchQuery;
+        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;
@@ -627,45 +653,49 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
   /**
    * create search query for Elasticsearch
-   *
-   * @param {object | undefined} option optional paramas
    * @returns {object} query object
    */
-  createSearchQuery(option?) {
-    let fields = ['path', 'bookmark_count', 'comment_count', 'seenUsers_count', 'updated_at', 'tag_names', 'comments'];
-    if (option) {
-      fields = option.fields || fields;
-    }
+  createSearchQuery(): SearchQuery {
+    const fields = ['path', 'bookmark_count', 'comment_count', 'seenUsers_count', 'updated_at', 'tag_names', 'comments'];
 
     // sort by score
-    // eslint-disable-next-line prefer-const
-    let query = {
+    const query: SearchQuery = {
       index: this.aliasName,
       _source: fields,
       body: {
-        query: {}, // query
+        query: {
+          bool: {},
+        },
       },
     };
 
     return query;
   }
 
-  appendResultSize(query, from?, size?) {
+  appendResultSize(query: SearchQuery, from?: number, size?: number): void {
     query.from = from || DEFAULT_OFFSET;
     query.size = size || DEFAULT_LIMIT;
   }
 
-  appendSortOrder(query, sortAxis: SORT_AXIS, sortOrder: SORT_ORDER) {
+  appendSortOrder(query: SearchQuery, sortAxis: SORT_AXIS, sortOrder: SORT_ORDER): void {
+    if (query.body == null) {
+      throw new Error('query.body is not initialized');
+    }
+
     // 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];
-    query.body.sort = { [sort]: { order } };
+
+    query.body.sort = {
+      [sort]: { order },
+    };
+
   }
 
-  initializeBoolQuery(query) {
+  initializeBoolQuery(query: SearchQuery): SearchQuery {
     // query is created by createSearchQuery()
-    if (!query.body.query.bool) {
-      query.body.query.bool = {};
+    if (query?.body?.query?.bool == null) {
+      throw new Error('query.body.query.bool is not initialized');
     }
 
     const isInitialized = (query) => { return !!query && Array.isArray(query) };
@@ -682,14 +712,30 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     return query;
   }
 
-  appendCriteriaForQueryString(query, parsedKeywords: ESQueryTerms): void {
+  appendCriteriaForQueryString(query: SearchQuery, parsedKeywords: ESQueryTerms): void {
     query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
 
+    if (query.body?.query?.bool == null) {
+      throw new Error('query.body.query.bool is not initialized');
+    }
+
+    if (query.body?.query?.bool.must == null || !Array.isArray(query.body?.query?.bool.must)) {
+      throw new Error('query.body.query.bool.must is not initialized');
+    }
+
+    if (query.body?.query?.bool.must_not == null || !Array.isArray(query.body?.query?.bool.must_not)) {
+      throw new Error('query.body.query.bool.must_not is not initialized');
+    }
+
+    if (query.body?.query?.bool.filter == null || !Array.isArray(query.body?.query?.bool.filter)) {
+      throw new Error('query.body.query.bool.filter is not initialized');
+    }
+
     if (parsedKeywords.match.length > 0) {
       const q = {
         multi_match: {
           query: parsedKeywords.match.join(' '),
-          type: 'most_fields',
+          type: 'most_fields' as const,
           fields: ['path.ja^2', 'path.en^2', 'body.ja', 'body.en', 'comments.ja', 'comments.en'],
         },
       };
@@ -701,18 +747,18 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
         multi_match: {
           query: parsedKeywords.not_match.join(' '),
           fields: ['path.ja', 'path.en', 'body.ja', 'body.en', 'comments.ja', 'comments.en'],
-          operator: 'or',
+          operator: 'or' as const,
         },
       };
       query.body.query.bool.must_not.push(q);
     }
 
     if (parsedKeywords.phrase.length > 0) {
-      parsedKeywords.phrase.forEach((phrase) => {
+      for (const phrase of parsedKeywords.phrase) {
         const phraseQuery = {
           multi_match: {
-            query: phrase, // each phrase is quoteted words like "This is GROWI"
-            type: 'phrase',
+            query: phrase, // query is created by createSearchQuery()
+            type: 'phrase' as const,
             fields: [
               // Not use "*.ja" fields here, because we want to analyze (parse) search words
               'path.raw^2',
@@ -722,15 +768,15 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
           },
         };
         query.body.query.bool.must.push(phraseQuery);
-      });
+      }
     }
 
     if (parsedKeywords.not_phrase.length > 0) {
-      parsedKeywords.not_phrase.forEach((phrase) => {
+      for (const phrase of parsedKeywords.not_phrase) {
         const notPhraseQuery = {
           multi_match: {
             query: phrase, // each phrase is quoteted words
-            type: 'phrase',
+            type: 'phrase' as const,
             fields: [
               // Not use "*.ja" fields here, because we want to analyze (parse) search words
               'path.raw^2',
@@ -739,7 +785,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
           },
         };
         query.body.query.bool.must_not.push(notPhraseQuery);
-      });
+      }
     }
 
     if (parsedKeywords.prefix.length > 0) {
@@ -771,12 +817,16 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     }
   }
 
-  async filterPagesByViewer(query, user, userGroups) {
+  async filterPagesByViewer(query: SearchQuery, user, userGroups): Promise<void> {
     const showPagesRestrictedByOwner = !configManager.getConfig('security:list-policy:hideRestrictedByOwner');
     const showPagesRestrictedByGroup = !configManager.getConfig('security:list-policy:hideRestrictedByGroup');
 
     query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
 
+    if (query.body?.query?.bool?.filter == null || !Array.isArray(query.body?.query?.bool?.filter)) {
+      throw new Error('query.body.query.bool is not initialized');
+    }
+
     const Page = mongoose.model('Page') as unknown as PageModel;
     const {
       GRANT_PUBLIC, GRANT_SPECIFIED, GRANT_OWNER, GRANT_USER_GROUP,
@@ -835,7 +885,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;
 
@@ -859,43 +909,11 @@ 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: SearchQuery): void {
+    if (query.body == null) {
+      throw new Error('query.body is not initialized');
+    }
+
     query.body.highlight = {
       fragmenter: 'simple',
       pre_tags: ["<em class='highlighted-keyword'>"],
@@ -928,15 +946,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);
 
@@ -967,7 +980,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

+ 178 - 15
pnpm-lock.yaml

@@ -221,11 +221,14 @@ importers:
         specifier: ^8.15.4
         version: 8.15.4
       '@elastic/elasticsearch7':
-        specifier: npm:@elastic/elasticsearch@^7.17.0
+        specifier: npm:@elastic/elasticsearch@^7.17.4
         version: '@elastic/elasticsearch@7.17.13'
       '@elastic/elasticsearch8':
-        specifier: npm:@elastic/elasticsearch@^8.7.0
-        version: '@elastic/elasticsearch@8.14.0'
+        specifier: npm:@elastic/elasticsearch@^8.18.2
+        version: '@elastic/elasticsearch@8.18.2'
+      '@elastic/elasticsearch9':
+        specifier: npm:@elastic/elasticsearch@^9.0.3
+        version: '@elastic/elasticsearch@9.0.3'
       '@godaddy/terminus':
         specifier: ^4.9.0
         version: 4.12.1
@@ -2738,14 +2741,22 @@ packages:
     resolution: {integrity: sha512-GMXtFVqd3FgUlTtPL/GDc+3GhwvfZ0kSuegCvVVqb58kd+0I6U6u7PL8QFRLHtwzqLEBmYLdwr4PRkBAWKGlzA==}
     engines: {node: '>=12'}
 
-  '@elastic/elasticsearch@8.14.0':
-    resolution: {integrity: sha512-MGrgCI4y+Ozssf5Q2IkVJlqt5bUMnKIICG2qxeOfrJNrVugMCBCAQypyesmSSocAtNm8IX3LxfJ3jQlFHmKe2w==}
+  '@elastic/elasticsearch@8.18.2':
+    resolution: {integrity: sha512-2pOc/hGdxkbaDavfAlnUfjJdVsFRCGqg7fpsWJfJ2UzpgViIyojdViHg8zOCT1J14lAwvDgb9CNETWa3SBZRfw==}
     engines: {node: '>=18'}
 
-  '@elastic/transport@8.6.1':
-    resolution: {integrity: sha512-3vGs4W3wP5oeIT/4j1vcvd+t7m6ndP0uyb5GDY23LQCmbtI5Oq0aQwD9gb09KJbLFLUbI7db9vMFPzKavSFA5g==}
+  '@elastic/elasticsearch@9.0.3':
+    resolution: {integrity: sha512-aagnssrVQi538wExO0Au169amtq68sXSwQMyzblQVAsqcmbqRTtzmGhKOjnDP0LK3ml0Mtje1uX+Vda7RhqDsA==}
     engines: {node: '>=18'}
 
+  '@elastic/transport@8.9.7':
+    resolution: {integrity: sha512-zdLkkahbWM/O1MAZ0rAu0xg+JJUgRoAGSOf2TWLkdDk42BMqOfwVG+Qz1ZnbhfydkpWiSmGMhbRBhqqlqWdwog==}
+    engines: {node: '>=18'}
+
+  '@elastic/transport@9.0.2':
+    resolution: {integrity: sha512-7okzzK9wP+qIFAw49/jAFYYHpJHBsDYfFt6dI2OBU8PRHEFCBqAPErTH5GBtgrs6rx/U1798kton5Ofv/tIHdw==}
+    engines: {node: '>=20'}
+
   '@emnapi/core@1.2.0':
     resolution: {integrity: sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==}
 
@@ -5336,6 +5347,12 @@ packages:
   '@types/color-name@1.1.1':
     resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==}
 
+  '@types/command-line-args@5.2.3':
+    resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==}
+
+  '@types/command-line-usage@5.0.4':
+    resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==}
+
   '@types/connect@3.4.38':
     resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
 
@@ -6149,6 +6166,10 @@ packages:
     resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==}
     engines: {node: '>= 8'}
 
+  apache-arrow@19.0.1:
+    resolution: {integrity: sha512-APmMLzS4qbTivLrPdQXexGM4JRr+0g62QDaobzEvip/FdQIrv2qLy0mD5Qdmw4buydtVJgbFeKR8f59I6PPGDg==}
+    hasBin: true
+
   app-root-path@3.1.0:
     resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==}
     engines: {node: '>= 6.0.0'}
@@ -6196,6 +6217,10 @@ packages:
     resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
     engines: {node: '>= 0.4'}
 
+  array-back@6.2.2:
+    resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==}
+    engines: {node: '>=12.17'}
+
   array-buffer-byte-length@1.0.1:
     resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
     engines: {node: '>= 0.4'}
@@ -6690,6 +6715,10 @@ packages:
   chainsaw@0.1.0:
     resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==}
 
+  chalk-template@0.4.0:
+    resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==}
+    engines: {node: '>=12'}
+
   chalk@1.1.3:
     resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
     engines: {node: '>=0.10.0'}
@@ -6954,6 +6983,19 @@ packages:
   comma-separated-tokens@2.0.2:
     resolution: {integrity: sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==}
 
+  command-line-args@6.0.1:
+    resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==}
+    engines: {node: '>=12.20'}
+    peerDependencies:
+      '@75lb/nature': latest
+    peerDependenciesMeta:
+      '@75lb/nature':
+        optional: true
+
+  command-line-usage@7.0.3:
+    resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==}
+    engines: {node: '>=12.20.0'}
+
   commander@10.0.1:
     resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
     engines: {node: '>=14'}
@@ -8782,6 +8824,15 @@ packages:
     resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
     engines: {node: '>=8'}
 
+  find-replace@5.0.2:
+    resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@75lb/nature': latest
+    peerDependenciesMeta:
+      '@75lb/nature':
+        optional: true
+
   find-up-simple@1.0.0:
     resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==}
     engines: {node: '>=18'}
@@ -8817,6 +8868,9 @@ packages:
     resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
     hasBin: true
 
+  flatbuffers@24.12.23:
+    resolution: {integrity: sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==}
+
   flatted@3.3.1:
     resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
 
@@ -10132,6 +10186,10 @@ packages:
   json-bigint@1.0.0:
     resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
 
+  json-bignum@0.0.3:
+    resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==}
+    engines: {node: '>=0.8'}
+
   json-buffer@3.0.1:
     resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
 
@@ -12951,6 +13009,12 @@ packages:
   secure-json-parse@2.7.0:
     resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
 
+  secure-json-parse@3.0.2:
+    resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==}
+
+  secure-json-parse@4.0.0:
+    resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==}
+
   semver@5.5.1:
     resolution: {integrity: sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==}
     hasBin: true
@@ -13648,6 +13712,10 @@ packages:
     resolution: {integrity: sha512-CSZRtSRZ8RhJGMtWyLRqlarmWPPlsgZJHtV6cz0VTHNOg+R7UBoE2eNPQmB5Qrhtk3RX2AAcJmVwMXFULVQSwg==}
     engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
 
+  table-layout@4.1.1:
+    resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==}
+    engines: {node: '>=12.17'}
+
   table@6.8.2:
     resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==}
     engines: {node: '>=10.0.0'}
@@ -14141,6 +14209,10 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  typical@7.3.0:
+    resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==}
+    engines: {node: '>=12.17'}
+
   typpy@2.3.11:
     resolution: {integrity: sha512-Jh/fykZSaxeKO0ceMAs6agki9T5TNA9kiIR6fzKbvafKpIw8UlNlHhzuqKyi5lfJJ5VojJOx9tooIbyy7vHV/g==}
 
@@ -14194,10 +14266,14 @@ packages:
   undici-types@6.21.0:
     resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
 
-  undici@6.19.2:
-    resolution: {integrity: sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==}
+  undici@6.21.3:
+    resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
     engines: {node: '>=18.17'}
 
+  undici@7.10.0:
+    resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==}
+    engines: {node: '>=20.18.1'}
+
   unicorn-magic@0.1.0:
     resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
     engines: {node: '>=18'}
@@ -14700,6 +14776,10 @@ packages:
   wordwrap@1.0.0:
     resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
 
+  wordwrapjs@5.1.0:
+    resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==}
+    engines: {node: '>=12.17'}
+
   wrap-ansi@6.2.0:
     resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
     engines: {node: '>=8'}
@@ -16833,21 +16913,45 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@elastic/elasticsearch@8.14.0':
+  '@elastic/elasticsearch@8.18.2':
+    dependencies:
+      '@elastic/transport': 8.9.7
+      apache-arrow: 19.0.1
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - '@75lb/nature'
+      - supports-color
+
+  '@elastic/elasticsearch@9.0.3':
+    dependencies:
+      '@elastic/transport': 9.0.2
+      apache-arrow: 19.0.1
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - '@75lb/nature'
+      - supports-color
+
+  '@elastic/transport@8.9.7':
     dependencies:
-      '@elastic/transport': 8.6.1
+      '@opentelemetry/api': 1.9.0
+      debug: 4.4.1(supports-color@5.5.0)
+      hpagent: 1.2.0
+      ms: 2.1.3
+      secure-json-parse: 3.0.2
       tslib: 2.8.1
+      undici: 6.21.3
     transitivePeerDependencies:
       - supports-color
 
-  '@elastic/transport@8.6.1':
+  '@elastic/transport@9.0.2':
     dependencies:
+      '@opentelemetry/api': 1.9.0
       debug: 4.4.1(supports-color@5.5.0)
       hpagent: 1.2.0
       ms: 2.1.3
-      secure-json-parse: 2.7.0
+      secure-json-parse: 4.0.0
       tslib: 2.8.1
-      undici: 6.19.2
+      undici: 7.10.0
     transitivePeerDependencies:
       - supports-color
 
@@ -20430,6 +20534,10 @@ snapshots:
 
   '@types/color-name@1.1.1': {}
 
+  '@types/command-line-args@5.2.3': {}
+
+  '@types/command-line-usage@5.0.4': {}
+
   '@types/connect@3.4.38':
     dependencies:
       '@types/node': 22.15.21
@@ -21491,6 +21599,20 @@ snapshots:
       normalize-path: 3.0.0
       picomatch: 2.3.1
 
+  apache-arrow@19.0.1:
+    dependencies:
+      '@swc/helpers': 0.5.15
+      '@types/command-line-args': 5.2.3
+      '@types/command-line-usage': 5.0.4
+      '@types/node': 20.14.0
+      command-line-args: 6.0.1
+      command-line-usage: 7.0.3
+      flatbuffers: 24.12.23
+      json-bignum: 0.0.3
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - '@75lb/nature'
+
   app-root-path@3.1.0: {}
 
   append-field@1.0.0: {}
@@ -21557,6 +21679,8 @@ snapshots:
 
   aria-query@5.3.1: {}
 
+  array-back@6.2.2: {}
+
   array-buffer-byte-length@1.0.1:
     dependencies:
       call-bind: 1.0.7
@@ -22217,6 +22341,10 @@ snapshots:
     dependencies:
       traverse: 0.3.9
 
+  chalk-template@0.4.0:
+    dependencies:
+      chalk: 4.1.2
+
   chalk@1.1.3:
     dependencies:
       ansi-styles: 2.2.1
@@ -22500,6 +22628,20 @@ snapshots:
 
   comma-separated-tokens@2.0.2: {}
 
+  command-line-args@6.0.1:
+    dependencies:
+      array-back: 6.2.2
+      find-replace: 5.0.2
+      lodash.camelcase: 4.3.0
+      typical: 7.3.0
+
+  command-line-usage@7.0.3:
+    dependencies:
+      array-back: 6.2.2
+      chalk-template: 0.4.0
+      table-layout: 4.1.1
+      typical: 7.3.0
+
   commander@10.0.1:
     optional: true
 
@@ -24317,6 +24459,8 @@ snapshots:
       make-dir: 3.1.0
       pkg-dir: 4.2.0
 
+  find-replace@5.0.2: {}
+
   find-up-simple@1.0.0: {}
 
   find-up@1.1.2:
@@ -24355,6 +24499,8 @@ snapshots:
 
   flat@5.0.2: {}
 
+  flatbuffers@24.12.23: {}
+
   flatted@3.3.1: {}
 
   fn-args@5.0.0: {}
@@ -26010,6 +26156,8 @@ snapshots:
     dependencies:
       bignumber.js: 9.1.2
 
+  json-bignum@0.0.3: {}
+
   json-buffer@3.0.1: {}
 
   json-parse-better-errors@1.0.1: {}
@@ -29524,6 +29672,10 @@ snapshots:
 
   secure-json-parse@2.7.0: {}
 
+  secure-json-parse@3.0.2: {}
+
+  secure-json-parse@4.0.0: {}
+
   semver@5.5.1: {}
 
   semver@5.7.1: {}
@@ -30409,6 +30561,11 @@ snapshots:
       '@pkgr/utils': 2.3.0
       tslib: 2.8.1
 
+  table-layout@4.1.1:
+    dependencies:
+      array-back: 6.2.2
+      wordwrapjs: 5.1.0
+
   table@6.8.2:
     dependencies:
       ajv: 8.17.1
@@ -30901,6 +31058,8 @@ snapshots:
 
   typescript@5.4.2: {}
 
+  typical@7.3.0: {}
+
   typpy@2.3.11:
     dependencies:
       function.name: 1.0.12
@@ -30950,7 +31109,9 @@ snapshots:
 
   undici-types@6.21.0: {}
 
-  undici@6.19.2: {}
+  undici@6.21.3: {}
+
+  undici@7.10.0: {}
 
   unicorn-magic@0.1.0: {}
 
@@ -31547,6 +31708,8 @@ snapshots:
 
   wordwrap@1.0.0: {}
 
+  wordwrapjs@5.1.0: {}
+
   wrap-ansi@6.2.0:
     dependencies:
       ansi-styles: 4.2.1