@h-ai/vecdb 0.1.0-alpha5
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/LICENSE +202 -0
- package/README.md +95 -0
- package/dist/index.d.ts +477 -0
- package/dist/index.js +1056 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { core, ok, err } from '@h-ai/core';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
// src/vecdb-config.ts
|
|
6
|
+
|
|
7
|
+
// messages/en-US.json
|
|
8
|
+
var en_US_default = {
|
|
9
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
10
|
+
vecdb_notInitialized: "Vector database not initialized, please call vecdb.init() first",
|
|
11
|
+
vecdb_initFailed: "Failed to initialize vector database: {error}",
|
|
12
|
+
vecdb_closeFailed: "Failed to close vector database: {error}",
|
|
13
|
+
vecdb_configError: "Vector database config validation failed: {error}",
|
|
14
|
+
vecdb_initInProgress: "Vector database initialization is already in progress",
|
|
15
|
+
vecdb_unsupportedType: "Unsupported vector database type: {type}",
|
|
16
|
+
vecdb_driverNotFound: "Vector database driver not installed: {driver}, please run pnpm add {driver}",
|
|
17
|
+
vecdb_connectionFailed: "Failed to connect to vector database: {error}",
|
|
18
|
+
vecdb_collectionNotFound: "Collection not found: {name}",
|
|
19
|
+
vecdb_collectionAlreadyExists: "Collection already exists: {name}",
|
|
20
|
+
vecdb_dimensionMismatch: "Vector dimension mismatch: expected {expected}, actual {actual}",
|
|
21
|
+
vecdb_insertFailed: "Failed to insert vectors: {error}",
|
|
22
|
+
vecdb_deleteFailed: "Failed to delete vectors: {error}",
|
|
23
|
+
vecdb_updateFailed: "Failed to update vectors: {error}",
|
|
24
|
+
vecdb_queryFailed: "Failed to query vectors: {error}",
|
|
25
|
+
vecdb_indexBuildFailed: "Failed to build index: {error}",
|
|
26
|
+
vecdb_serializationFailed: "Serialization/deserialization failed: {error}",
|
|
27
|
+
vecdb_configPathRequired: "Database storage path is required",
|
|
28
|
+
vecdb_configDatabaseRequired: "Database name is required"
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// messages/zh-CN.json
|
|
32
|
+
var zh_CN_default = {
|
|
33
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
34
|
+
vecdb_notInitialized: "\u5411\u91CF\u6570\u636E\u5E93\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 vecdb.init()",
|
|
35
|
+
vecdb_initFailed: "\u521D\u59CB\u5316\u5411\u91CF\u6570\u636E\u5E93\u5931\u8D25: {error}",
|
|
36
|
+
vecdb_closeFailed: "\u5173\u95ED\u5411\u91CF\u6570\u636E\u5E93\u5931\u8D25: {error}",
|
|
37
|
+
vecdb_configError: "\u5411\u91CF\u6570\u636E\u5E93\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A{error}",
|
|
38
|
+
vecdb_initInProgress: "\u5411\u91CF\u6570\u636E\u5E93\u521D\u59CB\u5316\u6B63\u5728\u8FDB\u884C\u4E2D",
|
|
39
|
+
vecdb_unsupportedType: "\u4E0D\u652F\u6301\u7684\u5411\u91CF\u6570\u636E\u5E93\u7C7B\u578B: {type}",
|
|
40
|
+
vecdb_driverNotFound: "\u5411\u91CF\u6570\u636E\u5E93\u9A71\u52A8\u672A\u5B89\u88C5: {driver}\uFF0C\u8BF7\u6267\u884C pnpm add {driver}",
|
|
41
|
+
vecdb_connectionFailed: "\u8FDE\u63A5\u5411\u91CF\u6570\u636E\u5E93\u5931\u8D25: {error}",
|
|
42
|
+
vecdb_collectionNotFound: "\u96C6\u5408\u4E0D\u5B58\u5728: {name}",
|
|
43
|
+
vecdb_collectionAlreadyExists: "\u96C6\u5408\u5DF2\u5B58\u5728: {name}",
|
|
44
|
+
vecdb_dimensionMismatch: "\u5411\u91CF\u7EF4\u5EA6\u4E0D\u5339\u914D: \u671F\u671B {expected}\uFF0C\u5B9E\u9645 {actual}",
|
|
45
|
+
vecdb_insertFailed: "\u63D2\u5165\u5411\u91CF\u5931\u8D25: {error}",
|
|
46
|
+
vecdb_deleteFailed: "\u5220\u9664\u5411\u91CF\u5931\u8D25: {error}",
|
|
47
|
+
vecdb_updateFailed: "\u66F4\u65B0\u5411\u91CF\u5931\u8D25: {error}",
|
|
48
|
+
vecdb_queryFailed: "\u67E5\u8BE2\u5411\u91CF\u5931\u8D25: {error}",
|
|
49
|
+
vecdb_indexBuildFailed: "\u6784\u5EFA\u7D22\u5F15\u5931\u8D25: {error}",
|
|
50
|
+
vecdb_serializationFailed: "\u5E8F\u5217\u5316/\u53CD\u5E8F\u5217\u5316\u5931\u8D25: {error}",
|
|
51
|
+
vecdb_configPathRequired: "\u6570\u636E\u5E93\u5B58\u50A8\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A",
|
|
52
|
+
vecdb_configDatabaseRequired: "\u6570\u636E\u5E93\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A"
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/vecdb-i18n.ts
|
|
56
|
+
var vecdbM = core.i18n.createMessageGetter({
|
|
57
|
+
"zh-CN": zh_CN_default,
|
|
58
|
+
"en-US": en_US_default
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// src/vecdb-config.ts
|
|
62
|
+
var VecdbTypeSchema = z.enum(["lancedb", "pgvector", "qdrant"]);
|
|
63
|
+
var DistanceMetricSchema = z.enum(["cosine", "euclidean", "dot"]).default("cosine");
|
|
64
|
+
var LancedbConfigSchema = z.object({
|
|
65
|
+
type: z.literal("lancedb"),
|
|
66
|
+
/** 数据库存储路径(本地目录) */
|
|
67
|
+
path: z.string().min(1, vecdbM("vecdb_configPathRequired")),
|
|
68
|
+
/** 距离度量(默认 cosine) */
|
|
69
|
+
metric: DistanceMetricSchema.optional()
|
|
70
|
+
});
|
|
71
|
+
var PgvectorIndexTypeSchema = z.enum(["ivfflat", "hnsw"]).default("hnsw");
|
|
72
|
+
var PgvectorConfigSchema = z.object({
|
|
73
|
+
type: z.literal("pgvector"),
|
|
74
|
+
/** 连接字符串(可选,优先使用) */
|
|
75
|
+
url: z.string().optional(),
|
|
76
|
+
/** 数据库主机地址(默认 localhost) */
|
|
77
|
+
host: z.string().default("localhost"),
|
|
78
|
+
/** 端口号(默认 5432) */
|
|
79
|
+
port: z.number().int().min(1).max(65535).default(5432),
|
|
80
|
+
/** 数据库名称 */
|
|
81
|
+
database: z.string().min(1, vecdbM("vecdb_configDatabaseRequired")),
|
|
82
|
+
/** 用户名 */
|
|
83
|
+
user: z.string().optional(),
|
|
84
|
+
/** 密码 */
|
|
85
|
+
password: z.string().optional(),
|
|
86
|
+
/** 索引类型(默认 hnsw) */
|
|
87
|
+
indexType: PgvectorIndexTypeSchema.optional(),
|
|
88
|
+
/** 距离度量(默认 cosine) */
|
|
89
|
+
metric: DistanceMetricSchema.optional(),
|
|
90
|
+
/** 表名前缀(默认 'vec_') */
|
|
91
|
+
tablePrefix: z.string().default("vec_")
|
|
92
|
+
});
|
|
93
|
+
var QdrantConfigSchema = z.object({
|
|
94
|
+
type: z.literal("qdrant"),
|
|
95
|
+
/** Qdrant 服务器 URL */
|
|
96
|
+
url: z.string().url().default("http://localhost:6333"),
|
|
97
|
+
/** API Key(可选) */
|
|
98
|
+
apiKey: z.string().optional(),
|
|
99
|
+
/** 距离度量(默认 cosine) */
|
|
100
|
+
metric: DistanceMetricSchema.optional()
|
|
101
|
+
});
|
|
102
|
+
var VecdbConfigSchema = z.discriminatedUnion("type", [
|
|
103
|
+
LancedbConfigSchema,
|
|
104
|
+
PgvectorConfigSchema,
|
|
105
|
+
QdrantConfigSchema
|
|
106
|
+
]);
|
|
107
|
+
var VecdbErrorInfo = {
|
|
108
|
+
CONNECTION_FAILED: "001:500",
|
|
109
|
+
QUERY_FAILED: "002:500",
|
|
110
|
+
COLLECTION_NOT_FOUND: "003:404",
|
|
111
|
+
COLLECTION_ALREADY_EXISTS: "004:409",
|
|
112
|
+
DIMENSION_MISMATCH: "005:400",
|
|
113
|
+
INSERT_FAILED: "006:500",
|
|
114
|
+
DELETE_FAILED: "007:500",
|
|
115
|
+
UPDATE_FAILED: "008:500",
|
|
116
|
+
INDEX_BUILD_FAILED: "009:500",
|
|
117
|
+
NOT_INITIALIZED: "010:500",
|
|
118
|
+
CONFIG_ERROR: "011:500",
|
|
119
|
+
UNSUPPORTED_TYPE: "012:400",
|
|
120
|
+
DRIVER_NOT_FOUND: "013:500",
|
|
121
|
+
SERIALIZATION_FAILED: "014:500"
|
|
122
|
+
};
|
|
123
|
+
var HaiVecdbError = core.error.buildHaiErrorsDef("vecdb", VecdbErrorInfo);
|
|
124
|
+
function errorMsgFromCode(def, errorStr) {
|
|
125
|
+
switch (def) {
|
|
126
|
+
case HaiVecdbError.DELETE_FAILED:
|
|
127
|
+
return vecdbM("vecdb_deleteFailed", { params: { error: errorStr } });
|
|
128
|
+
case HaiVecdbError.INSERT_FAILED:
|
|
129
|
+
return vecdbM("vecdb_insertFailed", { params: { error: errorStr } });
|
|
130
|
+
case HaiVecdbError.UPDATE_FAILED:
|
|
131
|
+
return vecdbM("vecdb_updateFailed", { params: { error: errorStr } });
|
|
132
|
+
default:
|
|
133
|
+
return vecdbM("vecdb_queryFailed", { params: { error: errorStr } });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function wrapOp(ctx, fn, errorDef, errorLabel, errorMeta) {
|
|
137
|
+
if (!ctx.isConnected()) {
|
|
138
|
+
return err(HaiVecdbError.NOT_INITIALIZED, vecdbM("vecdb_notInitialized"));
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return await fn();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
ctx.logger.error(errorLabel, { ...errorMeta, error });
|
|
144
|
+
return err(errorDef, errorMsgFromCode(errorDef, String(error)), error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function createBaseCollectionOps(ctx, driver) {
|
|
148
|
+
return {
|
|
149
|
+
create: (name, options) => wrapOp(
|
|
150
|
+
ctx,
|
|
151
|
+
() => driver.create(name, options),
|
|
152
|
+
HaiVecdbError.QUERY_FAILED,
|
|
153
|
+
"Failed to create collection",
|
|
154
|
+
{ name }
|
|
155
|
+
),
|
|
156
|
+
drop: (name) => wrapOp(
|
|
157
|
+
ctx,
|
|
158
|
+
() => driver.drop(name),
|
|
159
|
+
HaiVecdbError.DELETE_FAILED,
|
|
160
|
+
"Failed to drop collection",
|
|
161
|
+
{ name }
|
|
162
|
+
),
|
|
163
|
+
exists: (name) => wrapOp(
|
|
164
|
+
ctx,
|
|
165
|
+
() => driver.exists(name),
|
|
166
|
+
HaiVecdbError.QUERY_FAILED,
|
|
167
|
+
"Failed to check collection",
|
|
168
|
+
{ name }
|
|
169
|
+
),
|
|
170
|
+
info: (name) => wrapOp(
|
|
171
|
+
ctx,
|
|
172
|
+
() => driver.info(name),
|
|
173
|
+
HaiVecdbError.QUERY_FAILED,
|
|
174
|
+
"Failed to get collection info",
|
|
175
|
+
{ name }
|
|
176
|
+
),
|
|
177
|
+
list: () => wrapOp(
|
|
178
|
+
ctx,
|
|
179
|
+
() => driver.list(),
|
|
180
|
+
HaiVecdbError.QUERY_FAILED,
|
|
181
|
+
"Failed to list collections"
|
|
182
|
+
)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function createBaseVectorOps(ctx, driver) {
|
|
186
|
+
return {
|
|
187
|
+
insert: (collection, documents) => wrapOp(
|
|
188
|
+
ctx,
|
|
189
|
+
() => driver.insert(collection, documents),
|
|
190
|
+
HaiVecdbError.INSERT_FAILED,
|
|
191
|
+
"Failed to insert vectors",
|
|
192
|
+
{ collection }
|
|
193
|
+
),
|
|
194
|
+
upsert: (collection, documents) => wrapOp(
|
|
195
|
+
ctx,
|
|
196
|
+
() => driver.upsert(collection, documents),
|
|
197
|
+
HaiVecdbError.UPDATE_FAILED,
|
|
198
|
+
"Failed to upsert vectors",
|
|
199
|
+
{ collection }
|
|
200
|
+
),
|
|
201
|
+
delete: (collection, ids) => wrapOp(
|
|
202
|
+
ctx,
|
|
203
|
+
() => driver.delete(collection, ids),
|
|
204
|
+
HaiVecdbError.DELETE_FAILED,
|
|
205
|
+
"Failed to delete vectors",
|
|
206
|
+
{ collection }
|
|
207
|
+
),
|
|
208
|
+
search: (collection, vector, options) => wrapOp(
|
|
209
|
+
ctx,
|
|
210
|
+
() => driver.search(collection, vector, options),
|
|
211
|
+
HaiVecdbError.QUERY_FAILED,
|
|
212
|
+
"Failed to search vectors",
|
|
213
|
+
{ collection }
|
|
214
|
+
),
|
|
215
|
+
count: (collection) => wrapOp(
|
|
216
|
+
ctx,
|
|
217
|
+
() => driver.count(collection),
|
|
218
|
+
HaiVecdbError.QUERY_FAILED,
|
|
219
|
+
"Failed to count vectors",
|
|
220
|
+
{ collection }
|
|
221
|
+
)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/providers/vecdb-provider-lancedb.ts
|
|
226
|
+
var logger = core.logger.child({ module: "vecdb", scope: "lancedb" });
|
|
227
|
+
function escapeLanceFilterValue(value) {
|
|
228
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
229
|
+
}
|
|
230
|
+
function createLancedbProvider() {
|
|
231
|
+
let connection = null;
|
|
232
|
+
let config = null;
|
|
233
|
+
const collectionMetas = /* @__PURE__ */ new Map();
|
|
234
|
+
async function loadLancedb() {
|
|
235
|
+
try {
|
|
236
|
+
const mod = await import('@lancedb/lancedb');
|
|
237
|
+
return ok(mod);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logger.error("Failed to load @lancedb/lancedb", { error });
|
|
240
|
+
return err(HaiVecdbError.DRIVER_NOT_FOUND, vecdbM("vecdb_driverNotFound", { params: { driver: "@lancedb/lancedb" } }), error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function openTable(name) {
|
|
244
|
+
if (!connection)
|
|
245
|
+
return null;
|
|
246
|
+
try {
|
|
247
|
+
const tableNames = await connection.tableNames();
|
|
248
|
+
if (tableNames.includes(name)) {
|
|
249
|
+
return await connection.openTable(name);
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const ctx = { isConnected: () => connection !== null, logger };
|
|
257
|
+
const collectionDriver = {
|
|
258
|
+
async create(name, options) {
|
|
259
|
+
logger.debug("Creating collection", { name, dimension: options.dimension, metric: options.metric });
|
|
260
|
+
const tableNames = await connection.tableNames();
|
|
261
|
+
if (tableNames.includes(name)) {
|
|
262
|
+
return err(HaiVecdbError.COLLECTION_ALREADY_EXISTS, vecdbM("vecdb_collectionAlreadyExists", { params: { name } }));
|
|
263
|
+
}
|
|
264
|
+
const dimension = options.dimension;
|
|
265
|
+
const metric = options.metric ?? config?.metric ?? "cosine";
|
|
266
|
+
const initRecord = {
|
|
267
|
+
id: "__init__",
|
|
268
|
+
vector: Array.from({ length: dimension }, () => 0),
|
|
269
|
+
content: "",
|
|
270
|
+
metadata: "{}"
|
|
271
|
+
};
|
|
272
|
+
const table = await connection.createTable(name, [initRecord]);
|
|
273
|
+
await table.delete('id = "__init__"');
|
|
274
|
+
collectionMetas.set(name, { dimension, metric });
|
|
275
|
+
logger.info("Collection created", { name, dimension, metric });
|
|
276
|
+
return ok(void 0);
|
|
277
|
+
},
|
|
278
|
+
async drop(name) {
|
|
279
|
+
logger.debug("Dropping collection", { name });
|
|
280
|
+
const tableNames = await connection.tableNames();
|
|
281
|
+
if (!tableNames.includes(name)) {
|
|
282
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
|
|
283
|
+
}
|
|
284
|
+
await connection.dropTable(name);
|
|
285
|
+
collectionMetas.delete(name);
|
|
286
|
+
logger.info("Collection dropped", { name });
|
|
287
|
+
return ok(void 0);
|
|
288
|
+
},
|
|
289
|
+
async exists(name) {
|
|
290
|
+
logger.debug("Checking collection exists", { name });
|
|
291
|
+
const tableNames = await connection.tableNames();
|
|
292
|
+
return ok(tableNames.includes(name));
|
|
293
|
+
},
|
|
294
|
+
async info(name) {
|
|
295
|
+
logger.debug("Getting collection info", { name });
|
|
296
|
+
const table = await openTable(name);
|
|
297
|
+
if (!table) {
|
|
298
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
|
|
299
|
+
}
|
|
300
|
+
const count = await table.countRows();
|
|
301
|
+
const meta = collectionMetas.get(name);
|
|
302
|
+
return ok({
|
|
303
|
+
name,
|
|
304
|
+
dimension: meta?.dimension ?? 0,
|
|
305
|
+
metric: meta?.metric ?? config?.metric ?? "cosine",
|
|
306
|
+
count
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
async list() {
|
|
310
|
+
logger.debug("Listing collections");
|
|
311
|
+
const tableNames = await connection.tableNames();
|
|
312
|
+
return ok(tableNames);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const vectorDriver = {
|
|
316
|
+
async insert(collection, documents) {
|
|
317
|
+
logger.debug("Inserting vectors", { collection, count: documents.length });
|
|
318
|
+
const table = await openTable(collection);
|
|
319
|
+
if (!table) {
|
|
320
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
|
|
321
|
+
}
|
|
322
|
+
const records = documents.map((doc) => ({
|
|
323
|
+
id: doc.id,
|
|
324
|
+
vector: doc.vector,
|
|
325
|
+
content: doc.content ?? "",
|
|
326
|
+
metadata: JSON.stringify(doc.metadata ?? {})
|
|
327
|
+
}));
|
|
328
|
+
await table.add(records);
|
|
329
|
+
logger.info("Vectors inserted", { collection, count: documents.length });
|
|
330
|
+
return ok(void 0);
|
|
331
|
+
},
|
|
332
|
+
async upsert(collection, documents) {
|
|
333
|
+
logger.debug("Upserting vectors", { collection, count: documents.length });
|
|
334
|
+
const table = await openTable(collection);
|
|
335
|
+
if (!table) {
|
|
336
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
|
|
337
|
+
}
|
|
338
|
+
const records = documents.map((doc) => ({
|
|
339
|
+
id: doc.id,
|
|
340
|
+
vector: doc.vector,
|
|
341
|
+
content: doc.content ?? "",
|
|
342
|
+
metadata: JSON.stringify(doc.metadata ?? {})
|
|
343
|
+
}));
|
|
344
|
+
const ids = documents.map((d) => `"${escapeLanceFilterValue(d.id)}"`).join(", ");
|
|
345
|
+
await table.delete(`id IN (${ids})`);
|
|
346
|
+
await table.add(records);
|
|
347
|
+
logger.info("Vectors upserted", { collection, count: documents.length });
|
|
348
|
+
return ok(void 0);
|
|
349
|
+
},
|
|
350
|
+
async delete(collection, ids) {
|
|
351
|
+
logger.debug("Deleting vectors", { collection, count: ids.length });
|
|
352
|
+
const table = await openTable(collection);
|
|
353
|
+
if (!table) {
|
|
354
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
|
|
355
|
+
}
|
|
356
|
+
const idList = ids.map((id) => `"${escapeLanceFilterValue(id)}"`).join(", ");
|
|
357
|
+
await table.delete(`id IN (${idList})`);
|
|
358
|
+
logger.info("Vectors deleted", { collection, count: ids.length });
|
|
359
|
+
return ok(void 0);
|
|
360
|
+
},
|
|
361
|
+
async search(collection, vector, options) {
|
|
362
|
+
logger.debug("Searching vectors", { collection, topK: options?.topK, hasFilter: !!options?.filter });
|
|
363
|
+
const table = await openTable(collection);
|
|
364
|
+
if (!table) {
|
|
365
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
|
|
366
|
+
}
|
|
367
|
+
const topK = options?.topK ?? 10;
|
|
368
|
+
const minScore = options?.minScore ?? 0;
|
|
369
|
+
let query = table.search(vector).limit(topK);
|
|
370
|
+
if (options?.filter) {
|
|
371
|
+
const filterParts = [];
|
|
372
|
+
for (const [key, value] of Object.entries(options.filter)) {
|
|
373
|
+
filterParts.push(`metadata LIKE '%"${escapeLanceFilterValue(key)}":"${escapeLanceFilterValue(String(value))}"%'`);
|
|
374
|
+
}
|
|
375
|
+
if (filterParts.length > 0) {
|
|
376
|
+
query = query.where(filterParts.join(" AND "));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const results = await query.toArray();
|
|
380
|
+
const searchResults = results.map((row) => {
|
|
381
|
+
const distance = row._distance ?? 0;
|
|
382
|
+
const score = 1 / (1 + distance);
|
|
383
|
+
return {
|
|
384
|
+
id: row.id,
|
|
385
|
+
score,
|
|
386
|
+
content: row.content || void 0,
|
|
387
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
388
|
+
};
|
|
389
|
+
}).filter((r) => r.score >= minScore);
|
|
390
|
+
return ok(searchResults);
|
|
391
|
+
},
|
|
392
|
+
async count(collection) {
|
|
393
|
+
logger.debug("Counting vectors", { collection });
|
|
394
|
+
const table = await openTable(collection);
|
|
395
|
+
if (!table) {
|
|
396
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name: collection } }));
|
|
397
|
+
}
|
|
398
|
+
const count = await table.countRows();
|
|
399
|
+
return ok(count);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
return {
|
|
403
|
+
name: "lancedb",
|
|
404
|
+
async connect(cfg) {
|
|
405
|
+
if (cfg.type !== "lancedb") {
|
|
406
|
+
return err(HaiVecdbError.UNSUPPORTED_TYPE, vecdbM("vecdb_unsupportedType", { params: { type: cfg.type } }));
|
|
407
|
+
}
|
|
408
|
+
const lanceConfig = cfg;
|
|
409
|
+
const loadResult = await loadLancedb();
|
|
410
|
+
if (!loadResult.success)
|
|
411
|
+
return loadResult;
|
|
412
|
+
const lancedb = loadResult.data;
|
|
413
|
+
try {
|
|
414
|
+
connection = await lancedb.connect(lanceConfig.path);
|
|
415
|
+
config = lanceConfig;
|
|
416
|
+
const tableNames = await connection.tableNames();
|
|
417
|
+
await Promise.allSettled(tableNames.map(async (name) => {
|
|
418
|
+
try {
|
|
419
|
+
const table = await connection.openTable(name);
|
|
420
|
+
const count = await table.countRows();
|
|
421
|
+
if (count === 0)
|
|
422
|
+
return;
|
|
423
|
+
const rows = await table.search(Array.from({ length: 1 }).fill(0)).limit(1).toArray();
|
|
424
|
+
if (rows.length > 0 && rows[0].vector) {
|
|
425
|
+
collectionMetas.set(name, {
|
|
426
|
+
dimension: rows[0].vector.length,
|
|
427
|
+
metric: lanceConfig.metric ?? "cosine"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
}));
|
|
433
|
+
logger.info("LanceDB connected", { path: lanceConfig.path });
|
|
434
|
+
return ok(void 0);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
logger.error("Failed to connect to LanceDB", { error });
|
|
437
|
+
return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_connectionFailed", { params: { error: String(error) } }), error);
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
async close() {
|
|
441
|
+
connection = null;
|
|
442
|
+
config = null;
|
|
443
|
+
collectionMetas.clear();
|
|
444
|
+
logger.info("LanceDB connection closed");
|
|
445
|
+
return ok(void 0);
|
|
446
|
+
},
|
|
447
|
+
isConnected() {
|
|
448
|
+
return connection !== null;
|
|
449
|
+
},
|
|
450
|
+
collection: createBaseCollectionOps(ctx, collectionDriver),
|
|
451
|
+
vector: createBaseVectorOps(ctx, vectorDriver)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
var logger2 = core.logger.child({ module: "vecdb", scope: "pgvector" });
|
|
455
|
+
function createPgvectorProvider() {
|
|
456
|
+
let pool = null;
|
|
457
|
+
let config = null;
|
|
458
|
+
function tableName(collection) {
|
|
459
|
+
return `${config?.tablePrefix ?? "vec_"}${collection}`;
|
|
460
|
+
}
|
|
461
|
+
function quoteIdent(name) {
|
|
462
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
463
|
+
}
|
|
464
|
+
function distanceOp() {
|
|
465
|
+
const metric = config?.metric ?? "cosine";
|
|
466
|
+
switch (metric) {
|
|
467
|
+
case "cosine":
|
|
468
|
+
return "<=>";
|
|
469
|
+
case "euclidean":
|
|
470
|
+
return "<->";
|
|
471
|
+
case "dot":
|
|
472
|
+
return "<#>";
|
|
473
|
+
default:
|
|
474
|
+
return "<=>";
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function distanceToScore(distance) {
|
|
478
|
+
const metric = config?.metric ?? "cosine";
|
|
479
|
+
switch (metric) {
|
|
480
|
+
case "cosine":
|
|
481
|
+
return 1 - distance / 2;
|
|
482
|
+
case "euclidean":
|
|
483
|
+
return 1 / (1 + distance);
|
|
484
|
+
case "dot":
|
|
485
|
+
return -distance;
|
|
486
|
+
default:
|
|
487
|
+
return 1 / (1 + distance);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const ctx = { isConnected: () => pool !== null, logger: logger2 };
|
|
491
|
+
const collectionDriver = {
|
|
492
|
+
async create(name, options) {
|
|
493
|
+
const table = tableName(name);
|
|
494
|
+
const dimension = options.dimension;
|
|
495
|
+
const metric = options.metric ?? config?.metric ?? "cosine";
|
|
496
|
+
const indexType = config?.indexType ?? "hnsw";
|
|
497
|
+
logger2.debug("Creating collection", { name, dimension, metric, indexType });
|
|
498
|
+
const checkResult = await pool.query(
|
|
499
|
+
`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)`,
|
|
500
|
+
[table]
|
|
501
|
+
);
|
|
502
|
+
if (checkResult.rows[0].exists) {
|
|
503
|
+
return err(HaiVecdbError.COLLECTION_ALREADY_EXISTS, vecdbM("vecdb_collectionAlreadyExists", { params: { name } }));
|
|
504
|
+
}
|
|
505
|
+
const qi = quoteIdent(table);
|
|
506
|
+
await pool.query(`
|
|
507
|
+
CREATE TABLE ${qi} (
|
|
508
|
+
id TEXT PRIMARY KEY,
|
|
509
|
+
vector vector(${dimension}),
|
|
510
|
+
content TEXT,
|
|
511
|
+
metadata JSONB DEFAULT '{}'::jsonb
|
|
512
|
+
)
|
|
513
|
+
`);
|
|
514
|
+
const distOp = distanceOp();
|
|
515
|
+
const opsClass = distOp === "<=>" ? "vector_cosine_ops" : distOp === "<->" ? "vector_l2_ops" : "vector_ip_ops";
|
|
516
|
+
if (indexType === "hnsw") {
|
|
517
|
+
await pool.query(`
|
|
518
|
+
CREATE INDEX ON ${qi}
|
|
519
|
+
USING hnsw (vector ${opsClass})
|
|
520
|
+
`);
|
|
521
|
+
} else {
|
|
522
|
+
await pool.query(`
|
|
523
|
+
CREATE INDEX ON ${qi}
|
|
524
|
+
USING ivfflat (vector ${opsClass})
|
|
525
|
+
WITH (lists = 100)
|
|
526
|
+
`);
|
|
527
|
+
}
|
|
528
|
+
logger2.info("Collection created", { name, dimension, metric, indexType });
|
|
529
|
+
return ok(void 0);
|
|
530
|
+
},
|
|
531
|
+
async drop(name) {
|
|
532
|
+
const table = tableName(name);
|
|
533
|
+
logger2.debug("Dropping collection", { name });
|
|
534
|
+
const checkResult = await pool.query(
|
|
535
|
+
`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)`,
|
|
536
|
+
[table]
|
|
537
|
+
);
|
|
538
|
+
if (!checkResult.rows[0].exists) {
|
|
539
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
|
|
540
|
+
}
|
|
541
|
+
await pool.query(`DROP TABLE ${quoteIdent(table)}`);
|
|
542
|
+
logger2.info("Collection dropped", { name });
|
|
543
|
+
return ok(void 0);
|
|
544
|
+
},
|
|
545
|
+
async exists(name) {
|
|
546
|
+
logger2.debug("Checking collection exists", { name });
|
|
547
|
+
const result = await pool.query(
|
|
548
|
+
`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)`,
|
|
549
|
+
[tableName(name)]
|
|
550
|
+
);
|
|
551
|
+
return ok(result.rows[0].exists);
|
|
552
|
+
},
|
|
553
|
+
async info(name) {
|
|
554
|
+
const table = tableName(name);
|
|
555
|
+
logger2.debug("Getting collection info", { name });
|
|
556
|
+
const existsResult = await pool.query(
|
|
557
|
+
`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)`,
|
|
558
|
+
[table]
|
|
559
|
+
);
|
|
560
|
+
if (!existsResult.rows[0].exists) {
|
|
561
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
|
|
562
|
+
}
|
|
563
|
+
const countResult = await pool.query(`SELECT COUNT(*) as count FROM ${quoteIdent(table)}`);
|
|
564
|
+
const count = Number.parseInt(String(countResult.rows[0].count), 10);
|
|
565
|
+
const dimResult = await pool.query(`
|
|
566
|
+
SELECT atttypmod FROM pg_attribute
|
|
567
|
+
WHERE attrelid = $1::regclass AND attname = 'vector'
|
|
568
|
+
`, [quoteIdent(table)]);
|
|
569
|
+
const dimension = dimResult.rows.length > 0 ? Number(dimResult.rows[0].atttypmod) : 0;
|
|
570
|
+
return ok({
|
|
571
|
+
name,
|
|
572
|
+
dimension,
|
|
573
|
+
metric: config?.metric ?? "cosine",
|
|
574
|
+
count
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
async list() {
|
|
578
|
+
const prefix = config?.tablePrefix ?? "vec_";
|
|
579
|
+
logger2.debug("Listing collections");
|
|
580
|
+
const result = await pool.query(
|
|
581
|
+
`SELECT table_name FROM information_schema.tables WHERE table_name LIKE $1 AND table_schema = 'public'`,
|
|
582
|
+
[`${prefix}%`]
|
|
583
|
+
);
|
|
584
|
+
const names = result.rows.map(
|
|
585
|
+
(row) => String(row.table_name).slice(prefix.length)
|
|
586
|
+
);
|
|
587
|
+
return ok(names);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
const vectorDriver = {
|
|
591
|
+
async insert(collection, documents) {
|
|
592
|
+
const table = tableName(collection);
|
|
593
|
+
logger2.debug("Inserting vectors", { collection, count: documents.length });
|
|
594
|
+
const qi = quoteIdent(table);
|
|
595
|
+
const values = [];
|
|
596
|
+
const params = [];
|
|
597
|
+
for (let i = 0; i < documents.length; i++) {
|
|
598
|
+
const base = i * 4;
|
|
599
|
+
values.push(`($${base + 1}, $${base + 2}::vector, $${base + 3}, $${base + 4}::jsonb)`);
|
|
600
|
+
const doc = documents[i];
|
|
601
|
+
params.push(doc.id, `[${doc.vector.join(",")}]`, doc.content ?? "", JSON.stringify(doc.metadata ?? {}));
|
|
602
|
+
}
|
|
603
|
+
await pool.query(
|
|
604
|
+
`INSERT INTO ${qi} (id, vector, content, metadata) VALUES ${values.join(", ")}`,
|
|
605
|
+
params
|
|
606
|
+
);
|
|
607
|
+
logger2.info("Vectors inserted", { collection, count: documents.length });
|
|
608
|
+
return ok(void 0);
|
|
609
|
+
},
|
|
610
|
+
async upsert(collection, documents) {
|
|
611
|
+
const table = tableName(collection);
|
|
612
|
+
logger2.debug("Upserting vectors", { collection, count: documents.length });
|
|
613
|
+
const qi = quoteIdent(table);
|
|
614
|
+
const values = [];
|
|
615
|
+
const params = [];
|
|
616
|
+
for (let i = 0; i < documents.length; i++) {
|
|
617
|
+
const base = i * 4;
|
|
618
|
+
values.push(`($${base + 1}, $${base + 2}::vector, $${base + 3}, $${base + 4}::jsonb)`);
|
|
619
|
+
const doc = documents[i];
|
|
620
|
+
params.push(doc.id, `[${doc.vector.join(",")}]`, doc.content ?? "", JSON.stringify(doc.metadata ?? {}));
|
|
621
|
+
}
|
|
622
|
+
await pool.query(
|
|
623
|
+
`INSERT INTO ${qi} (id, vector, content, metadata) VALUES ${values.join(", ")}
|
|
624
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
625
|
+
vector = EXCLUDED.vector,
|
|
626
|
+
content = EXCLUDED.content,
|
|
627
|
+
metadata = EXCLUDED.metadata`,
|
|
628
|
+
params
|
|
629
|
+
);
|
|
630
|
+
logger2.info("Vectors upserted", { collection, count: documents.length });
|
|
631
|
+
return ok(void 0);
|
|
632
|
+
},
|
|
633
|
+
async delete(collection, ids) {
|
|
634
|
+
const table = tableName(collection);
|
|
635
|
+
logger2.debug("Deleting vectors", { collection, count: ids.length });
|
|
636
|
+
const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ");
|
|
637
|
+
await pool.query(`DELETE FROM ${quoteIdent(table)} WHERE id IN (${placeholders})`, ids);
|
|
638
|
+
logger2.info("Vectors deleted", { collection, count: ids.length });
|
|
639
|
+
return ok(void 0);
|
|
640
|
+
},
|
|
641
|
+
async search(collection, vector, options) {
|
|
642
|
+
const table = tableName(collection);
|
|
643
|
+
const topK = options?.topK ?? 10;
|
|
644
|
+
const minScore = options?.minScore ?? 0;
|
|
645
|
+
const op = distanceOp();
|
|
646
|
+
logger2.debug("Searching vectors", { collection, topK, hasFilter: !!options?.filter });
|
|
647
|
+
const vectorStr = `[${vector.join(",")}]`;
|
|
648
|
+
let filterSQL = "";
|
|
649
|
+
const filterParams = [vectorStr, topK];
|
|
650
|
+
let paramIndex = 3;
|
|
651
|
+
if (options?.filter) {
|
|
652
|
+
const filterParts = [];
|
|
653
|
+
for (const [key, value] of Object.entries(options.filter)) {
|
|
654
|
+
filterParts.push(`metadata->>$${paramIndex} = $${paramIndex + 1}`);
|
|
655
|
+
filterParams.push(key, String(value));
|
|
656
|
+
paramIndex += 2;
|
|
657
|
+
}
|
|
658
|
+
if (filterParts.length > 0) {
|
|
659
|
+
filterSQL = `WHERE ${filterParts.join(" AND ")}`;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const result = await pool.query(
|
|
663
|
+
`SELECT id, content, metadata, vector ${op} $1::vector AS distance
|
|
664
|
+
FROM ${quoteIdent(table)}
|
|
665
|
+
${filterSQL}
|
|
666
|
+
ORDER BY vector ${op} $1::vector
|
|
667
|
+
LIMIT $2`,
|
|
668
|
+
filterParams
|
|
669
|
+
);
|
|
670
|
+
const searchResults = result.rows.map((row) => {
|
|
671
|
+
const score = distanceToScore(row.distance);
|
|
672
|
+
return {
|
|
673
|
+
id: row.id,
|
|
674
|
+
score,
|
|
675
|
+
content: row.content || void 0,
|
|
676
|
+
metadata: row.metadata
|
|
677
|
+
};
|
|
678
|
+
}).filter((r) => r.score >= minScore);
|
|
679
|
+
return ok(searchResults);
|
|
680
|
+
},
|
|
681
|
+
async count(collection) {
|
|
682
|
+
const table = tableName(collection);
|
|
683
|
+
logger2.debug("Counting vectors", { collection });
|
|
684
|
+
const result = await pool.query(`SELECT COUNT(*) as count FROM ${quoteIdent(table)}`);
|
|
685
|
+
return ok(Number.parseInt(String(result.rows[0].count), 10));
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
return {
|
|
689
|
+
name: "pgvector",
|
|
690
|
+
async connect(cfg) {
|
|
691
|
+
if (cfg.type !== "pgvector") {
|
|
692
|
+
return err(HaiVecdbError.UNSUPPORTED_TYPE, vecdbM("vecdb_unsupportedType", { params: { type: cfg.type } }));
|
|
693
|
+
}
|
|
694
|
+
const pgConfig = cfg;
|
|
695
|
+
try {
|
|
696
|
+
const { Pool } = await import('pg');
|
|
697
|
+
const poolConfig = pgConfig.url ? { connectionString: pgConfig.url } : {
|
|
698
|
+
host: pgConfig.host,
|
|
699
|
+
port: pgConfig.port,
|
|
700
|
+
database: pgConfig.database,
|
|
701
|
+
user: pgConfig.user,
|
|
702
|
+
password: pgConfig.password
|
|
703
|
+
};
|
|
704
|
+
pool = new Pool(poolConfig);
|
|
705
|
+
await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
706
|
+
config = pgConfig;
|
|
707
|
+
logger2.info("pgvector connected", { host: pgConfig.host, database: pgConfig.database });
|
|
708
|
+
return ok(void 0);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
logger2.error("Failed to connect to pgvector", { error: error instanceof Error ? error.message : String(error) });
|
|
711
|
+
return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_connectionFailed", { params: { error: error instanceof Error ? error.message : String(error) } }), error);
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
async close() {
|
|
715
|
+
try {
|
|
716
|
+
if (pool) {
|
|
717
|
+
await pool.end();
|
|
718
|
+
}
|
|
719
|
+
pool = null;
|
|
720
|
+
config = null;
|
|
721
|
+
logger2.info("pgvector connection closed");
|
|
722
|
+
return ok(void 0);
|
|
723
|
+
} catch (error) {
|
|
724
|
+
logger2.error("Failed to close pgvector connection", { error: error instanceof Error ? error.message : String(error) });
|
|
725
|
+
return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_closeFailed", { params: { error: String(error) } }), error);
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
isConnected() {
|
|
729
|
+
return pool !== null;
|
|
730
|
+
},
|
|
731
|
+
collection: createBaseCollectionOps(ctx, collectionDriver),
|
|
732
|
+
vector: createBaseVectorOps(ctx, vectorDriver)
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
var logger3 = core.logger.child({ module: "vecdb", scope: "qdrant" });
|
|
736
|
+
function hashToUuid(id) {
|
|
737
|
+
const hash = createHash("sha256").update(id).digest("hex");
|
|
738
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
|
|
739
|
+
}
|
|
740
|
+
function toQdrantDistance(metric) {
|
|
741
|
+
switch (metric) {
|
|
742
|
+
case "cosine":
|
|
743
|
+
return "Cosine";
|
|
744
|
+
case "euclidean":
|
|
745
|
+
return "Euclid";
|
|
746
|
+
case "dot":
|
|
747
|
+
return "Dot";
|
|
748
|
+
default:
|
|
749
|
+
return "Cosine";
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function createQdrantProvider() {
|
|
753
|
+
let client = null;
|
|
754
|
+
let config = null;
|
|
755
|
+
const ctx = { isConnected: () => client !== null, logger: logger3 };
|
|
756
|
+
const collectionDriver = {
|
|
757
|
+
async create(name, options) {
|
|
758
|
+
logger3.debug("Creating collection", { name, dimension: options.dimension, metric: options.metric });
|
|
759
|
+
const collections = await client.getCollections();
|
|
760
|
+
const exists = collections.collections.some((c) => c.name === name);
|
|
761
|
+
if (exists) {
|
|
762
|
+
return err(HaiVecdbError.COLLECTION_ALREADY_EXISTS, vecdbM("vecdb_collectionAlreadyExists", { params: { name } }));
|
|
763
|
+
}
|
|
764
|
+
const metric = options.metric ?? config?.metric ?? "cosine";
|
|
765
|
+
await client.createCollection(name, {
|
|
766
|
+
vectors: {
|
|
767
|
+
size: options.dimension,
|
|
768
|
+
distance: toQdrantDistance(metric)
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
logger3.info("Collection created", { name, dimension: options.dimension, metric });
|
|
772
|
+
return ok(void 0);
|
|
773
|
+
},
|
|
774
|
+
async drop(name) {
|
|
775
|
+
logger3.debug("Dropping collection", { name });
|
|
776
|
+
try {
|
|
777
|
+
await client.getCollection(name);
|
|
778
|
+
} catch {
|
|
779
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
|
|
780
|
+
}
|
|
781
|
+
await client.deleteCollection(name);
|
|
782
|
+
logger3.info("Collection dropped", { name });
|
|
783
|
+
return ok(void 0);
|
|
784
|
+
},
|
|
785
|
+
async exists(name) {
|
|
786
|
+
logger3.debug("Checking collection exists", { name });
|
|
787
|
+
try {
|
|
788
|
+
await client.getCollection(name);
|
|
789
|
+
return ok(true);
|
|
790
|
+
} catch {
|
|
791
|
+
return ok(false);
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
async info(name) {
|
|
795
|
+
logger3.debug("Getting collection info", { name });
|
|
796
|
+
let collectionInfo;
|
|
797
|
+
try {
|
|
798
|
+
collectionInfo = await client.getCollection(name);
|
|
799
|
+
} catch {
|
|
800
|
+
return err(HaiVecdbError.COLLECTION_NOT_FOUND, vecdbM("vecdb_collectionNotFound", { params: { name } }));
|
|
801
|
+
}
|
|
802
|
+
const vectorsConfig = collectionInfo.config?.params?.vectors;
|
|
803
|
+
const dimension = vectorsConfig?.size ?? 0;
|
|
804
|
+
const qdrantDistance = vectorsConfig?.distance ?? "Cosine";
|
|
805
|
+
const metric = qdrantDistance === "Euclid" ? "euclidean" : qdrantDistance === "Dot" ? "dot" : "cosine";
|
|
806
|
+
return ok({
|
|
807
|
+
name,
|
|
808
|
+
dimension,
|
|
809
|
+
metric,
|
|
810
|
+
count: collectionInfo.points_count ?? 0
|
|
811
|
+
});
|
|
812
|
+
},
|
|
813
|
+
async list() {
|
|
814
|
+
logger3.debug("Listing collections");
|
|
815
|
+
const collections = await client.getCollections();
|
|
816
|
+
const names = collections.collections.map((c) => c.name);
|
|
817
|
+
return ok(names);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
const vectorDriver = {
|
|
821
|
+
async insert(collection, documents) {
|
|
822
|
+
logger3.debug("Inserting vectors", { collection, count: documents.length });
|
|
823
|
+
const points = documents.map((doc) => ({
|
|
824
|
+
id: hashToUuid(doc.id),
|
|
825
|
+
vector: doc.vector,
|
|
826
|
+
payload: {
|
|
827
|
+
_id: doc.id,
|
|
828
|
+
content: doc.content ?? "",
|
|
829
|
+
...doc.metadata
|
|
830
|
+
}
|
|
831
|
+
}));
|
|
832
|
+
await client.upsert(collection, { points });
|
|
833
|
+
logger3.info("Vectors inserted", { collection, count: documents.length });
|
|
834
|
+
return ok(void 0);
|
|
835
|
+
},
|
|
836
|
+
async upsert(collection, documents) {
|
|
837
|
+
logger3.debug("Upserting vectors", { collection, count: documents.length });
|
|
838
|
+
const points = documents.map((doc) => ({
|
|
839
|
+
id: hashToUuid(doc.id),
|
|
840
|
+
vector: doc.vector,
|
|
841
|
+
payload: {
|
|
842
|
+
_id: doc.id,
|
|
843
|
+
content: doc.content ?? "",
|
|
844
|
+
...doc.metadata
|
|
845
|
+
}
|
|
846
|
+
}));
|
|
847
|
+
await client.upsert(collection, { points });
|
|
848
|
+
logger3.info("Vectors upserted", { collection, count: documents.length });
|
|
849
|
+
return ok(void 0);
|
|
850
|
+
},
|
|
851
|
+
async delete(collection, ids) {
|
|
852
|
+
logger3.debug("Deleting vectors", { collection, count: ids.length });
|
|
853
|
+
const uuids = ids.map(hashToUuid);
|
|
854
|
+
await client.delete(collection, { points: uuids });
|
|
855
|
+
logger3.info("Vectors deleted", { collection, count: ids.length });
|
|
856
|
+
return ok(void 0);
|
|
857
|
+
},
|
|
858
|
+
async search(collection, vector, options) {
|
|
859
|
+
const topK = options?.topK ?? 10;
|
|
860
|
+
const minScore = options?.minScore ?? 0;
|
|
861
|
+
logger3.debug("Searching vectors", { collection, topK, hasFilter: !!options?.filter });
|
|
862
|
+
let filter;
|
|
863
|
+
if (options?.filter && Object.keys(options.filter).length > 0) {
|
|
864
|
+
const mustConditions = Object.entries(options.filter).map(([key, value]) => ({
|
|
865
|
+
key,
|
|
866
|
+
match: { value }
|
|
867
|
+
}));
|
|
868
|
+
filter = { must: mustConditions };
|
|
869
|
+
}
|
|
870
|
+
const searchResult = await client.search(collection, {
|
|
871
|
+
vector,
|
|
872
|
+
limit: topK,
|
|
873
|
+
score_threshold: minScore > 0 ? minScore : void 0,
|
|
874
|
+
filter,
|
|
875
|
+
with_payload: true
|
|
876
|
+
});
|
|
877
|
+
const results = searchResult.map((point) => {
|
|
878
|
+
const payload = point.payload ?? {};
|
|
879
|
+
const { _id, content, ...metadata } = payload;
|
|
880
|
+
return {
|
|
881
|
+
id: _id ?? "",
|
|
882
|
+
score: point.score,
|
|
883
|
+
content: content || void 0,
|
|
884
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : void 0
|
|
885
|
+
};
|
|
886
|
+
});
|
|
887
|
+
return ok(results);
|
|
888
|
+
},
|
|
889
|
+
async count(collection) {
|
|
890
|
+
logger3.debug("Counting vectors", { collection });
|
|
891
|
+
const info = await client.getCollection(collection);
|
|
892
|
+
return ok(info.points_count ?? 0);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
return {
|
|
896
|
+
name: "qdrant",
|
|
897
|
+
async connect(cfg) {
|
|
898
|
+
if (cfg.type !== "qdrant") {
|
|
899
|
+
return err(HaiVecdbError.UNSUPPORTED_TYPE, vecdbM("vecdb_unsupportedType", { params: { type: cfg.type } }));
|
|
900
|
+
}
|
|
901
|
+
const qdrantConfig = cfg;
|
|
902
|
+
try {
|
|
903
|
+
const { QdrantClient: QdrantClientClass } = await import('@qdrant/js-client-rest');
|
|
904
|
+
const qdrantClient = new QdrantClientClass({
|
|
905
|
+
url: qdrantConfig.url,
|
|
906
|
+
apiKey: qdrantConfig.apiKey
|
|
907
|
+
});
|
|
908
|
+
await qdrantClient.getCollections();
|
|
909
|
+
client = qdrantClient;
|
|
910
|
+
config = qdrantConfig;
|
|
911
|
+
logger3.info("Qdrant connected", { url: qdrantConfig.url });
|
|
912
|
+
return ok(void 0);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
logger3.error("Failed to connect to Qdrant", { error: error instanceof Error ? error.message : String(error) });
|
|
915
|
+
return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_connectionFailed", { params: { error: String(error) } }), error);
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
async close() {
|
|
919
|
+
client = null;
|
|
920
|
+
config = null;
|
|
921
|
+
logger3.info("Qdrant connection closed");
|
|
922
|
+
return ok(void 0);
|
|
923
|
+
},
|
|
924
|
+
isConnected() {
|
|
925
|
+
return client !== null;
|
|
926
|
+
},
|
|
927
|
+
collection: createBaseCollectionOps(ctx, collectionDriver),
|
|
928
|
+
vector: createBaseVectorOps(ctx, vectorDriver)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/vecdb-main.ts
|
|
933
|
+
var logger4 = core.logger.child({ module: "vecdb", scope: "main" });
|
|
934
|
+
var currentProvider = null;
|
|
935
|
+
var currentConfig = null;
|
|
936
|
+
var initInProgress = false;
|
|
937
|
+
function createProvider(config) {
|
|
938
|
+
switch (config.type) {
|
|
939
|
+
case "lancedb":
|
|
940
|
+
return createLancedbProvider();
|
|
941
|
+
case "pgvector":
|
|
942
|
+
return createPgvectorProvider();
|
|
943
|
+
case "qdrant":
|
|
944
|
+
return createQdrantProvider();
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
var notInitialized = core.module.createNotInitializedKit(
|
|
948
|
+
HaiVecdbError.NOT_INITIALIZED,
|
|
949
|
+
() => vecdbM("vecdb_notInitialized")
|
|
950
|
+
);
|
|
951
|
+
var notInitializedCollection = notInitialized.proxy();
|
|
952
|
+
var notInitializedVector = notInitialized.proxy();
|
|
953
|
+
var vecdb = {
|
|
954
|
+
/**
|
|
955
|
+
* 初始化向量数据库连接
|
|
956
|
+
*
|
|
957
|
+
* @param config - 向量数据库配置(允许部分字段,内部会补齐默认值)
|
|
958
|
+
* @returns 初始化结果,失败时包含错误信息
|
|
959
|
+
*/
|
|
960
|
+
async init(config) {
|
|
961
|
+
if (initInProgress) {
|
|
962
|
+
logger4.warn("Vecdb init already in progress, skipping concurrent call");
|
|
963
|
+
return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_initInProgress"));
|
|
964
|
+
}
|
|
965
|
+
initInProgress = true;
|
|
966
|
+
try {
|
|
967
|
+
if (currentProvider) {
|
|
968
|
+
logger4.warn("Vecdb module is already initialized, reinitializing");
|
|
969
|
+
await vecdb.close();
|
|
970
|
+
}
|
|
971
|
+
logger4.info("Initializing vecdb module");
|
|
972
|
+
const parseResult = VecdbConfigSchema.safeParse(config);
|
|
973
|
+
if (!parseResult.success) {
|
|
974
|
+
logger4.error("Vecdb config validation failed", { error: parseResult.error.message });
|
|
975
|
+
return err(HaiVecdbError.CONFIG_ERROR, vecdbM("vecdb_configError", { params: { error: parseResult.error.message } }), parseResult.error);
|
|
976
|
+
}
|
|
977
|
+
const parsed = parseResult.data;
|
|
978
|
+
try {
|
|
979
|
+
const provider = createProvider(parsed);
|
|
980
|
+
const connectResult = await provider.connect(parsed);
|
|
981
|
+
if (!connectResult.success) {
|
|
982
|
+
logger4.error("Vecdb module initialization failed", {
|
|
983
|
+
code: connectResult.error.code,
|
|
984
|
+
message: connectResult.error.message
|
|
985
|
+
});
|
|
986
|
+
return connectResult;
|
|
987
|
+
}
|
|
988
|
+
currentProvider = provider;
|
|
989
|
+
currentConfig = parsed;
|
|
990
|
+
logger4.info("Vecdb module initialized", { type: parsed.type });
|
|
991
|
+
return ok(void 0);
|
|
992
|
+
} catch (error) {
|
|
993
|
+
logger4.error("Vecdb module initialization failed", { error });
|
|
994
|
+
return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_initFailed", { params: { error: error instanceof Error ? error.message : String(error) } }), error);
|
|
995
|
+
}
|
|
996
|
+
} finally {
|
|
997
|
+
initInProgress = false;
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
/**
|
|
1001
|
+
* 获取集合管理操作接口
|
|
1002
|
+
*
|
|
1003
|
+
* 未初始化时返回占位对象(所有调用返回 NOT_INITIALIZED)。
|
|
1004
|
+
*/
|
|
1005
|
+
get collection() {
|
|
1006
|
+
return currentProvider?.collection ?? notInitializedCollection;
|
|
1007
|
+
},
|
|
1008
|
+
/**
|
|
1009
|
+
* 获取向量操作接口
|
|
1010
|
+
*
|
|
1011
|
+
* 未初始化时返回占位对象(所有调用返回 NOT_INITIALIZED)。
|
|
1012
|
+
*/
|
|
1013
|
+
get vector() {
|
|
1014
|
+
return currentProvider?.vector ?? notInitializedVector;
|
|
1015
|
+
},
|
|
1016
|
+
/** 获取当前配置(未初始化时为 null) */
|
|
1017
|
+
get config() {
|
|
1018
|
+
return currentConfig;
|
|
1019
|
+
},
|
|
1020
|
+
/** 检查是否已初始化 */
|
|
1021
|
+
get isInitialized() {
|
|
1022
|
+
return currentProvider !== null && currentProvider.isConnected();
|
|
1023
|
+
},
|
|
1024
|
+
/**
|
|
1025
|
+
* 关闭向量数据库连接
|
|
1026
|
+
*
|
|
1027
|
+
* 多次调用安全,未初始化时直接返回。
|
|
1028
|
+
*/
|
|
1029
|
+
async close() {
|
|
1030
|
+
if (!currentProvider) {
|
|
1031
|
+
currentConfig = null;
|
|
1032
|
+
logger4.info("Vecdb module already closed, skipping");
|
|
1033
|
+
return ok(void 0);
|
|
1034
|
+
}
|
|
1035
|
+
logger4.info("Closing vecdb module");
|
|
1036
|
+
try {
|
|
1037
|
+
const closeResult = await currentProvider.close();
|
|
1038
|
+
if (!closeResult.success) {
|
|
1039
|
+
logger4.error("Vecdb module close failed", { code: closeResult.error.code, message: closeResult.error.message });
|
|
1040
|
+
return closeResult;
|
|
1041
|
+
}
|
|
1042
|
+
logger4.info("Vecdb module closed");
|
|
1043
|
+
return ok(void 0);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
logger4.error("Vecdb module close failed", { error });
|
|
1046
|
+
return err(HaiVecdbError.CONNECTION_FAILED, vecdbM("vecdb_closeFailed", { params: { error: error instanceof Error ? error.message : String(error) } }), error);
|
|
1047
|
+
} finally {
|
|
1048
|
+
currentProvider = null;
|
|
1049
|
+
currentConfig = null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
export { DistanceMetricSchema, HaiVecdbError, LancedbConfigSchema, PgvectorConfigSchema, PgvectorIndexTypeSchema, QdrantConfigSchema, VecdbConfigSchema, VecdbTypeSchema, vecdb };
|
|
1055
|
+
//# sourceMappingURL=index.js.map
|
|
1056
|
+
//# sourceMappingURL=index.js.map
|