@h-ai/vecdb 0.1.0-alpha.66 → 0.1.0-alpha.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { core, ok, err } from '@h-ai/core';
2
+ import { core, err, ok } from '@h-ai/core';
3
+ import process from 'process';
3
4
  import { spawn } from 'child_process';
4
- import { setTimeout } from 'timers/promises';
5
+ import { setTimeout as setTimeout$1 } from 'timers/promises';
5
6
  import { createRequire } from 'module';
6
7
  import { createHash } from 'crypto';
7
8
 
@@ -20,6 +21,11 @@ var en_US_default = {
20
21
  vecdb_connectionFailed: "Failed to connect to vector database: {error}",
21
22
  vecdb_collectionNotFound: "Collection not found: {name}",
22
23
  vecdb_collectionAlreadyExists: "Collection already exists: {name}",
24
+ vecdb_invalidCollectionSchema: "Collection has no valid persisted vector dimension or distance metric: {name}",
25
+ vecdb_atomicUpsertUnavailable: "LanceDB collection or driver lacks safe atomic merge primary-key support; no writes were performed. Rebuild the collection or use a supported driver",
26
+ vecdb_duplicateDocumentIds: "Vector document IDs must be unique within a batch; no writes were performed",
27
+ vecdb_jsonMetadataRequired: "LanceDB collection requires native JSON metadata; rebuild legacy string metadata collections before writing or filtering",
28
+ vecdb_invalidMetadataFilter: "LanceDB metadata filter must be a plain object of top-level string, finite number, boolean or null values; nested objects, arrays and undefined are unsupported",
23
29
  vecdb_dimensionMismatch: "Vector dimension mismatch: expected {expected}, actual {actual}",
24
30
  vecdb_insertFailed: "Failed to insert vectors: {error}",
25
31
  vecdb_deleteFailed: "Failed to delete vectors: {error}",
@@ -29,7 +35,43 @@ var en_US_default = {
29
35
  vecdb_serializationFailed: "Serialization/deserialization failed: {error}",
30
36
  vecdb_invalidDimension: "Invalid vector dimension: {dimension}, must be a positive integer",
31
37
  vecdb_configPathRequired: "Database storage path is required",
32
- vecdb_configDatabaseRequired: "Database name is required"
38
+ vecdb_configDatabaseRequired: "Database name is required",
39
+ vecdb_sharedBackendRequired: "Production mode requires a shared backend; local files and memory backends are for development or explicit end-to-end tests.",
40
+ vecdb_chromaEndpointInvalid: "Chroma requires an HTTP(S) root endpoint without credentials, path prefix, query or fragment",
41
+ vecdb_chromaTokenInvalid: "Chroma Bearer token must contain only printable ASCII characters without whitespace"
42
+ };
43
+
44
+ // messages/ja-JP.json
45
+ var ja_JP_default = {
46
+ $schema: "https://inlang.com/schema/inlang-message-format",
47
+ vecdb_notInitialized: "\u30D9\u30AF\u30C8\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306Bvecdb.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
48
+ vecdb_initFailed: "\u30D9\u30AF\u30C8\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
49
+ vecdb_closeFailed: "\u30D9\u30AF\u30C8\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u7D42\u4E86\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
50
+ vecdb_configError: "\u30D9\u30AF\u30C8\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u8A2D\u5B9A\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
51
+ vecdb_initInProgress: "\u30D9\u30AF\u30C8\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u521D\u671F\u5316\u306F\u65E2\u306B\u5B9F\u884C\u4E2D\u3067\u3059",
52
+ vecdb_unsupportedType: "\u672A\u5BFE\u5FDC\u306E\u30D9\u30AF\u30C8\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u7A2E\u985E\u3067\u3059: {type}",
53
+ vecdb_driverNotFound: "\u30D9\u30AF\u30C8\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u30C9\u30E9\u30A4\u30D0\u30FC\u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u3066\u3044\u307E\u305B\u3093: {driver}\u3002pnpm add {driver}\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
54
+ vecdb_connectionFailed: "\u30D9\u30AF\u30C8\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u3078\u306E\u63A5\u7D9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
55
+ vecdb_collectionNotFound: "\u30B3\u30EC\u30AF\u30B7\u30E7\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: {name}",
56
+ vecdb_collectionAlreadyExists: "\u30B3\u30EC\u30AF\u30B7\u30E7\u30F3\u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059: {name}",
57
+ vecdb_invalidCollectionSchema: "\u30B3\u30EC\u30AF\u30B7\u30E7\u30F3\u306B\u6709\u52B9\u306A\u6C38\u7D9A\u5316\u6E08\u307F\u306E\u30D9\u30AF\u30C8\u30EB\u6B21\u5143\u307E\u305F\u306F\u8DDD\u96E2\u30E1\u30C8\u30EA\u30C3\u30AF\u304C\u3042\u308A\u307E\u305B\u3093\uFF1A{name}",
58
+ vecdb_atomicUpsertUnavailable: "LanceDB \u30B3\u30EC\u30AF\u30B7\u30E7\u30F3\u307E\u305F\u306F\u30C9\u30E9\u30A4\u30D0\u30FC\u306B\u5B89\u5168\u306A\u4E3B\u30AD\u30FC\u306B\u3088\u308B\u30A2\u30C8\u30DF\u30C3\u30AF\u306A merge \u6A5F\u80FD\u304C\u3042\u308A\u307E\u305B\u3093\u3002\u66F8\u304D\u8FBC\u307F\u306F\u884C\u3063\u3066\u3044\u307E\u305B\u3093\u3002\u30B3\u30EC\u30AF\u30B7\u30E7\u30F3\u3092\u518D\u4F5C\u6210\u3059\u308B\u304B\u3001\u5BFE\u5FDC\u30C9\u30E9\u30A4\u30D0\u30FC\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044",
59
+ vecdb_duplicateDocumentIds: "\u540C\u4E00\u30D0\u30C3\u30C1\u306E\u30D9\u30AF\u30C8\u30EB\u6587\u66F8 ID \u306F\u4E00\u610F\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002\u66F8\u304D\u8FBC\u307F\u306F\u884C\u3063\u3066\u3044\u307E\u305B\u3093",
60
+ vecdb_jsonMetadataRequired: "LanceDB \u30B3\u30EC\u30AF\u30B7\u30E7\u30F3\u306B\u306F\u30CD\u30A4\u30C6\u30A3\u30D6 JSON \u30E1\u30BF\u30C7\u30FC\u30BF\u304C\u5FC5\u8981\u3067\u3059\u3002\u65E7\u5F62\u5F0F\u306E\u6587\u5B57\u5217\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u4F7F\u7528\u3059\u308B\u30B3\u30EC\u30AF\u30B7\u30E7\u30F3\u306F\u3001\u66F8\u304D\u8FBC\u307F\u3084\u30D5\u30A3\u30EB\u30BF\u30FC\u51E6\u7406\u306E\u524D\u306B\u518D\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044",
61
+ vecdb_invalidMetadataFilter: "LanceDB \u30E1\u30BF\u30C7\u30FC\u30BF\u30D5\u30A3\u30EB\u30BF\u30FC\u306F\u901A\u5E38\u306E\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3068\u3057\u3001\u6700\u4E0A\u4F4D\u306E\u5024\u306B\u306F\u6587\u5B57\u5217\u3001\u6709\u9650\u6570\u3001\u771F\u507D\u5024\u3001null \u306E\u307F\u6307\u5B9A\u3067\u304D\u307E\u3059\u3002\u5165\u308C\u5B50\u306E\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3001\u914D\u5217\u3001undefined \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093",
62
+ vecdb_dimensionMismatch: "\u30D9\u30AF\u30C8\u30EB\u306E\u6B21\u5143\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: \u671F\u5F85\u5024{expected}\u3001\u5B9F\u969B\u306E\u5024{actual}",
63
+ vecdb_insertFailed: "\u30D9\u30AF\u30C8\u30EB\u306E\u633F\u5165\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
64
+ vecdb_deleteFailed: "\u30D9\u30AF\u30C8\u30EB\u306E\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
65
+ vecdb_updateFailed: "\u30D9\u30AF\u30C8\u30EB\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
66
+ vecdb_queryFailed: "\u30D9\u30AF\u30C8\u30EB\u306E\u691C\u7D22\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
67
+ vecdb_indexBuildFailed: "\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u306E\u69CB\u7BC9\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
68
+ vecdb_serializationFailed: "\u30B7\u30EA\u30A2\u30E9\u30A4\u30BA\u307E\u305F\u306F\u30C7\u30B7\u30EA\u30A2\u30E9\u30A4\u30BA\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
69
+ vecdb_invalidDimension: "\u30D9\u30AF\u30C8\u30EB\u306E\u6B21\u5143\u304C\u7121\u52B9\u3067\u3059: {dimension}\u3002\u6B63\u306E\u6574\u6570\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
70
+ vecdb_configPathRequired: "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u4FDD\u5B58\u30D1\u30B9\u306F\u5FC5\u9808\u3067\u3059",
71
+ vecdb_configDatabaseRequired: "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u540D\u306F\u5FC5\u9808\u3067\u3059",
72
+ vecdb_sharedBackendRequired: "\u672C\u756A\u74B0\u5883\u306B\u306F\u5171\u6709\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u304C\u5FC5\u8981\u3067\u3059\u3002\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB\u3068\u30E1\u30E2\u30EA\u30FC\u30D0\u30C3\u30AF\u30A8\u30F3\u30C9\u306F\u958B\u767A\u74B0\u5883\u307E\u305F\u306F\u660E\u793A\u7684\u306A E2E \u30C6\u30B9\u30C8\u3067\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059\u3002",
73
+ vecdb_chromaEndpointInvalid: "Chroma \u306B\u306F HTTP(S) \u306E\u30EB\u30FC\u30C8\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u8A8D\u8A3C\u60C5\u5831\u3001\u30D1\u30B9\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9\u3001\u30AF\u30A8\u30EA\u3001\u30D5\u30E9\u30B0\u30E1\u30F3\u30C8\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093",
74
+ vecdb_chromaTokenInvalid: "Chroma Bearer \u30C8\u30FC\u30AF\u30F3\u306B\u306F\u7A7A\u767D\u3092\u542B\u307E\u306A\u3044\u8868\u793A\u53EF\u80FD\u306A ASCII \u6587\u5B57\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059"
33
75
  };
34
76
 
35
77
  // messages/zh-CN.json
@@ -45,6 +87,11 @@ var zh_CN_default = {
45
87
  vecdb_connectionFailed: "\u8FDE\u63A5\u5411\u91CF\u6570\u636E\u5E93\u5931\u8D25: {error}",
46
88
  vecdb_collectionNotFound: "\u96C6\u5408\u4E0D\u5B58\u5728: {name}",
47
89
  vecdb_collectionAlreadyExists: "\u96C6\u5408\u5DF2\u5B58\u5728: {name}",
90
+ vecdb_invalidCollectionSchema: "\u96C6\u5408\u7F3A\u5C11\u6709\u6548\u7684\u6301\u4E45\u5316\u5411\u91CF\u7EF4\u5EA6\u6216\u8DDD\u79BB\u5EA6\u91CF\u5B9A\u4E49: {name}",
91
+ vecdb_atomicUpsertUnavailable: "LanceDB \u96C6\u5408\u6216\u9A71\u52A8\u7F3A\u5C11\u5B89\u5168\u7684\u539F\u5B50 merge \u4E3B\u952E\u80FD\u529B\uFF0C\u672A\u6267\u884C\u5199\u5165\uFF0C\u8BF7\u91CD\u5EFA\u96C6\u5408\u6216\u4F7F\u7528\u53D7\u652F\u6301\u7684\u9A71\u52A8",
92
+ vecdb_duplicateDocumentIds: "\u540C\u4E00\u6279\u5411\u91CF\u6587\u6863\u7684 ID \u4E0D\u5F97\u91CD\u590D\uFF0C\u672A\u6267\u884C\u5199\u5165",
93
+ vecdb_jsonMetadataRequired: "LanceDB \u96C6\u5408\u987B\u4F7F\u7528\u539F\u751F JSON \u5143\u6570\u636E\uFF0C\u65E7\u5B57\u7B26\u4E32\u5143\u6570\u636E\u96C6\u5408\u9700\u91CD\u5EFA\u540E\u624D\u80FD\u5199\u5165\u6216\u8FC7\u6EE4",
94
+ vecdb_invalidMetadataFilter: "LanceDB \u5143\u6570\u636E\u8FC7\u6EE4\u987B\u4E3A\u666E\u901A\u5BF9\u8C61\uFF0C\u503C\u4EC5\u652F\u6301\u9876\u5C42\u5B57\u7B26\u4E32\u3001\u6709\u9650\u6570\u503C\u3001\u5E03\u5C14\u6216 null\uFF1B\u4E0D\u652F\u6301\u5D4C\u5957\u5BF9\u8C61\u3001\u6570\u7EC4\u548C undefined",
48
95
  vecdb_dimensionMismatch: "\u5411\u91CF\u7EF4\u5EA6\u4E0D\u5339\u914D: \u671F\u671B {expected}\uFF0C\u5B9E\u9645 {actual}",
49
96
  vecdb_insertFailed: "\u63D2\u5165\u5411\u91CF\u5931\u8D25: {error}",
50
97
  vecdb_deleteFailed: "\u5220\u9664\u5411\u91CF\u5931\u8D25: {error}",
@@ -54,13 +101,17 @@ var zh_CN_default = {
54
101
  vecdb_serializationFailed: "\u5E8F\u5217\u5316/\u53CD\u5E8F\u5217\u5316\u5931\u8D25: {error}",
55
102
  vecdb_invalidDimension: "\u65E0\u6548\u7684\u5411\u91CF\u7EF4\u5EA6: {dimension}\uFF0C\u5FC5\u987B\u4E3A\u6B63\u6574\u6570",
56
103
  vecdb_configPathRequired: "\u6570\u636E\u5E93\u5B58\u50A8\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A",
57
- vecdb_configDatabaseRequired: "\u6570\u636E\u5E93\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A"
104
+ vecdb_configDatabaseRequired: "\u6570\u636E\u5E93\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A",
105
+ vecdb_sharedBackendRequired: "\u751F\u4EA7\u6A21\u5F0F\u9700\u8981\u8DE8\u8282\u70B9\u5171\u4EAB\u540E\u7AEF\uFF1B\u672C\u673A\u6587\u4EF6\u6216\u5185\u5B58\u540E\u7AEF\u4EC5\u7528\u4E8E\u5F00\u53D1\u548C\u663E\u5F0F\u7AEF\u5230\u7AEF\u6D4B\u8BD5\u3002",
106
+ vecdb_chromaEndpointInvalid: "Chroma \u4EC5\u652F\u6301 HTTP(S) \u6839\u7AEF\u70B9\uFF0C\u4E0D\u5F97\u5305\u542B\u51ED\u636E\u3001\u8DEF\u5F84\u524D\u7F00\u3001query \u6216 fragment",
107
+ vecdb_chromaTokenInvalid: "Chroma Bearer token \u4EC5\u652F\u6301\u65E0\u7A7A\u767D\u7684\u53EF\u6253\u5370 ASCII \u5B57\u7B26"
58
108
  };
59
109
 
60
110
  // src/vecdb-i18n.ts
61
111
  var vecdbM = core.i18n.createMessageGetter({
62
112
  "zh-CN": zh_CN_default,
63
- "en-US": en_US_default
113
+ "en-US": en_US_default,
114
+ "ja-JP": ja_JP_default
64
115
  });
65
116
 
66
117
  // src/vecdb-config.ts
@@ -76,9 +127,9 @@ var OperationLogConfigFieldSchema = {
76
127
  };
77
128
  var LancedbConfigSchema = z.object({
78
129
  type: z.literal("lancedb"),
79
- /** 数据库存储路径(本地目录) */
130
+ /** 持久化本地目录;独立节点的同名路径不会自动共享,分布式部署优先使用服务端后端。 */
80
131
  path: z.string().min(1, vecdbM("vecdb_configPathRequired")),
81
- /** 距离度量(默认 cosine) */
132
+ /** 新建集合的默认距离度量;已有集合使用 Arrow schema 中的持久定义。 */
82
133
  metric: DistanceMetricSchema.optional(),
83
134
  ...OperationLogConfigFieldSchema
84
135
  });
@@ -119,14 +170,21 @@ var ChromaConfigSchema = z.object({
119
170
  type: z.literal("chroma"),
120
171
  /** 本地持久化路径(嵌入式模式:提供且无 url 时自动拉起本地服务) */
121
172
  path: z.string().optional(),
122
- /** 已有 Chroma 服务 URL(提供时直连,不自动拉起进程) */
123
- url: z.string().url().optional(),
173
+ /** Chroma HTTP(S) 根端点;不可包含凭据、路径前缀、query 或 fragment;提供时优先于 host/port。 */
174
+ url: z.string().url().refine((value) => {
175
+ try {
176
+ const endpoint = new URL(value);
177
+ return ["http:", "https:"].includes(endpoint.protocol) && !endpoint.username && !endpoint.password && endpoint.pathname === "/" && !endpoint.search && !endpoint.hash;
178
+ } catch {
179
+ return false;
180
+ }
181
+ }, { error: () => vecdbM("vecdb_chromaEndpointInvalid") }).optional(),
124
182
  /** 主机(默认 localhost) */
125
183
  host: z.string().default("localhost"),
126
184
  /** 端口(默认 8000) */
127
185
  port: z.number().int().min(1).max(65535).default(8e3),
128
- /** API Key(可选,连接受保护的 Chroma 服务时使用) */
129
- apiKey: z.string().optional(),
186
+ /** 可选 Bearer token,发送 Authorization 请求头;用于采用此协议的自托管服务/网关,不是 Chroma Cloud 配置。空串表示未配置。 */
187
+ apiKey: z.string().regex(/^[\x21-\x7E]*$/, { error: () => vecdbM("vecdb_chromaTokenInvalid") }).optional(),
130
188
  /** 拉起本地服务的可执行命令(默认 'chroma',可指定绝对路径) */
131
189
  serverCommand: z.string().default("chroma"),
132
190
  /** 拉起本地服务的就绪等待超时(毫秒,默认 30000) */
@@ -365,14 +423,15 @@ function buildWhere(filter) {
365
423
  const entries = Object.entries(filter);
366
424
  if (entries.length === 0)
367
425
  return void 0;
368
- if (entries.length === 1)
369
- return { [entries[0][0]]: entries[0][1] };
370
- return { $and: entries.map(([key, value]) => ({ [key]: value })) };
426
+ const where = entries.length === 1 ? { [entries[0][0]]: entries[0][1] } : { $and: entries.map(([key, value]) => ({ [key]: value })) };
427
+ return where;
371
428
  }
372
429
  function createChromaProvider() {
373
430
  let client = null;
374
431
  let config = null;
375
432
  let serverProcess = null;
433
+ let spawnFailed = false;
434
+ let closing = null;
376
435
  function isMissingModuleError(error) {
377
436
  const message = error instanceof Error ? error.message : String(error);
378
437
  return message.includes("chromadb");
@@ -394,24 +453,93 @@ function createChromaProvider() {
394
453
  throw error;
395
454
  }
396
455
  }
397
- function startEmbeddedServer(chromaConfig) {
456
+ async function startEmbeddedServer(chromaConfig) {
398
457
  const args = ["run", "--path", chromaConfig.path ?? "./data/chroma", "--host", chromaConfig.host, "--port", String(chromaConfig.port)];
399
458
  logger.info("Starting embedded Chroma server", { command: chromaConfig.serverCommand, host: chromaConfig.host, port: chromaConfig.port });
459
+ spawnFailed = false;
400
460
  const proc = spawn(chromaConfig.serverCommand, args, { stdio: "ignore" });
401
461
  proc.on("error", (error) => {
462
+ spawnFailed = proc.pid === void 0;
402
463
  logger.error("Chroma server process error", { error: error.message });
403
464
  });
404
465
  serverProcess = proc;
466
+ await new Promise((resolve, reject) => {
467
+ proc.once("spawn", resolve);
468
+ proc.once("error", reject);
469
+ });
405
470
  }
406
- function stopEmbeddedServer() {
407
- if (!serverProcess)
471
+ async function stopEmbeddedServer() {
472
+ const owned = serverProcess;
473
+ if (!owned)
408
474
  return;
409
- try {
410
- serverProcess.kill();
411
- } catch (error) {
412
- logger.warn("Failed to stop Chroma server", { error: error instanceof Error ? error.message : String(error) });
475
+ const hasExited = () => owned.exitCode !== null || owned.signalCode !== null || spawnFailed && owned.pid === void 0;
476
+ if (!hasExited()) {
477
+ const child = owned;
478
+ await new Promise((resolve, reject) => {
479
+ let timer;
480
+ let settled = false;
481
+ function cleanup() {
482
+ clearTimeout(timer);
483
+ child.removeListener("exit", exited);
484
+ child.removeListener("error", failed);
485
+ }
486
+ function exited() {
487
+ if (settled)
488
+ return;
489
+ settled = true;
490
+ cleanup();
491
+ resolve();
492
+ }
493
+ function failed(cause) {
494
+ if (settled)
495
+ return;
496
+ if (hasExited()) {
497
+ exited();
498
+ return;
499
+ }
500
+ settled = true;
501
+ cleanup();
502
+ reject(cause);
503
+ }
504
+ function signal(name) {
505
+ try {
506
+ if (!child.kill(name))
507
+ failed(new Error("Chroma child termination signal was rejected"));
508
+ } catch (cause) {
509
+ failed(cause instanceof Error ? cause : new Error(String(cause)));
510
+ }
511
+ }
512
+ child.once("exit", exited);
513
+ child.once("error", failed);
514
+ timer = setTimeout(() => {
515
+ timer = setTimeout(() => failed(new Error("Chroma child exit was not confirmed")), 1500);
516
+ signal("SIGKILL");
517
+ }, 1500);
518
+ if (hasExited())
519
+ exited();
520
+ else signal("SIGTERM");
521
+ });
413
522
  }
414
- serverProcess = null;
523
+ if (serverProcess === owned)
524
+ serverProcess = null;
525
+ }
526
+ async function closeResources() {
527
+ if (!closing) {
528
+ client = null;
529
+ config = null;
530
+ closing = (async () => {
531
+ try {
532
+ await stopEmbeddedServer();
533
+ logger.info("Chroma connection closed");
534
+ return ok(void 0);
535
+ } catch (cause) {
536
+ return err({ ...HaiVecdbError.CONNECTION_FAILED, message: vecdbM("vecdb_closeFailed", { params: { error: cause instanceof Error ? cause.message : String(cause) } }), cause, ext: { cleanupPending: true } });
537
+ }
538
+ })().finally(() => {
539
+ closing = null;
540
+ });
541
+ }
542
+ return await closing;
415
543
  }
416
544
  async function waitForHeartbeat(chromaClient, timeout) {
417
545
  const deadline = Date.now() + timeout;
@@ -422,7 +550,7 @@ function createChromaProvider() {
422
550
  return;
423
551
  } catch (error) {
424
552
  lastError = error;
425
- await setTimeout(HEARTBEAT_POLL_INTERVAL);
553
+ await setTimeout$1(HEARTBEAT_POLL_INTERVAL);
426
554
  }
427
555
  }
428
556
  throw new Error(`Chroma heartbeat timed out after ${timeout}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
@@ -447,7 +575,7 @@ function createChromaProvider() {
447
575
  async create(name, options) {
448
576
  logger.debug("Creating collection", { name, dimension: options.dimension, metric: options.metric });
449
577
  const existing = await client.listCollections();
450
- const exists = existing.some((item) => (typeof item === "string" ? item : item.name) === name);
578
+ const exists = existing.some((item) => item.name === name);
451
579
  if (exists)
452
580
  return err(HaiVecdbError.COLLECTION_ALREADY_EXISTS, vecdbM("vecdb_collectionAlreadyExists", { params: { name } }));
453
581
  const metric = options.metric ?? config?.metric ?? "cosine";
@@ -462,7 +590,7 @@ function createChromaProvider() {
462
590
  async drop(name) {
463
591
  logger.debug("Dropping collection", { name });
464
592
  const existing = await client.listCollections();
465
- const exists = existing.some((item) => (typeof item === "string" ? item : item.name) === name);
593
+ const exists = existing.some((item) => item.name === name);
466
594
  if (!exists)
467
595
  return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
468
596
  await client.deleteCollection({ name });
@@ -472,12 +600,12 @@ function createChromaProvider() {
472
600
  async exists(name) {
473
601
  logger.debug("Checking collection exists", { name });
474
602
  const existing = await client.listCollections();
475
- return ok(existing.some((item) => (typeof item === "string" ? item : item.name) === name));
603
+ return ok(existing.some((item) => item.name === name));
476
604
  },
477
605
  async info(name) {
478
606
  logger.debug("Getting collection info", { name });
479
607
  const existing = await client.listCollections();
480
- if (!existing.some((item) => (typeof item === "string" ? item : item.name) === name))
608
+ if (!existing.some((item) => item.name === name))
481
609
  return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
482
610
  const collection = await getCollection(name);
483
611
  const count = await collection.count();
@@ -491,14 +619,16 @@ function createChromaProvider() {
491
619
  async list() {
492
620
  logger.debug("Listing collections");
493
621
  const existing = await client.listCollections();
494
- return ok(existing.map((item) => typeof item === "string" ? item : item.name));
622
+ return ok(existing.map((item) => item.name));
495
623
  }
496
624
  };
497
625
  function toWriteParams(documents) {
498
626
  return {
499
627
  ids: documents.map((doc) => doc.id),
500
628
  embeddings: documents.map((doc) => doc.vector),
501
- documents: documents.map((doc) => doc.content ?? null),
629
+ documents: documents.map((doc) => doc.content ?? ""),
630
+ // 统一 API 的 metadata 更宽;SDK 在 add/upsert 中先执行 validateMetadata,再发送请求。
631
+ // 使用上游 Metadata 类型跨越此边界,避免复制其标量、数组及稀疏向量规则;原生拒绝会由 base 转为 HaiResult。
502
632
  metadatas: documents.map((doc) => ({ ...doc.metadata, _content: doc.content ?? "" }))
503
633
  };
504
634
  }
@@ -577,35 +707,49 @@ function createChromaProvider() {
577
707
  async connect(cfg) {
578
708
  if (cfg.type !== "chroma")
579
709
  return err(HaiVecdbError.UNSUPPORTED_TYPE, vecdbM("vecdb_unsupportedType", { params: { type: cfg.type } }));
710
+ if (serverProcess || client || closing) {
711
+ const cleaned = await closeResources();
712
+ if (!cleaned.success)
713
+ return cleaned;
714
+ }
580
715
  const chromaConfig = cfg;
581
716
  const embedded = !!chromaConfig.path && !chromaConfig.url;
582
717
  const loadResult = await loadChromaModule();
583
718
  if (!loadResult.success)
584
719
  return loadResult;
720
+ const startupAbort = new AbortController();
721
+ const startupTimeout = embedded ? chromaConfig.startupTimeout : 5e3;
722
+ const startupTimer = setTimeout(() => startupAbort.abort(), startupTimeout);
585
723
  try {
586
724
  if (embedded)
587
- startEmbeddedServer(chromaConfig);
588
- const clientParams = chromaConfig.url ? { path: chromaConfig.url } : { host: chromaConfig.host, port: chromaConfig.port };
725
+ await startEmbeddedServer(chromaConfig);
726
+ const endpoint = chromaConfig.url ? new URL(chromaConfig.url) : void 0;
727
+ const clientParams = endpoint ? { host: endpoint.hostname, port: Number(endpoint.port || (endpoint.protocol === "https:" ? 443 : 80)), ssl: endpoint.protocol === "https:" } : { host: chromaConfig.host, port: chromaConfig.port, ssl: false };
589
728
  if (chromaConfig.apiKey)
590
- clientParams.auth = { provider: "token", credentials: chromaConfig.apiKey, tokenHeaderType: "AUTHORIZATION" };
729
+ clientParams.headers = { Authorization: `Bearer ${chromaConfig.apiKey}` };
730
+ clientParams.fetchOptions = { signal: startupAbort.signal };
591
731
  const chromaClient = new loadResult.data.ChromaClient(clientParams);
592
- await waitForHeartbeat(chromaClient, embedded ? chromaConfig.startupTimeout : 5e3);
732
+ await waitForHeartbeat(chromaClient, startupTimeout);
733
+ await chromaClient.countCollections();
734
+ startupAbort.signal.throwIfAborted();
735
+ if (embedded && (spawnFailed || !serverProcess || serverProcess.exitCode !== null || serverProcess.signalCode !== null))
736
+ throw new Error("Chroma child exited before readiness was confirmed");
593
737
  client = chromaClient;
594
738
  config = chromaConfig;
595
- logger.info("Chroma connected", { embedded, host: chromaConfig.host, port: chromaConfig.port });
739
+ logger.info("Chroma connected", { embedded, host: clientParams.host, port: clientParams.port, ssl: clientParams.ssl });
596
740
  return ok(void 0);
597
741
  } catch (error) {
598
- stopEmbeddedServer();
742
+ startupAbort.abort();
743
+ const cleanup = await closeResources();
599
744
  logger.error("Failed to connect to Chroma", { error: error instanceof Error ? error.message : String(error) });
600
- return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_connectionFailed", { params: { error: String(error) } }), error);
745
+ const failure = err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_connectionFailed", { params: { error: String(error) } }), error);
746
+ return !failure.success && !cleanup.success ? err({ ...failure.error, ext: { cleanupPending: true, cleanupError: cleanup.error } }) : failure;
747
+ } finally {
748
+ clearTimeout(startupTimer);
601
749
  }
602
750
  },
603
751
  async close() {
604
- stopEmbeddedServer();
605
- client = null;
606
- config = null;
607
- logger.info("Chroma connection closed");
608
- return ok(void 0);
752
+ return await closeResources();
609
753
  },
610
754
  isConnected() {
611
755
  return client !== null;
@@ -616,16 +760,29 @@ function createChromaProvider() {
616
760
  }
617
761
  var require2 = createRequire(import.meta.url);
618
762
  var logger2 = core.logger.child({ module: "vecdb", scope: "lancedb" });
619
- function escapeLanceFilterValue(value) {
620
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "\\%").replace(/_/g, "\\_");
621
- }
763
+ var COLLECTION_METRIC_KEY = "hai.vecdb.metric";
764
+ var PRIMARY_KEY_FLAG = "lance-schema:unenforced-primary-key";
765
+ var PRIMARY_KEY_POSITION = "lance-schema:unenforced-primary-key:position";
622
766
  function escapeLanceSqlValue(value) {
623
767
  return value.replace(/'/g, "''");
624
768
  }
769
+ function buildMetadataPredicate(filter) {
770
+ if (typeof filter !== "object" || filter === null || Array.isArray(filter) || ![Object.prototype, null].includes(Object.getPrototypeOf(filter)) || Reflect.ownKeys(filter).some((key) => typeof key !== "string")) {
771
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_invalidMetadataFilter"));
772
+ }
773
+ const predicates = [];
774
+ for (const [key, value] of Object.entries(filter)) {
775
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value))) {
776
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_invalidMetadataFilter"));
777
+ }
778
+ const extracted = `json_extract(json_get(metadata, '${escapeLanceSqlValue(key)}'), '$')`;
779
+ predicates.push(typeof value === "number" ? `TRY_CAST(${extracted} AS DOUBLE) = CAST(${value} AS DOUBLE)` : `${extracted} = '${escapeLanceSqlValue(JSON.stringify(value))}'`);
780
+ }
781
+ return ok(predicates.join(" AND "));
782
+ }
625
783
  function createLancedbProvider() {
626
784
  let connection = null;
627
785
  let config = null;
628
- const collectionMetas = /* @__PURE__ */ new Map();
629
786
  function loadLancedb() {
630
787
  try {
631
788
  const mod = require2("@lancedb/lancedb");
@@ -635,22 +792,30 @@ function createLancedbProvider() {
635
792
  return err(HaiVecdbError.DRIVER_NOT_FOUND, vecdbM("vecdb_driverNotFound", { params: { driver: "@lancedb/lancedb" } }), error);
636
793
  }
637
794
  }
638
- async function openTable(name) {
639
- if (!connection)
640
- return null;
795
+ async function withCollection(name, action) {
796
+ if (!(await connection.tableNames()).includes(name))
797
+ return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
798
+ const table = await connection.openTable(name);
641
799
  try {
642
- const tableNames = await connection.tableNames();
643
- if (tableNames.includes(name)) {
644
- return await connection.openTable(name);
645
- }
646
- return null;
647
- } catch {
648
- return null;
800
+ const schema = await table.schema();
801
+ const vectorType = schema.fields.find((field) => field.name === "vector")?.type;
802
+ const metric = DistanceMetricSchema.safeParse(schema.metadata.get(COLLECTION_METRIC_KEY) ?? null);
803
+ if (!vectorType || !("listSize" in vectorType) || typeof vectorType.listSize !== "number" || !Number.isInteger(vectorType.listSize) || vectorType.listSize <= 0 || !metric.success) {
804
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_invalidCollectionSchema", { params: { name } }));
805
+ }
806
+ const primaryKeys = schema.fields.filter((field) => field.metadata.has(PRIMARY_KEY_POSITION) || ["true", "1", "yes"].includes(field.metadata.get(PRIMARY_KEY_FLAG)?.toLowerCase() ?? ""));
807
+ const idField = primaryKeys[0];
808
+ const atomicWrites = primaryKeys.length === 1 && idField.name === "id" && !idField.nullable && ["true", "1", "yes"].includes(idField.metadata.get(PRIMARY_KEY_FLAG)?.toLowerCase() ?? "") && idField.metadata.get(PRIMARY_KEY_POSITION) === "1" && typeof table.mergeInsert === "function";
809
+ const driver = loadLancedb();
810
+ if (!driver.success)
811
+ return driver;
812
+ const jsonMetadata = schema.fields.find((field) => field.name === "metadata")?.metadata.get("ARROW:extension:name") === "lance.json";
813
+ const inputSchema = jsonMetadata ? schema.assign([driver.data.makeJsonField("metadata", false)]) : void 0;
814
+ return await action(table, { dimension: vectorType.listSize, metric: metric.data, atomicWrites, inputSchema });
815
+ } finally {
816
+ table.close();
649
817
  }
650
818
  }
651
- function getCollectionDimension(name) {
652
- return collectionMetas.get(name)?.dimension ?? null;
653
- }
654
819
  function createDimensionMismatchResult(expected, actual) {
655
820
  return err(
656
821
  HaiVecdbError.DIMENSION_MISMATCH,
@@ -659,18 +824,13 @@ function createLancedbProvider() {
659
824
  })
660
825
  );
661
826
  }
662
- function validateVectorDimension(collection, vector) {
663
- const expected = getCollectionDimension(collection);
664
- if (expected === null || vector.length === expected) {
827
+ function validateVectorDimension(expected, vector) {
828
+ if (vector.length === expected) {
665
829
  return null;
666
830
  }
667
831
  return createDimensionMismatchResult(expected, vector.length);
668
832
  }
669
- function validateDocumentsDimension(collection, documents) {
670
- const expected = getCollectionDimension(collection);
671
- if (expected === null) {
672
- return null;
673
- }
833
+ function validateDocumentsDimension(expected, documents) {
674
834
  for (const document of documents) {
675
835
  if (document.vector.length !== expected) {
676
836
  return createDimensionMismatchResult(expected, document.vector.length);
@@ -678,6 +838,15 @@ function createLancedbProvider() {
678
838
  }
679
839
  return null;
680
840
  }
841
+ function validateAtomicWrite(meta, documents) {
842
+ if (!meta.inputSchema)
843
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_jsonMetadataRequired"));
844
+ if (!meta.atomicWrites)
845
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_atomicUpsertUnavailable"));
846
+ if (new Set(documents.map((document) => document.id)).size !== documents.length)
847
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_duplicateDocumentIds"));
848
+ return null;
849
+ }
681
850
  const ctx = { isConnected: () => connection !== null, logger: logger2, operationLog: () => config?.operationLog };
682
851
  const collectionDriver = {
683
852
  async create(name, options) {
@@ -687,16 +856,22 @@ function createLancedbProvider() {
687
856
  return err(HaiVecdbError.COLLECTION_ALREADY_EXISTS, vecdbM("vecdb_collectionAlreadyExists", { params: { name } }));
688
857
  }
689
858
  const dimension = options.dimension;
690
- const metric = options.metric ?? config?.metric ?? "cosine";
691
- const initRecord = {
692
- id: "__init__",
859
+ const parsedMetric = DistanceMetricSchema.safeParse(options.metric ?? config?.metric);
860
+ if (!parsedMetric.success)
861
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_invalidCollectionSchema", { params: { name } }));
862
+ const metric = parsedMetric.data;
863
+ const driver = loadLancedb();
864
+ if (!driver.success)
865
+ return driver;
866
+ const inferred = driver.data.makeArrowTable([{
867
+ id: "",
693
868
  vector: Array.from({ length: dimension }).fill(0),
694
869
  content: "",
695
870
  metadata: "{}"
696
- };
697
- const table = await connection.createTable(name, [initRecord]);
698
- await table.delete("id = '__init__'");
699
- collectionMetas.set(name, { dimension, metric });
871
+ }], {}, /* @__PURE__ */ new Map([[COLLECTION_METRIC_KEY, metric]])).schema;
872
+ const schema = inferred.assign(inferred.fields.map((field) => field.name === "id" ? field.clone({ nullable: false, metadata: new Map([...field.metadata, [PRIMARY_KEY_FLAG, "true"], [PRIMARY_KEY_POSITION, "1"]]) }) : field.name === "metadata" ? driver.data.makeJsonField("metadata", false) : field));
873
+ const table = await connection.createEmptyTable(name, schema);
874
+ table.close();
700
875
  logger2.info("Collection created", { name, dimension, metric });
701
876
  return ok(void 0);
702
877
  },
@@ -707,7 +882,6 @@ function createLancedbProvider() {
707
882
  return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
708
883
  }
709
884
  await connection.dropTable(name);
710
- collectionMetas.delete(name);
711
885
  logger2.info("Collection dropped", { name });
712
886
  return ok(void 0);
713
887
  },
@@ -718,18 +892,7 @@ function createLancedbProvider() {
718
892
  },
719
893
  async info(name) {
720
894
  logger2.debug("Getting collection info", { name });
721
- const table = await openTable(name);
722
- if (!table) {
723
- return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
724
- }
725
- const count = await table.countRows();
726
- const meta = collectionMetas.get(name);
727
- return ok({
728
- name,
729
- dimension: meta?.dimension ?? 0,
730
- metric: meta?.metric ?? config?.metric ?? "cosine",
731
- count
732
- });
895
+ return withCollection(name, async (table, meta) => ok({ name, dimension: meta.dimension, metric: meta.metric, count: await table.countRows() }));
733
896
  },
734
897
  async list() {
735
898
  logger2.debug("Listing collections");
@@ -740,105 +903,100 @@ function createLancedbProvider() {
740
903
  const vectorDriver = {
741
904
  async insert(collection, documents) {
742
905
  logger2.debug("Inserting vectors", { collection, count: documents.length });
743
- const table = await openTable(collection);
744
- if (!table) {
745
- return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
746
- }
747
- const dimensionValidation = validateDocumentsDimension(collection, documents);
748
- if (dimensionValidation) {
749
- return dimensionValidation;
750
- }
751
- const records = documents.map((doc) => ({
752
- id: doc.id,
753
- vector: doc.vector,
754
- content: doc.content ?? "",
755
- metadata: JSON.stringify(doc.metadata ?? {})
756
- }));
757
- await table.add(records);
758
- logger2.info("Vectors inserted", { collection, count: documents.length });
759
- return ok(void 0);
906
+ return withCollection(collection, async (table, meta) => {
907
+ const atomicValidation = validateAtomicWrite(meta, documents);
908
+ if (atomicValidation)
909
+ return atomicValidation;
910
+ const dimensionValidation = validateDocumentsDimension(meta.dimension, documents);
911
+ if (dimensionValidation) {
912
+ return dimensionValidation;
913
+ }
914
+ const records = documents.map((doc) => ({
915
+ id: doc.id,
916
+ vector: doc.vector,
917
+ content: doc.content ?? "",
918
+ metadata: JSON.stringify(doc.metadata ?? {})
919
+ }));
920
+ const driver = loadLancedb();
921
+ if (!driver.success)
922
+ return driver;
923
+ await table.mergeInsert("id").whenNotMatchedInsertAll().execute(driver.data.makeArrowTable(records, { schema: meta.inputSchema }));
924
+ logger2.info("Vectors inserted", { collection, count: documents.length });
925
+ return ok(void 0);
926
+ });
760
927
  },
761
928
  async upsert(collection, documents) {
762
929
  logger2.debug("Upserting vectors", { collection, count: documents.length });
763
- const table = await openTable(collection);
764
- if (!table) {
765
- return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
766
- }
767
- const dimensionValidation = validateDocumentsDimension(collection, documents);
768
- if (dimensionValidation) {
769
- return dimensionValidation;
770
- }
771
- const records = documents.map((doc) => ({
772
- id: doc.id,
773
- vector: doc.vector,
774
- content: doc.content ?? "",
775
- metadata: JSON.stringify(doc.metadata ?? {})
776
- }));
777
- if (typeof table.mergeInsert === "function") {
778
- await table.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(records);
779
- } else {
780
- logger2.warn("LanceDB mergeInsert is unavailable, falling back to delete/add upsert");
781
- const ids = documents.map((d) => `'${escapeLanceSqlValue(d.id)}'`).join(", ");
782
- await table.delete(`id IN (${ids})`);
783
- await table.add(records);
784
- }
785
- logger2.info("Vectors upserted", { collection, count: documents.length });
786
- return ok(void 0);
930
+ return withCollection(collection, async (table, meta) => {
931
+ const atomicValidation = validateAtomicWrite(meta, documents);
932
+ if (atomicValidation)
933
+ return atomicValidation;
934
+ const dimensionValidation = validateDocumentsDimension(meta.dimension, documents);
935
+ if (dimensionValidation) {
936
+ return dimensionValidation;
937
+ }
938
+ const records = documents.map((doc) => ({
939
+ id: doc.id,
940
+ vector: doc.vector,
941
+ content: doc.content ?? "",
942
+ metadata: JSON.stringify(doc.metadata ?? {})
943
+ }));
944
+ const driver = loadLancedb();
945
+ if (!driver.success)
946
+ return driver;
947
+ await table.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(driver.data.makeArrowTable(records, { schema: meta.inputSchema }));
948
+ logger2.info("Vectors upserted", { collection, count: documents.length });
949
+ return ok(void 0);
950
+ });
787
951
  },
788
952
  async delete(collection, ids) {
789
953
  logger2.debug("Deleting vectors", { collection, count: ids.length });
790
- const table = await openTable(collection);
791
- if (!table) {
792
- return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
793
- }
794
- const idList = ids.map((id) => `'${escapeLanceSqlValue(id)}'`).join(", ");
795
- await table.delete(`id IN (${idList})`);
796
- logger2.info("Vectors deleted", { collection, count: ids.length });
797
- return ok(void 0);
954
+ return withCollection(collection, async (table) => {
955
+ const idList = ids.map((id) => `'${escapeLanceSqlValue(id)}'`).join(", ");
956
+ await table.delete(`id IN (${idList})`);
957
+ logger2.info("Vectors deleted", { collection, count: ids.length });
958
+ return ok(void 0);
959
+ });
798
960
  },
799
961
  async search(collection, vector, options) {
800
962
  logger2.debug("Searching vectors", { collection, topK: options?.topK, hasFilter: !!options?.filter });
801
- const table = await openTable(collection);
802
- if (!table) {
803
- return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
804
- }
805
- const dimensionValidation = validateVectorDimension(collection, vector);
806
- if (dimensionValidation) {
807
- return dimensionValidation;
808
- }
809
- const topK = options?.topK ?? 10;
810
- const minScore = options?.minScore ?? 0;
811
- let query = table.search(vector).limit(topK);
812
- if (options?.filter) {
813
- const filterParts = [];
814
- for (const [key, value] of Object.entries(options.filter)) {
815
- filterParts.push(`metadata LIKE '%"${escapeLanceFilterValue(key)}":"${escapeLanceFilterValue(String(value))}"%'`);
963
+ return withCollection(collection, async (table, meta) => {
964
+ const dimensionValidation = validateVectorDimension(meta.dimension, vector);
965
+ if (dimensionValidation) {
966
+ return dimensionValidation;
816
967
  }
817
- if (filterParts.length > 0) {
818
- query = query.where(filterParts.join(" AND "));
968
+ const topK = options?.topK ?? 10;
969
+ const minScore = options?.minScore ?? 0;
970
+ let query = table.vectorSearch(vector).distanceType(meta.metric === "euclidean" ? "l2" : meta.metric).limit(topK);
971
+ if (options?.filter !== void 0) {
972
+ if (!meta.inputSchema)
973
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_jsonMetadataRequired"));
974
+ const predicate = buildMetadataPredicate(options.filter);
975
+ if (!predicate.success)
976
+ return predicate;
977
+ if (predicate.data)
978
+ query = query.where(predicate.data);
819
979
  }
820
- }
821
- const results = await query.toArray();
822
- const searchResults = results.map((row) => {
823
- const distance = row._distance ?? 0;
824
- const score = 1 / (1 + distance);
825
- return {
826
- id: row.id,
827
- score,
828
- content: row.content || void 0,
829
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
830
- };
831
- }).filter((r) => r.score >= minScore);
832
- return ok(searchResults);
980
+ const results = await query.toArray();
981
+ const searchResults = results.map((row) => {
982
+ const distance = row._distance ?? 0;
983
+ const score = meta.metric === "cosine" ? Math.max(0, Math.min(1, 1 - distance / 2)) : meta.metric === "dot" ? 1 / (1 + Math.exp(distance - 1)) : 1 / (1 + Math.max(0, distance));
984
+ return {
985
+ id: row.id,
986
+ score,
987
+ content: row.content || void 0,
988
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0
989
+ };
990
+ }).filter((r) => r.score >= minScore);
991
+ return ok(searchResults);
992
+ });
833
993
  },
834
994
  async count(collection) {
835
995
  logger2.debug("Counting vectors", { collection });
836
- const table = await openTable(collection);
837
- if (!table) {
838
- return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
839
- }
840
- const count = await table.countRows();
841
- return ok(count);
996
+ return withCollection(collection, async (table) => {
997
+ const count = await table.countRows();
998
+ return ok(count);
999
+ });
842
1000
  }
843
1001
  };
844
1002
  return {
@@ -853,36 +1011,22 @@ function createLancedbProvider() {
853
1011
  return loadResult;
854
1012
  const lancedb = loadResult.data;
855
1013
  try {
856
- connection = await lancedb.connect(lanceConfig.path);
1014
+ connection = await lancedb.connect(lanceConfig.path, { readConsistencyInterval: 0 });
857
1015
  config = lanceConfig;
858
- const tableNames = await connection.tableNames();
859
- await Promise.allSettled(tableNames.map(async (name) => {
860
- try {
861
- const table = await connection.openTable(name);
862
- const count = await table.countRows();
863
- if (count === 0)
864
- return;
865
- const rows = await table.search(Array.from({ length: 1 }).fill(0)).limit(1).toArray();
866
- if (rows.length > 0 && rows[0].vector) {
867
- collectionMetas.set(name, {
868
- dimension: rows[0].vector.length,
869
- metric: lanceConfig.metric ?? "cosine"
870
- });
871
- }
872
- } catch {
873
- }
874
- }));
875
1016
  logger2.info("LanceDB connected", { path: lanceConfig.path });
876
1017
  return ok(void 0);
877
1018
  } catch (error) {
1019
+ connection?.close();
1020
+ connection = null;
1021
+ config = null;
878
1022
  logger2.error("Failed to connect to LanceDB", { error });
879
1023
  return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_connectionFailed", { params: { error: String(error) } }), error);
880
1024
  }
881
1025
  },
882
1026
  async close() {
1027
+ connection?.close();
883
1028
  connection = null;
884
1029
  config = null;
885
- collectionMetas.clear();
886
1030
  logger2.info("LanceDB connection closed");
887
1031
  return ok(void 0);
888
1032
  },
@@ -1257,18 +1401,6 @@ function createPgvectorProvider() {
1257
1401
  vector: createBaseVectorOps(ctx, vectorDriver)
1258
1402
  };
1259
1403
  }
1260
- function sanitizeUrl(url) {
1261
- try {
1262
- const parsed = new URL(url);
1263
- if (parsed.username)
1264
- parsed.username = "***";
1265
- if (parsed.password)
1266
- parsed.password = "***";
1267
- return parsed.toString();
1268
- } catch {
1269
- return "(invalid url)";
1270
- }
1271
- }
1272
1404
  var logger4 = core.logger.child({ module: "vecdb", scope: "qdrant" });
1273
1405
  function isQdrantModule(value) {
1274
1406
  if (typeof value !== "object" || value === null) {
@@ -1289,8 +1421,6 @@ function toQdrantDistance(metric) {
1289
1421
  return "Euclid";
1290
1422
  case "dot":
1291
1423
  return "Dot";
1292
- default:
1293
- return "Cosine";
1294
1424
  }
1295
1425
  }
1296
1426
  function createQdrantProvider() {
@@ -1318,12 +1448,7 @@ function createQdrantProvider() {
1318
1448
  }
1319
1449
  }
1320
1450
  function isQdrantNotFoundError(error) {
1321
- const getCollectionErrorCtor = client?.getCollection.Error;
1322
- if (getCollectionErrorCtor && error instanceof getCollectionErrorCtor) {
1323
- return error.getActualType?.().status === 404;
1324
- }
1325
- const maybeError = error;
1326
- return maybeError.status === 404 || maybeError.response?.status === 404 || maybeError.$metadata?.httpStatusCode === 404;
1451
+ return typeof error === "object" && error !== null && "status" in error && error.status === 404;
1327
1452
  }
1328
1453
  async function getCollectionInfo(name) {
1329
1454
  try {
@@ -1335,12 +1460,22 @@ function createQdrantProvider() {
1335
1460
  throw error;
1336
1461
  }
1337
1462
  }
1463
+ function readVectorDefinition(info) {
1464
+ const vectors = info.config.params.vectors;
1465
+ if (!vectors || !("size" in vectors) || typeof vectors.size !== "number" || !Number.isSafeInteger(vectors.size) || vectors.size <= 0)
1466
+ return void 0;
1467
+ if (!("distance" in vectors))
1468
+ return void 0;
1469
+ const metric = vectors.distance === "Cosine" ? "cosine" : vectors.distance === "Euclid" ? "euclidean" : vectors.distance === "Dot" ? "dot" : void 0;
1470
+ return metric ? { dimension: vectors.size, metric } : void 0;
1471
+ }
1338
1472
  async function getCollectionDimension(name) {
1339
1473
  const infoResult = await getCollectionInfo(name);
1340
1474
  if (!infoResult.success) {
1341
1475
  return infoResult;
1342
1476
  }
1343
- return ok(infoResult.data.config?.params?.vectors?.size ?? 0);
1477
+ const definition = readVectorDefinition(infoResult.data);
1478
+ return definition ? ok(definition.dimension) : err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_invalidCollectionSchema", { params: { name } }));
1344
1479
  }
1345
1480
  function validateVectorDimension(expected, actual) {
1346
1481
  if (expected === actual) {
@@ -1401,14 +1536,12 @@ function createQdrantProvider() {
1401
1536
  return infoResult;
1402
1537
  }
1403
1538
  const collectionInfo = infoResult.data;
1404
- const vectorsConfig = collectionInfo.config?.params?.vectors;
1405
- const dimension = vectorsConfig?.size ?? 0;
1406
- const qdrantDistance = vectorsConfig?.distance ?? "Cosine";
1407
- const metric = qdrantDistance === "Euclid" ? "euclidean" : qdrantDistance === "Dot" ? "dot" : "cosine";
1539
+ const definition = readVectorDefinition(collectionInfo);
1540
+ if (!definition)
1541
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_invalidCollectionSchema", { params: { name } }));
1408
1542
  return ok({
1409
1543
  name,
1410
- dimension,
1411
- metric,
1544
+ ...definition,
1412
1545
  count: collectionInfo.points_count ?? 0
1413
1546
  });
1414
1547
  },
@@ -1441,7 +1574,7 @@ function createQdrantProvider() {
1441
1574
  _content: doc.content ?? ""
1442
1575
  }
1443
1576
  }));
1444
- await client.upsert(collection, { points });
1577
+ await client.upsert(collection, { points, wait: true });
1445
1578
  logger4.info("Vectors inserted", { collection, count: documents.length });
1446
1579
  return ok(void 0);
1447
1580
  },
@@ -1466,14 +1599,14 @@ function createQdrantProvider() {
1466
1599
  _content: doc.content ?? ""
1467
1600
  }
1468
1601
  }));
1469
- await client.upsert(collection, { points });
1602
+ await client.upsert(collection, { points, wait: true });
1470
1603
  logger4.info("Vectors upserted", { collection, count: documents.length });
1471
1604
  return ok(void 0);
1472
1605
  },
1473
1606
  async delete(collection, ids) {
1474
1607
  logger4.debug("Deleting vectors", { collection, count: ids.length });
1475
1608
  const uuids = ids.map(hashToUuid);
1476
- await client.delete(collection, { points: uuids });
1609
+ await client.delete(collection, { points: uuids, wait: true });
1477
1610
  logger4.info("Vectors deleted", { collection, count: ids.length });
1478
1611
  return ok(void 0);
1479
1612
  },
@@ -1508,9 +1641,9 @@ function createQdrantProvider() {
1508
1641
  const payload = point.payload ?? {};
1509
1642
  const { _id, _content, ...metadata } = payload;
1510
1643
  return {
1511
- id: _id ?? "",
1644
+ id: typeof _id === "string" ? _id : "",
1512
1645
  score: point.score,
1513
- content: _content || void 0,
1646
+ content: typeof _content === "string" && _content.length > 0 ? _content : void 0,
1514
1647
  metadata: Object.keys(metadata).length > 0 ? metadata : void 0
1515
1648
  };
1516
1649
  });
@@ -1518,11 +1651,8 @@ function createQdrantProvider() {
1518
1651
  },
1519
1652
  async count(collection) {
1520
1653
  logger4.debug("Counting vectors", { collection });
1521
- const infoResult = await getCollectionInfo(collection);
1522
- if (!infoResult.success) {
1523
- return infoResult;
1524
- }
1525
- return ok(infoResult.data.points_count ?? 0);
1654
+ const count = await client.count(collection, { exact: true });
1655
+ return ok(count.count);
1526
1656
  }
1527
1657
  };
1528
1658
  return {
@@ -1545,7 +1675,7 @@ function createQdrantProvider() {
1545
1675
  await qdrantClient.getCollections();
1546
1676
  client = qdrantClient;
1547
1677
  config = qdrantConfig;
1548
- logger4.info("Qdrant connected", { url: sanitizeUrl(qdrantConfig.url) });
1678
+ logger4.info("Qdrant connected", { url: core.sanitize.sanitizeUrl(qdrantConfig.url) });
1549
1679
  return ok(void 0);
1550
1680
  } catch (error) {
1551
1681
  logger4.error("Failed to connect to Qdrant", { error: error instanceof Error ? error.message : String(error) });
@@ -1571,6 +1701,38 @@ var logger5 = core.logger.child({ module: "vecdb", scope: "main" });
1571
1701
  var currentProvider = null;
1572
1702
  var currentConfig = null;
1573
1703
  var initInProgress = false;
1704
+ var pendingProvider = null;
1705
+ var initCompletion = null;
1706
+ var closingPromise = null;
1707
+ async function closeOwnedProvider() {
1708
+ if (currentProvider) {
1709
+ pendingProvider = currentProvider;
1710
+ currentProvider = null;
1711
+ }
1712
+ currentConfig = null;
1713
+ if (!pendingProvider)
1714
+ return ok(void 0);
1715
+ try {
1716
+ const result = await pendingProvider.close();
1717
+ if (!result.success)
1718
+ return err({ ...result.error, ext: { ...result.error.ext, cleanupPending: true } });
1719
+ pendingProvider = null;
1720
+ return result;
1721
+ } catch (cause) {
1722
+ return err({ ...HaiVecdbError.CONNECTION_FAILED, message: vecdbM("vecdb_closeFailed", { params: { error: cause instanceof Error ? cause.message : String(cause) } }), cause, ext: { cleanupPending: true } });
1723
+ }
1724
+ }
1725
+ async function failInitialization(result) {
1726
+ const cleanup = await closeOwnedProvider();
1727
+ if (result.success)
1728
+ return result;
1729
+ if (!cleanup.success)
1730
+ return err({ ...result.error, ext: { ...result.error.ext, cleanupPending: true, cleanupError: cleanup.error } });
1731
+ const ext = { ...result.error.ext };
1732
+ delete ext.cleanupPending;
1733
+ delete ext.cleanupError;
1734
+ return err({ ...result.error, ext: Object.keys(ext).length ? ext : void 0 });
1735
+ }
1574
1736
  function createProvider(config) {
1575
1737
  switch (config.type) {
1576
1738
  case "lancedb":
@@ -1589,6 +1751,20 @@ var notInitialized = core.module.createNotInitializedKit(
1589
1751
  );
1590
1752
  var notInitializedCollection = notInitialized.proxy();
1591
1753
  var notInitializedVector = notInitialized.proxy();
1754
+ var collectionOps = {
1755
+ create: (name, options) => currentProvider && !closingPromise ? currentProvider.collection.create(name, options) : notInitializedCollection.create(name, options),
1756
+ drop: (name) => currentProvider && !closingPromise ? currentProvider.collection.drop(name) : notInitializedCollection.drop(name),
1757
+ exists: (name) => currentProvider && !closingPromise ? currentProvider.collection.exists(name) : notInitializedCollection.exists(name),
1758
+ info: (name) => currentProvider && !closingPromise ? currentProvider.collection.info(name) : notInitializedCollection.info(name),
1759
+ list: () => currentProvider && !closingPromise ? currentProvider.collection.list() : notInitializedCollection.list()
1760
+ };
1761
+ var vectorOps = {
1762
+ insert: (collection, documents) => currentProvider && !closingPromise ? currentProvider.vector.insert(collection, documents) : notInitializedVector.insert(collection, documents),
1763
+ upsert: (collection, documents) => currentProvider && !closingPromise ? currentProvider.vector.upsert(collection, documents) : notInitializedVector.upsert(collection, documents),
1764
+ delete: (collection, ids) => currentProvider && !closingPromise ? currentProvider.vector.delete(collection, ids) : notInitializedVector.delete(collection, ids),
1765
+ search: (collection, vector, options) => currentProvider && !closingPromise ? currentProvider.vector.search(collection, vector, options) : notInitializedVector.search(collection, vector, options),
1766
+ count: (collection) => currentProvider && !closingPromise ? currentProvider.vector.count(collection) : notInitializedVector.count(collection)
1767
+ };
1592
1768
  var vecdb = {
1593
1769
  /**
1594
1770
  * 初始化向量数据库连接
@@ -1597,19 +1773,19 @@ var vecdb = {
1597
1773
  * @returns 初始化结果,失败时包含错误信息
1598
1774
  */
1599
1775
  async init(config) {
1600
- if (initInProgress) {
1776
+ if (initInProgress || closingPromise) {
1601
1777
  logger5.warn("Vecdb init already in progress, skipping concurrent call");
1602
1778
  return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_initInProgress"));
1603
1779
  }
1604
1780
  initInProgress = true;
1781
+ let resolveInit;
1782
+ initCompletion = new Promise((resolve) => {
1783
+ resolveInit = resolve;
1784
+ });
1605
1785
  try {
1606
- if (currentProvider) {
1607
- logger5.warn("Vecdb module is already initialized, reinitializing");
1608
- const closeResult = await vecdb.close();
1609
- if (!closeResult.success) {
1610
- return closeResult;
1611
- }
1612
- }
1786
+ const cleanup = await closeOwnedProvider();
1787
+ if (!cleanup.success)
1788
+ return cleanup;
1613
1789
  logger5.info("Initializing vecdb module");
1614
1790
  const parseResult = VecdbConfigSchema.safeParse(config);
1615
1791
  if (!parseResult.success) {
@@ -1617,26 +1793,32 @@ var vecdb = {
1617
1793
  return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_configError", { params: { error: parseResult.error.message } }), parseResult.error);
1618
1794
  }
1619
1795
  const parsed = parseResult.data;
1796
+ if (core.module.requiresSharedState(process.env) && (parsed.type === "lancedb" || parsed.type === "chroma" && !!parsed.path && !parsed.url))
1797
+ return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_sharedBackendRequired"));
1620
1798
  try {
1621
1799
  const provider = createProvider(parsed);
1800
+ pendingProvider = provider;
1622
1801
  const connectResult = await provider.connect(parsed);
1623
1802
  if (!connectResult.success) {
1624
1803
  logger5.error("Vecdb module initialization failed", {
1625
1804
  code: connectResult.error.code,
1626
1805
  message: connectResult.error.message
1627
1806
  });
1628
- return connectResult;
1807
+ return await failInitialization(connectResult);
1629
1808
  }
1809
+ pendingProvider = null;
1630
1810
  currentProvider = provider;
1631
1811
  currentConfig = parsed;
1632
1812
  logger5.info("Vecdb module initialized", { type: parsed.type });
1633
1813
  return ok(void 0);
1634
1814
  } catch (error) {
1635
1815
  logger5.error("Vecdb module initialization failed", { error });
1636
- return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_initFailed", { params: { error: error instanceof Error ? error.message : String(error) } }), error);
1816
+ return await failInitialization(err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_initFailed", { params: { error: error instanceof Error ? error.message : String(error) } }), error));
1637
1817
  }
1638
1818
  } finally {
1639
1819
  initInProgress = false;
1820
+ initCompletion = null;
1821
+ resolveInit();
1640
1822
  }
1641
1823
  },
1642
1824
  /**
@@ -1645,7 +1827,7 @@ var vecdb = {
1645
1827
  * 未初始化时返回占位对象(所有调用返回 NOT_INITIALIZED)。
1646
1828
  */
1647
1829
  get collection() {
1648
- return currentProvider?.collection ?? notInitializedCollection;
1830
+ return collectionOps;
1649
1831
  },
1650
1832
  /**
1651
1833
  * 获取向量操作接口
@@ -1653,15 +1835,15 @@ var vecdb = {
1653
1835
  * 未初始化时返回占位对象(所有调用返回 NOT_INITIALIZED)。
1654
1836
  */
1655
1837
  get vector() {
1656
- return currentProvider?.vector ?? notInitializedVector;
1838
+ return vectorOps;
1657
1839
  },
1658
1840
  /** 获取当前脱敏配置快照(未初始化时为 null) */
1659
1841
  get config() {
1660
- return currentConfig ? core.sanitize.sanitizeSensitiveFields(currentConfig) : null;
1842
+ return currentConfig && !closingPromise ? core.sanitize.sanitizeSensitiveFields(currentConfig) : null;
1661
1843
  },
1662
1844
  /** 获取底层后端的原始连接信息(含凭证,未初始化时为 null;仅供同进程内集成使用) */
1663
1845
  get rawConnection() {
1664
- if (!currentConfig)
1846
+ if (!currentConfig || closingPromise)
1665
1847
  return null;
1666
1848
  if (currentConfig.type === "qdrant")
1667
1849
  return { type: "qdrant", url: currentConfig.url, apiKey: currentConfig.apiKey };
@@ -1690,7 +1872,7 @@ var vecdb = {
1690
1872
  },
1691
1873
  /** 检查是否已初始化 */
1692
1874
  get isInitialized() {
1693
- return currentProvider !== null && currentProvider.isConnected();
1875
+ return currentProvider !== null && !closingPromise && currentProvider.isConnected();
1694
1876
  },
1695
1877
  /**
1696
1878
  * 关闭向量数据库连接
@@ -1698,27 +1880,16 @@ var vecdb = {
1698
1880
  * 多次调用安全,未初始化时直接返回。
1699
1881
  */
1700
1882
  async close() {
1701
- if (!currentProvider) {
1702
- currentConfig = null;
1703
- logger5.info("Vecdb module already closed, skipping");
1704
- return ok(void 0);
1705
- }
1706
- logger5.info("Closing vecdb module");
1707
- try {
1708
- const closeResult = await currentProvider.close();
1709
- if (!closeResult.success) {
1710
- logger5.error("Vecdb module close failed", { code: closeResult.error.code, message: closeResult.error.message });
1711
- return closeResult;
1712
- }
1713
- logger5.info("Vecdb module closed");
1714
- return ok(void 0);
1715
- } catch (error) {
1716
- logger5.error("Vecdb module close failed", { error });
1717
- return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_closeFailed", { params: { error: error instanceof Error ? error.message : String(error) } }), error);
1718
- } finally {
1719
- currentProvider = null;
1720
- currentConfig = null;
1883
+ if (!closingPromise) {
1884
+ closingPromise = (async () => {
1885
+ if (initCompletion)
1886
+ await initCompletion;
1887
+ return closeOwnedProvider();
1888
+ })().finally(() => {
1889
+ closingPromise = null;
1890
+ });
1721
1891
  }
1892
+ return await closingPromise;
1722
1893
  }
1723
1894
  };
1724
1895