@wiscale/velesdb-sdk 1.3.0 → 1.5.1
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/README.md +157 -3
- package/dist/index.d.mts +386 -11
- package/dist/index.d.ts +386 -11
- package/dist/index.js +899 -60
- package/dist/index.mjs +895 -59
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -25,6 +25,12 @@ var NotFoundError = class extends VelesDBError {
|
|
|
25
25
|
this.name = "NotFoundError";
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
+
var BackpressureError = class extends VelesDBError {
|
|
29
|
+
constructor(message = "Server backpressure: too many requests") {
|
|
30
|
+
super(message, "BACKPRESSURE");
|
|
31
|
+
this.name = "BackpressureError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
28
34
|
|
|
29
35
|
// src/backends/wasm.ts
|
|
30
36
|
var WasmBackend = class {
|
|
@@ -56,6 +62,33 @@ var WasmBackend = class {
|
|
|
56
62
|
throw new ConnectionError("WASM backend not initialized");
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
normalizeIdString(id) {
|
|
66
|
+
const trimmed = id.trim();
|
|
67
|
+
return /^\d+$/.test(trimmed) ? trimmed : null;
|
|
68
|
+
}
|
|
69
|
+
canonicalPayloadKeyFromResultId(id) {
|
|
70
|
+
if (typeof id === "bigint") {
|
|
71
|
+
return id.toString();
|
|
72
|
+
}
|
|
73
|
+
if (typeof id === "number") {
|
|
74
|
+
return String(Math.trunc(id));
|
|
75
|
+
}
|
|
76
|
+
const normalized = this.normalizeIdString(id);
|
|
77
|
+
if (normalized !== null) {
|
|
78
|
+
return normalized.replace(/^0+(?=\d)/, "");
|
|
79
|
+
}
|
|
80
|
+
return String(this.toNumericId(id));
|
|
81
|
+
}
|
|
82
|
+
canonicalPayloadKey(id) {
|
|
83
|
+
if (typeof id === "number") {
|
|
84
|
+
return String(Math.trunc(id));
|
|
85
|
+
}
|
|
86
|
+
const normalized = this.normalizeIdString(id);
|
|
87
|
+
if (normalized !== null) {
|
|
88
|
+
return normalized.replace(/^0+(?=\d)/, "");
|
|
89
|
+
}
|
|
90
|
+
return String(this.toNumericId(id));
|
|
91
|
+
}
|
|
59
92
|
async createCollection(name, config) {
|
|
60
93
|
this.ensureInitialized();
|
|
61
94
|
if (this.collections.has(name)) {
|
|
@@ -121,9 +154,13 @@ var WasmBackend = class {
|
|
|
121
154
|
"DIMENSION_MISMATCH"
|
|
122
155
|
);
|
|
123
156
|
}
|
|
124
|
-
collection.store.insert(BigInt(id), vector);
|
|
125
157
|
if (doc.payload) {
|
|
126
|
-
collection.
|
|
158
|
+
collection.store.insert_with_payload(BigInt(id), vector, doc.payload);
|
|
159
|
+
} else {
|
|
160
|
+
collection.store.insert(BigInt(id), vector);
|
|
161
|
+
}
|
|
162
|
+
if (doc.payload) {
|
|
163
|
+
collection.payloads.set(this.canonicalPayloadKey(doc.id), doc.payload);
|
|
127
164
|
}
|
|
128
165
|
}
|
|
129
166
|
async insertBatch(collectionName, docs) {
|
|
@@ -133,7 +170,7 @@ var WasmBackend = class {
|
|
|
133
170
|
throw new NotFoundError(`Collection '${collectionName}'`);
|
|
134
171
|
}
|
|
135
172
|
for (const doc of docs) {
|
|
136
|
-
const vectorLen =
|
|
173
|
+
const vectorLen = doc.vector.length;
|
|
137
174
|
if (vectorLen !== collection.config.dimension) {
|
|
138
175
|
throw new VelesDBError(
|
|
139
176
|
`Vector dimension mismatch for doc ${doc.id}: expected ${collection.config.dimension}, got ${vectorLen}`,
|
|
@@ -142,14 +179,22 @@ var WasmBackend = class {
|
|
|
142
179
|
}
|
|
143
180
|
}
|
|
144
181
|
collection.store.reserve(docs.length);
|
|
145
|
-
const batch =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
182
|
+
const batch = [];
|
|
183
|
+
for (const doc of docs) {
|
|
184
|
+
const id = BigInt(this.toNumericId(doc.id));
|
|
185
|
+
const vector = doc.vector instanceof Float32Array ? doc.vector : new Float32Array(doc.vector);
|
|
186
|
+
if (doc.payload) {
|
|
187
|
+
collection.store.insert_with_payload(id, vector, doc.payload);
|
|
188
|
+
} else {
|
|
189
|
+
batch.push([id, Array.from(vector)]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (batch.length > 0) {
|
|
193
|
+
collection.store.insert_batch(batch);
|
|
194
|
+
}
|
|
150
195
|
for (const doc of docs) {
|
|
151
196
|
if (doc.payload) {
|
|
152
|
-
collection.payloads.set(
|
|
197
|
+
collection.payloads.set(this.canonicalPayloadKey(doc.id), doc.payload);
|
|
153
198
|
}
|
|
154
199
|
}
|
|
155
200
|
}
|
|
@@ -167,12 +212,43 @@ var WasmBackend = class {
|
|
|
167
212
|
);
|
|
168
213
|
}
|
|
169
214
|
const k = options?.k ?? 10;
|
|
215
|
+
if (options?.sparseVector) {
|
|
216
|
+
const { indices, values } = this.sparseVectorToArrays(options.sparseVector);
|
|
217
|
+
if (queryVector.length > 0 && collection.config.dimension && collection.config.dimension > 0) {
|
|
218
|
+
const denseResults = collection.store.search(queryVector, k);
|
|
219
|
+
const sparseResults = collection.store.sparse_search(
|
|
220
|
+
new Uint32Array(indices),
|
|
221
|
+
new Float32Array(values),
|
|
222
|
+
k
|
|
223
|
+
);
|
|
224
|
+
const sparseArray = sparseResults;
|
|
225
|
+
const denseForFuse = denseResults.map(([id, score]) => [Number(id), score]);
|
|
226
|
+
const sparseForFuse = sparseArray.map((r) => [Number(r.doc_id), r.score]);
|
|
227
|
+
const fused = this.wasmModule.hybrid_search_fuse(denseForFuse, sparseForFuse, 60);
|
|
228
|
+
return fused.slice(0, k).map((r) => ({
|
|
229
|
+
id: String(r.doc_id),
|
|
230
|
+
score: r.score,
|
|
231
|
+
payload: collection.payloads.get(this.canonicalPayloadKeyFromResultId(r.doc_id))
|
|
232
|
+
}));
|
|
233
|
+
} else {
|
|
234
|
+
const sparseResults = collection.store.sparse_search(
|
|
235
|
+
new Uint32Array(indices),
|
|
236
|
+
new Float32Array(values),
|
|
237
|
+
k
|
|
238
|
+
);
|
|
239
|
+
return sparseResults.map((r) => ({
|
|
240
|
+
id: String(r.doc_id),
|
|
241
|
+
score: r.score,
|
|
242
|
+
payload: collection.payloads.get(this.canonicalPayloadKeyFromResultId(r.doc_id))
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
170
246
|
if (options?.filter) {
|
|
171
247
|
const results = collection.store.search_with_filter(queryVector, k, options.filter);
|
|
172
248
|
return results.map((r) => ({
|
|
173
249
|
id: String(r.id),
|
|
174
250
|
score: r.score,
|
|
175
|
-
payload: r.payload || collection.payloads.get(
|
|
251
|
+
payload: r.payload || collection.payloads.get(this.canonicalPayloadKeyFromResultId(r.id))
|
|
176
252
|
}));
|
|
177
253
|
}
|
|
178
254
|
const rawResults = collection.store.search(queryVector, k);
|
|
@@ -182,7 +258,7 @@ var WasmBackend = class {
|
|
|
182
258
|
id: stringId,
|
|
183
259
|
score
|
|
184
260
|
};
|
|
185
|
-
const payload = collection.payloads.get(
|
|
261
|
+
const payload = collection.payloads.get(this.canonicalPayloadKeyFromResultId(id));
|
|
186
262
|
if (payload) {
|
|
187
263
|
result.payload = payload;
|
|
188
264
|
}
|
|
@@ -206,7 +282,7 @@ var WasmBackend = class {
|
|
|
206
282
|
const numericId = this.toNumericId(id);
|
|
207
283
|
const removed = collection.store.remove(BigInt(numericId));
|
|
208
284
|
if (removed) {
|
|
209
|
-
collection.payloads.delete(
|
|
285
|
+
collection.payloads.delete(this.canonicalPayloadKey(id));
|
|
210
286
|
}
|
|
211
287
|
return removed;
|
|
212
288
|
}
|
|
@@ -216,38 +292,140 @@ var WasmBackend = class {
|
|
|
216
292
|
if (!collection) {
|
|
217
293
|
throw new NotFoundError(`Collection '${collectionName}'`);
|
|
218
294
|
}
|
|
219
|
-
const
|
|
220
|
-
|
|
295
|
+
const numericId = this.toNumericId(id);
|
|
296
|
+
const point = collection.store.get(BigInt(numericId));
|
|
297
|
+
if (!point) {
|
|
221
298
|
return null;
|
|
222
299
|
}
|
|
300
|
+
const payload = point.payload ?? collection.payloads.get(this.canonicalPayloadKey(numericId));
|
|
223
301
|
return {
|
|
224
|
-
id,
|
|
225
|
-
vector:
|
|
226
|
-
// Not available in current WASM impl
|
|
302
|
+
id: String(point.id),
|
|
303
|
+
vector: Array.isArray(point.vector) ? point.vector : Array.from(point.vector),
|
|
227
304
|
payload
|
|
228
305
|
};
|
|
229
306
|
}
|
|
230
307
|
async textSearch(_collection, _query, _options) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
308
|
+
this.ensureInitialized();
|
|
309
|
+
const collection = this.collections.get(_collection);
|
|
310
|
+
if (!collection) {
|
|
311
|
+
throw new NotFoundError(`Collection '${_collection}'`);
|
|
312
|
+
}
|
|
313
|
+
const k = _options?.k ?? 10;
|
|
314
|
+
const field = void 0;
|
|
315
|
+
const raw = collection.store.text_search(_query, k, field);
|
|
316
|
+
return raw.map((r) => {
|
|
317
|
+
if (Array.isArray(r)) {
|
|
318
|
+
const key2 = this.canonicalPayloadKeyFromResultId(r[0]);
|
|
319
|
+
return { id: String(r[0]), score: r[1], payload: collection.payloads.get(key2) };
|
|
320
|
+
}
|
|
321
|
+
const key = this.canonicalPayloadKeyFromResultId(r.id);
|
|
322
|
+
return { id: String(r.id), score: r.score, payload: r.payload ?? collection.payloads.get(key) };
|
|
323
|
+
});
|
|
235
324
|
}
|
|
236
325
|
async hybridSearch(_collection, _vector, _textQuery, _options) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
326
|
+
this.ensureInitialized();
|
|
327
|
+
const collection = this.collections.get(_collection);
|
|
328
|
+
if (!collection) {
|
|
329
|
+
throw new NotFoundError(`Collection '${_collection}'`);
|
|
330
|
+
}
|
|
331
|
+
const queryVector = _vector instanceof Float32Array ? _vector : new Float32Array(_vector);
|
|
332
|
+
const k = _options?.k ?? 10;
|
|
333
|
+
const vectorWeight = _options?.vectorWeight ?? 0.5;
|
|
334
|
+
const raw = collection.store.hybrid_search(queryVector, _textQuery, k, vectorWeight);
|
|
335
|
+
return raw.map((r) => {
|
|
336
|
+
const key = this.canonicalPayloadKeyFromResultId(r.id);
|
|
337
|
+
return {
|
|
338
|
+
id: String(r.id),
|
|
339
|
+
score: r.score,
|
|
340
|
+
payload: r.payload ?? collection.payloads.get(key)
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async query(_collection, _queryString, _params, _options) {
|
|
345
|
+
this.ensureInitialized();
|
|
346
|
+
const collection = this.collections.get(_collection);
|
|
347
|
+
if (!collection) {
|
|
348
|
+
throw new NotFoundError(`Collection '${_collection}'`);
|
|
349
|
+
}
|
|
350
|
+
const paramsVector = _params?.q;
|
|
351
|
+
if (!Array.isArray(paramsVector) && !(paramsVector instanceof Float32Array)) {
|
|
352
|
+
throw new VelesDBError(
|
|
353
|
+
"WASM query() expects params.q to contain the query embedding vector.",
|
|
354
|
+
"BAD_REQUEST"
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const requestedK = _params?.k;
|
|
358
|
+
const k = typeof requestedK === "number" && Number.isInteger(requestedK) && requestedK > 0 ? requestedK : 10;
|
|
359
|
+
const raw = collection.store.query(
|
|
360
|
+
paramsVector instanceof Float32Array ? paramsVector : new Float32Array(paramsVector),
|
|
361
|
+
k
|
|
240
362
|
);
|
|
363
|
+
return {
|
|
364
|
+
results: raw.map((r) => ({
|
|
365
|
+
nodeId: r.nodeId ?? r.node_id,
|
|
366
|
+
vectorScore: r.vectorScore ?? r.vector_score ?? null,
|
|
367
|
+
graphScore: r.graphScore ?? r.graph_score ?? null,
|
|
368
|
+
fusedScore: r.fusedScore ?? r.fused_score ?? 0,
|
|
369
|
+
bindings: r.bindings ?? {},
|
|
370
|
+
columnData: r.columnData ?? r.column_data ?? null
|
|
371
|
+
})),
|
|
372
|
+
stats: {
|
|
373
|
+
executionTimeMs: 0,
|
|
374
|
+
strategy: "wasm-query",
|
|
375
|
+
scannedNodes: raw.length
|
|
376
|
+
}
|
|
377
|
+
};
|
|
241
378
|
}
|
|
242
|
-
async
|
|
379
|
+
async multiQuerySearch(_collection, _vectors, _options) {
|
|
380
|
+
this.ensureInitialized();
|
|
381
|
+
const collection = this.collections.get(_collection);
|
|
382
|
+
if (!collection) {
|
|
383
|
+
throw new NotFoundError(`Collection '${_collection}'`);
|
|
384
|
+
}
|
|
385
|
+
if (_vectors.length === 0) {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
const numVectors = _vectors.length;
|
|
389
|
+
const dimension = collection.config.dimension ?? 0;
|
|
390
|
+
const flat = new Float32Array(numVectors * dimension);
|
|
391
|
+
_vectors.forEach((vector, idx) => {
|
|
392
|
+
const src = vector instanceof Float32Array ? vector : new Float32Array(vector);
|
|
393
|
+
flat.set(src, idx * dimension);
|
|
394
|
+
});
|
|
395
|
+
const strategy = _options?.fusion ?? "rrf";
|
|
396
|
+
if (strategy === "weighted") {
|
|
397
|
+
throw new VelesDBError(
|
|
398
|
+
"Fusion strategy 'weighted' is not supported in WASM backend.",
|
|
399
|
+
"NOT_SUPPORTED"
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const raw = collection.store.multi_query_search(
|
|
403
|
+
flat,
|
|
404
|
+
numVectors,
|
|
405
|
+
_options?.k ?? 10,
|
|
406
|
+
strategy,
|
|
407
|
+
_options?.fusionParams?.k ?? 60
|
|
408
|
+
);
|
|
409
|
+
return raw.map((r) => {
|
|
410
|
+
if (Array.isArray(r)) {
|
|
411
|
+
const key2 = this.canonicalPayloadKeyFromResultId(r[0]);
|
|
412
|
+
return { id: String(r[0]), score: r[1], payload: collection.payloads.get(key2) };
|
|
413
|
+
}
|
|
414
|
+
const key = this.canonicalPayloadKeyFromResultId(r.id);
|
|
415
|
+
return { id: String(r.id), score: r.score, payload: r.payload ?? collection.payloads.get(key) };
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async queryExplain(_queryString, _params) {
|
|
419
|
+
this.ensureInitialized();
|
|
243
420
|
throw new VelesDBError(
|
|
244
|
-
"
|
|
421
|
+
"Query explain is not supported in WASM backend. Use REST backend for EXPLAIN support.",
|
|
245
422
|
"NOT_SUPPORTED"
|
|
246
423
|
);
|
|
247
424
|
}
|
|
248
|
-
async
|
|
425
|
+
async collectionSanity(_collection) {
|
|
426
|
+
this.ensureInitialized();
|
|
249
427
|
throw new VelesDBError(
|
|
250
|
-
"
|
|
428
|
+
"Collection sanity endpoint is not supported in WASM backend. Use REST backend for diagnostics.",
|
|
251
429
|
"NOT_SUPPORTED"
|
|
252
430
|
);
|
|
253
431
|
}
|
|
@@ -273,13 +451,25 @@ var WasmBackend = class {
|
|
|
273
451
|
this.collections.clear();
|
|
274
452
|
this._initialized = false;
|
|
275
453
|
}
|
|
454
|
+
sparseVectorToArrays(sv) {
|
|
455
|
+
const indices = [];
|
|
456
|
+
const values = [];
|
|
457
|
+
for (const [k, v] of Object.entries(sv)) {
|
|
458
|
+
indices.push(Number(k));
|
|
459
|
+
values.push(v);
|
|
460
|
+
}
|
|
461
|
+
return { indices, values };
|
|
462
|
+
}
|
|
276
463
|
toNumericId(id) {
|
|
277
464
|
if (typeof id === "number") {
|
|
278
465
|
return id;
|
|
279
466
|
}
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
|
|
467
|
+
const normalized = this.normalizeIdString(id);
|
|
468
|
+
if (normalized !== null) {
|
|
469
|
+
const parsed = Number(normalized);
|
|
470
|
+
if (Number.isSafeInteger(parsed)) {
|
|
471
|
+
return parsed;
|
|
472
|
+
}
|
|
283
473
|
}
|
|
284
474
|
let hash = 0;
|
|
285
475
|
for (let i = 0; i < id.length; i++) {
|
|
@@ -343,6 +533,23 @@ var WasmBackend = class {
|
|
|
343
533
|
"NOT_SUPPORTED"
|
|
344
534
|
);
|
|
345
535
|
}
|
|
536
|
+
// ========================================================================
|
|
537
|
+
// Sparse / PQ / Streaming (v1.5)
|
|
538
|
+
// ========================================================================
|
|
539
|
+
async trainPq(_collection, _options) {
|
|
540
|
+
this.ensureInitialized();
|
|
541
|
+
throw new VelesDBError(
|
|
542
|
+
"PQ training is not available in WASM mode. Use REST backend for PQ training.",
|
|
543
|
+
"NOT_SUPPORTED"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
async streamInsert(_collection, _docs) {
|
|
547
|
+
this.ensureInitialized();
|
|
548
|
+
throw new VelesDBError(
|
|
549
|
+
"Streaming insert is not available in WASM mode. Use REST backend for streaming.",
|
|
550
|
+
"NOT_SUPPORTED"
|
|
551
|
+
);
|
|
552
|
+
}
|
|
346
553
|
};
|
|
347
554
|
|
|
348
555
|
// src/backends/rest.ts
|
|
@@ -405,11 +612,52 @@ var RestBackend = class {
|
|
|
405
612
|
return {};
|
|
406
613
|
}
|
|
407
614
|
const payload = data;
|
|
408
|
-
const
|
|
409
|
-
const
|
|
615
|
+
const nestedError = payload.error && typeof payload.error === "object" ? payload.error : void 0;
|
|
616
|
+
const codeField = nestedError?.code ?? payload.code;
|
|
617
|
+
const code = typeof codeField === "string" ? codeField : void 0;
|
|
618
|
+
const messageField = nestedError?.message ?? payload.message ?? payload.error;
|
|
410
619
|
const message = typeof messageField === "string" ? messageField : void 0;
|
|
411
620
|
return { code, message };
|
|
412
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Parse node ID safely to handle u64 values above Number.MAX_SAFE_INTEGER.
|
|
624
|
+
* Returns bigint for large values, number for safe values.
|
|
625
|
+
*/
|
|
626
|
+
parseNodeId(value) {
|
|
627
|
+
if (value === null || value === void 0) {
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
if (typeof value === "bigint") {
|
|
631
|
+
return value;
|
|
632
|
+
}
|
|
633
|
+
if (typeof value === "string") {
|
|
634
|
+
const num = Number(value);
|
|
635
|
+
if (num > Number.MAX_SAFE_INTEGER) {
|
|
636
|
+
return BigInt(value);
|
|
637
|
+
}
|
|
638
|
+
return num;
|
|
639
|
+
}
|
|
640
|
+
if (typeof value === "number") {
|
|
641
|
+
if (value > Number.MAX_SAFE_INTEGER) {
|
|
642
|
+
return value;
|
|
643
|
+
}
|
|
644
|
+
return value;
|
|
645
|
+
}
|
|
646
|
+
return 0;
|
|
647
|
+
}
|
|
648
|
+
parseRestPointId(id) {
|
|
649
|
+
if (typeof id !== "number" || !Number.isFinite(id) || id < 0 || !Number.isInteger(id) || id > Number.MAX_SAFE_INTEGER) {
|
|
650
|
+
throw new ValidationError(
|
|
651
|
+
`REST backend requires numeric u64-compatible IDs in JS safe integer range (0..${Number.MAX_SAFE_INTEGER}). Received: ${String(id)}`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
return id;
|
|
655
|
+
}
|
|
656
|
+
isLikelyAggregationQuery(query) {
|
|
657
|
+
return /\bGROUP\s+BY\b|\bHAVING\b|\bCOUNT\s*\(|\bSUM\s*\(|\bAVG\s*\(|\bMIN\s*\(|\bMAX\s*\(/i.test(
|
|
658
|
+
query
|
|
659
|
+
);
|
|
660
|
+
}
|
|
413
661
|
async request(method, path, body) {
|
|
414
662
|
const url = `${this.baseUrl}${path}`;
|
|
415
663
|
const headers = {
|
|
@@ -498,14 +746,17 @@ var RestBackend = class {
|
|
|
498
746
|
}
|
|
499
747
|
async insert(collection, doc) {
|
|
500
748
|
this.ensureInitialized();
|
|
749
|
+
const restId = this.parseRestPointId(doc.id);
|
|
501
750
|
const vector = doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector;
|
|
502
751
|
const response = await this.request(
|
|
503
752
|
"POST",
|
|
504
|
-
`/collections/${encodeURIComponent(collection)}/
|
|
753
|
+
`/collections/${encodeURIComponent(collection)}/points`,
|
|
505
754
|
{
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
755
|
+
points: [{
|
|
756
|
+
id: restId,
|
|
757
|
+
vector,
|
|
758
|
+
payload: doc.payload
|
|
759
|
+
}]
|
|
509
760
|
}
|
|
510
761
|
);
|
|
511
762
|
if (response.error) {
|
|
@@ -518,14 +769,14 @@ var RestBackend = class {
|
|
|
518
769
|
async insertBatch(collection, docs) {
|
|
519
770
|
this.ensureInitialized();
|
|
520
771
|
const vectors = docs.map((doc) => ({
|
|
521
|
-
id: doc.id,
|
|
772
|
+
id: this.parseRestPointId(doc.id),
|
|
522
773
|
vector: doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector,
|
|
523
774
|
payload: doc.payload
|
|
524
775
|
}));
|
|
525
776
|
const response = await this.request(
|
|
526
777
|
"POST",
|
|
527
|
-
`/collections/${encodeURIComponent(collection)}/
|
|
528
|
-
{ vectors }
|
|
778
|
+
`/collections/${encodeURIComponent(collection)}/points`,
|
|
779
|
+
{ points: vectors }
|
|
529
780
|
);
|
|
530
781
|
if (response.error) {
|
|
531
782
|
if (response.error.code === "NOT_FOUND") {
|
|
@@ -534,18 +785,29 @@ var RestBackend = class {
|
|
|
534
785
|
throw new VelesDBError(response.error.message, response.error.code);
|
|
535
786
|
}
|
|
536
787
|
}
|
|
788
|
+
sparseVectorToRestFormat(sv) {
|
|
789
|
+
const result = {};
|
|
790
|
+
for (const [k, v] of Object.entries(sv)) {
|
|
791
|
+
result[String(k)] = v;
|
|
792
|
+
}
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
537
795
|
async search(collection, query, options) {
|
|
538
796
|
this.ensureInitialized();
|
|
539
797
|
const queryVector = query instanceof Float32Array ? Array.from(query) : query;
|
|
798
|
+
const body = {
|
|
799
|
+
vector: queryVector,
|
|
800
|
+
top_k: options?.k ?? 10,
|
|
801
|
+
filter: options?.filter,
|
|
802
|
+
include_vectors: options?.includeVectors ?? false
|
|
803
|
+
};
|
|
804
|
+
if (options?.sparseVector) {
|
|
805
|
+
body.sparse_vector = this.sparseVectorToRestFormat(options.sparseVector);
|
|
806
|
+
}
|
|
540
807
|
const response = await this.request(
|
|
541
808
|
"POST",
|
|
542
809
|
`/collections/${encodeURIComponent(collection)}/search`,
|
|
543
|
-
|
|
544
|
-
vector: queryVector,
|
|
545
|
-
k: options?.k ?? 10,
|
|
546
|
-
filter: options?.filter,
|
|
547
|
-
include_vectors: options?.includeVectors ?? false
|
|
548
|
-
}
|
|
810
|
+
body
|
|
549
811
|
);
|
|
550
812
|
if (response.error) {
|
|
551
813
|
if (response.error.code === "NOT_FOUND") {
|
|
@@ -553,7 +815,7 @@ var RestBackend = class {
|
|
|
553
815
|
}
|
|
554
816
|
throw new VelesDBError(response.error.message, response.error.code);
|
|
555
817
|
}
|
|
556
|
-
return response.data ?? [];
|
|
818
|
+
return response.data?.results ?? [];
|
|
557
819
|
}
|
|
558
820
|
async searchBatch(collection, searches) {
|
|
559
821
|
this.ensureInitialized();
|
|
@@ -577,9 +839,10 @@ var RestBackend = class {
|
|
|
577
839
|
}
|
|
578
840
|
async delete(collection, id) {
|
|
579
841
|
this.ensureInitialized();
|
|
842
|
+
const restId = this.parseRestPointId(id);
|
|
580
843
|
const response = await this.request(
|
|
581
844
|
"DELETE",
|
|
582
|
-
`/collections/${encodeURIComponent(collection)}/
|
|
845
|
+
`/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(restId))}`
|
|
583
846
|
);
|
|
584
847
|
if (response.error) {
|
|
585
848
|
if (response.error.code === "NOT_FOUND") {
|
|
@@ -591,9 +854,10 @@ var RestBackend = class {
|
|
|
591
854
|
}
|
|
592
855
|
async get(collection, id) {
|
|
593
856
|
this.ensureInitialized();
|
|
857
|
+
const restId = this.parseRestPointId(id);
|
|
594
858
|
const response = await this.request(
|
|
595
859
|
"GET",
|
|
596
|
-
`/collections/${encodeURIComponent(collection)}/
|
|
860
|
+
`/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(restId))}`
|
|
597
861
|
);
|
|
598
862
|
if (response.error) {
|
|
599
863
|
if (response.error.code === "NOT_FOUND") {
|
|
@@ -644,11 +908,63 @@ var RestBackend = class {
|
|
|
644
908
|
}
|
|
645
909
|
return response.data?.results ?? [];
|
|
646
910
|
}
|
|
647
|
-
async query(queryString, params) {
|
|
911
|
+
async query(collection, queryString, params, options) {
|
|
648
912
|
this.ensureInitialized();
|
|
913
|
+
const endpoint = this.isLikelyAggregationQuery(queryString) ? "/aggregate" : "/query";
|
|
649
914
|
const response = await this.request(
|
|
650
915
|
"POST",
|
|
651
|
-
|
|
916
|
+
endpoint,
|
|
917
|
+
{
|
|
918
|
+
query: queryString,
|
|
919
|
+
params: params ?? {},
|
|
920
|
+
collection,
|
|
921
|
+
timeout_ms: options?.timeoutMs,
|
|
922
|
+
stream: options?.stream ?? false
|
|
923
|
+
}
|
|
924
|
+
);
|
|
925
|
+
if (response.error) {
|
|
926
|
+
if (response.error.code === "NOT_FOUND") {
|
|
927
|
+
throw new NotFoundError(`Collection '${collection}'`);
|
|
928
|
+
}
|
|
929
|
+
throw new VelesDBError(response.error.message, response.error.code);
|
|
930
|
+
}
|
|
931
|
+
const rawData = response.data;
|
|
932
|
+
if (rawData && Object.prototype.hasOwnProperty.call(rawData, "result")) {
|
|
933
|
+
return {
|
|
934
|
+
result: rawData.result,
|
|
935
|
+
stats: {
|
|
936
|
+
executionTimeMs: rawData.timing_ms ?? 0,
|
|
937
|
+
strategy: "aggregation",
|
|
938
|
+
scannedNodes: 0
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
results: (rawData?.results ?? []).map((r) => ({
|
|
944
|
+
// Server returns `id` (u64), map to nodeId with precision handling
|
|
945
|
+
nodeId: this.parseNodeId(r.id ?? r.node_id ?? r.nodeId),
|
|
946
|
+
// Server returns `score`, map to vectorScore (primary score for SELECT queries)
|
|
947
|
+
vectorScore: r.score ?? r.vector_score ?? r.vectorScore,
|
|
948
|
+
// graph_score not returned by SELECT queries, only by future MATCH queries
|
|
949
|
+
graphScore: r.graph_score ?? r.graphScore,
|
|
950
|
+
// Use score as fusedScore for compatibility
|
|
951
|
+
fusedScore: r.score ?? r.fused_score ?? r.fusedScore ?? 0,
|
|
952
|
+
// payload maps to bindings for compatibility
|
|
953
|
+
bindings: r.payload ?? r.bindings ?? {},
|
|
954
|
+
columnData: r.column_data ?? r.columnData
|
|
955
|
+
})),
|
|
956
|
+
stats: {
|
|
957
|
+
executionTimeMs: rawData?.timing_ms ?? 0,
|
|
958
|
+
strategy: "select",
|
|
959
|
+
scannedNodes: rawData?.rows_returned ?? 0
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
async queryExplain(queryString, params) {
|
|
964
|
+
this.ensureInitialized();
|
|
965
|
+
const response = await this.request(
|
|
966
|
+
"POST",
|
|
967
|
+
"/query/explain",
|
|
652
968
|
{
|
|
653
969
|
query: queryString,
|
|
654
970
|
params: params ?? {}
|
|
@@ -657,7 +973,68 @@ var RestBackend = class {
|
|
|
657
973
|
if (response.error) {
|
|
658
974
|
throw new VelesDBError(response.error.message, response.error.code);
|
|
659
975
|
}
|
|
660
|
-
|
|
976
|
+
const data = response.data;
|
|
977
|
+
return {
|
|
978
|
+
query: data.query,
|
|
979
|
+
queryType: data.query_type,
|
|
980
|
+
collection: data.collection,
|
|
981
|
+
plan: data.plan.map((step) => ({
|
|
982
|
+
step: step.step,
|
|
983
|
+
operation: step.operation,
|
|
984
|
+
description: step.description,
|
|
985
|
+
estimatedRows: step.estimated_rows
|
|
986
|
+
})),
|
|
987
|
+
estimatedCost: {
|
|
988
|
+
usesIndex: data.estimated_cost.uses_index,
|
|
989
|
+
indexName: data.estimated_cost.index_name,
|
|
990
|
+
selectivity: data.estimated_cost.selectivity,
|
|
991
|
+
complexity: data.estimated_cost.complexity
|
|
992
|
+
},
|
|
993
|
+
features: {
|
|
994
|
+
hasVectorSearch: data.features.has_vector_search,
|
|
995
|
+
hasFilter: data.features.has_filter,
|
|
996
|
+
hasOrderBy: data.features.has_order_by,
|
|
997
|
+
hasGroupBy: data.features.has_group_by,
|
|
998
|
+
hasAggregation: data.features.has_aggregation,
|
|
999
|
+
hasJoin: data.features.has_join,
|
|
1000
|
+
hasFusion: data.features.has_fusion,
|
|
1001
|
+
limit: data.features.limit,
|
|
1002
|
+
offset: data.features.offset
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
async collectionSanity(collection) {
|
|
1007
|
+
this.ensureInitialized();
|
|
1008
|
+
const response = await this.request(
|
|
1009
|
+
"GET",
|
|
1010
|
+
`/collections/${encodeURIComponent(collection)}/sanity`
|
|
1011
|
+
);
|
|
1012
|
+
if (response.error) {
|
|
1013
|
+
if (response.error.code === "NOT_FOUND") {
|
|
1014
|
+
throw new NotFoundError(`Collection '${collection}'`);
|
|
1015
|
+
}
|
|
1016
|
+
throw new VelesDBError(response.error.message, response.error.code);
|
|
1017
|
+
}
|
|
1018
|
+
const data = response.data;
|
|
1019
|
+
return {
|
|
1020
|
+
collection: data.collection,
|
|
1021
|
+
dimension: data.dimension,
|
|
1022
|
+
metric: data.metric,
|
|
1023
|
+
pointCount: data.point_count,
|
|
1024
|
+
isEmpty: data.is_empty,
|
|
1025
|
+
checks: {
|
|
1026
|
+
hasVectors: data.checks.has_vectors,
|
|
1027
|
+
searchReady: data.checks.search_ready,
|
|
1028
|
+
dimensionConfigured: data.checks.dimension_configured
|
|
1029
|
+
},
|
|
1030
|
+
diagnostics: {
|
|
1031
|
+
searchRequestsTotal: data.diagnostics.search_requests_total,
|
|
1032
|
+
dimensionMismatchTotal: data.diagnostics.dimension_mismatch_total,
|
|
1033
|
+
emptySearchResultsTotal: data.diagnostics.empty_search_results_total,
|
|
1034
|
+
filterParseErrorsTotal: data.diagnostics.filter_parse_errors_total
|
|
1035
|
+
},
|
|
1036
|
+
hints: data.hints ?? []
|
|
1037
|
+
};
|
|
661
1038
|
}
|
|
662
1039
|
async multiQuerySearch(collection, vectors, options) {
|
|
663
1040
|
this.ensureInitialized();
|
|
@@ -672,6 +1049,9 @@ var RestBackend = class {
|
|
|
672
1049
|
top_k: options?.k ?? 10,
|
|
673
1050
|
strategy: options?.fusion ?? "rrf",
|
|
674
1051
|
rrf_k: options?.fusionParams?.k ?? 60,
|
|
1052
|
+
avg_weight: options?.fusionParams?.avgWeight,
|
|
1053
|
+
max_weight: options?.fusionParams?.maxWeight,
|
|
1054
|
+
hit_weight: options?.fusionParams?.hitWeight,
|
|
675
1055
|
filter: options?.filter
|
|
676
1056
|
}
|
|
677
1057
|
);
|
|
@@ -710,6 +1090,81 @@ var RestBackend = class {
|
|
|
710
1090
|
throw new VelesDBError(response.error.message, response.error.code);
|
|
711
1091
|
}
|
|
712
1092
|
}
|
|
1093
|
+
// ========================================================================
|
|
1094
|
+
// Sparse / PQ / Streaming (v1.5)
|
|
1095
|
+
// ========================================================================
|
|
1096
|
+
async trainPq(collection, options) {
|
|
1097
|
+
this.ensureInitialized();
|
|
1098
|
+
const m = options?.m ?? 8;
|
|
1099
|
+
const k = options?.k ?? 256;
|
|
1100
|
+
const withClause = options?.opq ? `WITH (m=${m}, k=${k}, opq=true)` : `WITH (m=${m}, k=${k})`;
|
|
1101
|
+
const queryString = `TRAIN QUANTIZER ON ${collection} ${withClause}`;
|
|
1102
|
+
const response = await this.request(
|
|
1103
|
+
"POST",
|
|
1104
|
+
"/query",
|
|
1105
|
+
{ query: queryString }
|
|
1106
|
+
);
|
|
1107
|
+
if (response.error) {
|
|
1108
|
+
throw new VelesDBError(response.error.message, response.error.code);
|
|
1109
|
+
}
|
|
1110
|
+
return response.data?.message ?? "PQ training initiated";
|
|
1111
|
+
}
|
|
1112
|
+
async streamInsert(collection, docs) {
|
|
1113
|
+
this.ensureInitialized();
|
|
1114
|
+
for (const doc of docs) {
|
|
1115
|
+
const restId = this.parseRestPointId(doc.id);
|
|
1116
|
+
const vector = doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector;
|
|
1117
|
+
const body = {
|
|
1118
|
+
id: restId,
|
|
1119
|
+
vector,
|
|
1120
|
+
payload: doc.payload
|
|
1121
|
+
};
|
|
1122
|
+
if (doc.sparseVector) {
|
|
1123
|
+
body.sparse_vector = this.sparseVectorToRestFormat(doc.sparseVector);
|
|
1124
|
+
}
|
|
1125
|
+
const url = `${this.baseUrl}/collections/${encodeURIComponent(collection)}/stream/insert`;
|
|
1126
|
+
const headers = {
|
|
1127
|
+
"Content-Type": "application/json"
|
|
1128
|
+
};
|
|
1129
|
+
if (this.apiKey) {
|
|
1130
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1131
|
+
}
|
|
1132
|
+
const controller = new AbortController();
|
|
1133
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1134
|
+
try {
|
|
1135
|
+
const response = await fetch(url, {
|
|
1136
|
+
method: "POST",
|
|
1137
|
+
headers,
|
|
1138
|
+
body: JSON.stringify(body),
|
|
1139
|
+
signal: controller.signal
|
|
1140
|
+
});
|
|
1141
|
+
clearTimeout(timeoutId);
|
|
1142
|
+
if (response.status === 429) {
|
|
1143
|
+
throw new BackpressureError();
|
|
1144
|
+
}
|
|
1145
|
+
if (!response.ok && response.status !== 202) {
|
|
1146
|
+
const data = await response.json().catch(() => ({}));
|
|
1147
|
+
const errorPayload = this.extractErrorPayload(data);
|
|
1148
|
+
throw new VelesDBError(
|
|
1149
|
+
errorPayload.message ?? `HTTP ${response.status}`,
|
|
1150
|
+
errorPayload.code ?? this.mapStatusToErrorCode(response.status)
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
clearTimeout(timeoutId);
|
|
1155
|
+
if (error instanceof BackpressureError || error instanceof VelesDBError) {
|
|
1156
|
+
throw error;
|
|
1157
|
+
}
|
|
1158
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1159
|
+
throw new ConnectionError("Request timeout");
|
|
1160
|
+
}
|
|
1161
|
+
throw new ConnectionError(
|
|
1162
|
+
`Stream insert failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1163
|
+
error instanceof Error ? error : void 0
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
713
1168
|
async close() {
|
|
714
1169
|
this._initialized = false;
|
|
715
1170
|
}
|
|
@@ -1026,6 +1481,18 @@ var VelesDB = class {
|
|
|
1026
1481
|
if (!Array.isArray(doc.vector) && !(doc.vector instanceof Float32Array)) {
|
|
1027
1482
|
throw new ValidationError("Vector must be an array or Float32Array");
|
|
1028
1483
|
}
|
|
1484
|
+
if (this.config.backend === "rest" && (typeof doc.id !== "number" || !Number.isInteger(doc.id) || doc.id < 0 || doc.id > Number.MAX_SAFE_INTEGER)) {
|
|
1485
|
+
throw new ValidationError(
|
|
1486
|
+
`REST backend requires numeric u64-compatible document IDs in JS safe integer range (0..${Number.MAX_SAFE_INTEGER})`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
validateRestPointId(id) {
|
|
1491
|
+
if (this.config.backend === "rest" && (typeof id !== "number" || !Number.isInteger(id) || id < 0 || id > Number.MAX_SAFE_INTEGER)) {
|
|
1492
|
+
throw new ValidationError(
|
|
1493
|
+
`REST backend requires numeric u64-compatible document IDs in JS safe integer range (0..${Number.MAX_SAFE_INTEGER})`
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1029
1496
|
}
|
|
1030
1497
|
/**
|
|
1031
1498
|
* Search for similar vectors
|
|
@@ -1070,6 +1537,7 @@ var VelesDB = class {
|
|
|
1070
1537
|
*/
|
|
1071
1538
|
async delete(collection, id) {
|
|
1072
1539
|
this.ensureInitialized();
|
|
1540
|
+
this.validateRestPointId(id);
|
|
1073
1541
|
return this.backend.delete(collection, id);
|
|
1074
1542
|
}
|
|
1075
1543
|
/**
|
|
@@ -1081,6 +1549,7 @@ var VelesDB = class {
|
|
|
1081
1549
|
*/
|
|
1082
1550
|
async get(collection, id) {
|
|
1083
1551
|
this.ensureInitialized();
|
|
1552
|
+
this.validateRestPointId(id);
|
|
1084
1553
|
return this.backend.get(collection, id);
|
|
1085
1554
|
}
|
|
1086
1555
|
/**
|
|
@@ -1118,18 +1587,36 @@ var VelesDB = class {
|
|
|
1118
1587
|
return this.backend.hybridSearch(collection, vector, textQuery, options);
|
|
1119
1588
|
}
|
|
1120
1589
|
/**
|
|
1121
|
-
* Execute a VelesQL query
|
|
1590
|
+
* Execute a VelesQL multi-model query (EPIC-031 US-011)
|
|
1591
|
+
*
|
|
1592
|
+
* Supports hybrid vector + graph queries with VelesQL syntax.
|
|
1122
1593
|
*
|
|
1594
|
+
* @param collection - Collection name
|
|
1123
1595
|
* @param queryString - VelesQL query string
|
|
1124
|
-
* @param params -
|
|
1125
|
-
* @
|
|
1596
|
+
* @param params - Query parameters (vectors, scalars)
|
|
1597
|
+
* @param options - Query options (timeout, streaming)
|
|
1598
|
+
* @returns Query response with results and execution stats
|
|
1599
|
+
*
|
|
1600
|
+
* @example
|
|
1601
|
+
* ```typescript
|
|
1602
|
+
* const response = await db.query('docs', `
|
|
1603
|
+
* MATCH (d:Doc) WHERE vector NEAR $q LIMIT 20
|
|
1604
|
+
* `, { q: queryVector });
|
|
1605
|
+
*
|
|
1606
|
+
* for (const r of response.results) {
|
|
1607
|
+
* console.log(`Node ${r.nodeId}: ${r.fusedScore}`);
|
|
1608
|
+
* }
|
|
1609
|
+
* ```
|
|
1126
1610
|
*/
|
|
1127
|
-
async query(queryString, params) {
|
|
1611
|
+
async query(collection, queryString, params, options) {
|
|
1128
1612
|
this.ensureInitialized();
|
|
1613
|
+
if (!collection || typeof collection !== "string") {
|
|
1614
|
+
throw new ValidationError("Collection name must be a non-empty string");
|
|
1615
|
+
}
|
|
1129
1616
|
if (!queryString || typeof queryString !== "string") {
|
|
1130
1617
|
throw new ValidationError("Query string must be a non-empty string");
|
|
1131
1618
|
}
|
|
1132
|
-
return this.backend.query(queryString, params);
|
|
1619
|
+
return this.backend.query(collection, queryString, params, options);
|
|
1133
1620
|
}
|
|
1134
1621
|
/**
|
|
1135
1622
|
* Multi-query fusion search combining results from multiple query vectors
|
|
@@ -1158,6 +1645,20 @@ var VelesDB = class {
|
|
|
1158
1645
|
* });
|
|
1159
1646
|
* ```
|
|
1160
1647
|
*/
|
|
1648
|
+
async queryExplain(queryString, params) {
|
|
1649
|
+
this.ensureInitialized();
|
|
1650
|
+
if (!queryString || typeof queryString !== "string") {
|
|
1651
|
+
throw new ValidationError("Query string must be a non-empty string");
|
|
1652
|
+
}
|
|
1653
|
+
return this.backend.queryExplain(queryString, params);
|
|
1654
|
+
}
|
|
1655
|
+
async collectionSanity(collection) {
|
|
1656
|
+
this.ensureInitialized();
|
|
1657
|
+
if (!collection || typeof collection !== "string") {
|
|
1658
|
+
throw new ValidationError("Collection name must be a non-empty string");
|
|
1659
|
+
}
|
|
1660
|
+
return this.backend.collectionSanity(collection);
|
|
1661
|
+
}
|
|
1161
1662
|
async multiQuerySearch(collection, vectors, options) {
|
|
1162
1663
|
this.ensureInitialized();
|
|
1163
1664
|
if (!Array.isArray(vectors) || vectors.length === 0) {
|
|
@@ -1170,9 +1671,39 @@ var VelesDB = class {
|
|
|
1170
1671
|
}
|
|
1171
1672
|
return this.backend.multiQuerySearch(collection, vectors, options);
|
|
1172
1673
|
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Train Product Quantization on a collection
|
|
1676
|
+
*
|
|
1677
|
+
* @param collection - Collection name
|
|
1678
|
+
* @param options - PQ training options (m, k, opq)
|
|
1679
|
+
* @returns Server response message
|
|
1680
|
+
*/
|
|
1681
|
+
async trainPq(collection, options) {
|
|
1682
|
+
this.ensureInitialized();
|
|
1683
|
+
return this.backend.trainPq(collection, options);
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Stream-insert documents with backpressure support
|
|
1687
|
+
*
|
|
1688
|
+
* Sends documents sequentially to respect server backpressure.
|
|
1689
|
+
* Throws BackpressureError on 429 responses.
|
|
1690
|
+
*
|
|
1691
|
+
* @param collection - Collection name
|
|
1692
|
+
* @param docs - Documents to insert
|
|
1693
|
+
*/
|
|
1694
|
+
async streamInsert(collection, docs) {
|
|
1695
|
+
this.ensureInitialized();
|
|
1696
|
+
if (!Array.isArray(docs)) {
|
|
1697
|
+
throw new ValidationError("Documents must be an array");
|
|
1698
|
+
}
|
|
1699
|
+
for (const doc of docs) {
|
|
1700
|
+
this.validateDocument(doc);
|
|
1701
|
+
}
|
|
1702
|
+
await this.backend.streamInsert(collection, docs);
|
|
1703
|
+
}
|
|
1173
1704
|
/**
|
|
1174
1705
|
* Check if a collection is empty
|
|
1175
|
-
*
|
|
1706
|
+
*
|
|
1176
1707
|
* @param collection - Collection name
|
|
1177
1708
|
* @returns true if empty, false otherwise
|
|
1178
1709
|
*/
|
|
@@ -1367,12 +1898,317 @@ var VelesDB = class {
|
|
|
1367
1898
|
return this.backend.getNodeDegree(collection, nodeId);
|
|
1368
1899
|
}
|
|
1369
1900
|
};
|
|
1901
|
+
|
|
1902
|
+
// src/query-builder.ts
|
|
1903
|
+
var VelesQLBuilder = class _VelesQLBuilder {
|
|
1904
|
+
constructor(state) {
|
|
1905
|
+
this.state = {
|
|
1906
|
+
matchClauses: state?.matchClauses ?? [],
|
|
1907
|
+
whereClauses: state?.whereClauses ?? [],
|
|
1908
|
+
whereOperators: state?.whereOperators ?? [],
|
|
1909
|
+
params: state?.params ?? {},
|
|
1910
|
+
limitValue: state?.limitValue,
|
|
1911
|
+
offsetValue: state?.offsetValue,
|
|
1912
|
+
orderByClause: state?.orderByClause,
|
|
1913
|
+
returnClause: state?.returnClause,
|
|
1914
|
+
fusionOptions: state?.fusionOptions,
|
|
1915
|
+
currentNode: state?.currentNode,
|
|
1916
|
+
pendingRel: state?.pendingRel
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
clone(updates) {
|
|
1920
|
+
return new _VelesQLBuilder({
|
|
1921
|
+
...this.state,
|
|
1922
|
+
matchClauses: [...this.state.matchClauses],
|
|
1923
|
+
whereClauses: [...this.state.whereClauses],
|
|
1924
|
+
whereOperators: [...this.state.whereOperators],
|
|
1925
|
+
params: { ...this.state.params },
|
|
1926
|
+
...updates
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Start a MATCH clause with a node pattern
|
|
1931
|
+
*
|
|
1932
|
+
* @param alias - Node alias (e.g., 'n', 'person')
|
|
1933
|
+
* @param label - Optional node label(s)
|
|
1934
|
+
*/
|
|
1935
|
+
match(alias, label) {
|
|
1936
|
+
const labelStr = this.formatLabel(label);
|
|
1937
|
+
const nodePattern = `(${alias}${labelStr})`;
|
|
1938
|
+
return this.clone({
|
|
1939
|
+
matchClauses: [...this.state.matchClauses, nodePattern],
|
|
1940
|
+
currentNode: alias
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Add a relationship pattern
|
|
1945
|
+
*
|
|
1946
|
+
* @param type - Relationship type (e.g., 'KNOWS', 'FOLLOWS')
|
|
1947
|
+
* @param alias - Optional relationship alias
|
|
1948
|
+
* @param options - Relationship options (direction, hops)
|
|
1949
|
+
*/
|
|
1950
|
+
rel(type, alias, options) {
|
|
1951
|
+
return this.clone({
|
|
1952
|
+
pendingRel: { type, alias, options }
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Complete a relationship pattern with target node
|
|
1957
|
+
*
|
|
1958
|
+
* @param alias - Target node alias
|
|
1959
|
+
* @param label - Optional target node label(s)
|
|
1960
|
+
*/
|
|
1961
|
+
to(alias, label) {
|
|
1962
|
+
if (!this.state.pendingRel) {
|
|
1963
|
+
throw new Error("to() must be called after rel()");
|
|
1964
|
+
}
|
|
1965
|
+
const { type, alias: relAlias, options } = this.state.pendingRel;
|
|
1966
|
+
const direction = options?.direction ?? "outgoing";
|
|
1967
|
+
const labelStr = this.formatLabel(label);
|
|
1968
|
+
const relPattern = this.formatRelationship(type, relAlias, options);
|
|
1969
|
+
const targetNode = `(${alias}${labelStr})`;
|
|
1970
|
+
let fullPattern;
|
|
1971
|
+
switch (direction) {
|
|
1972
|
+
case "incoming":
|
|
1973
|
+
fullPattern = `<-${relPattern}-${targetNode}`;
|
|
1974
|
+
break;
|
|
1975
|
+
case "both":
|
|
1976
|
+
fullPattern = `-${relPattern}-${targetNode}`;
|
|
1977
|
+
break;
|
|
1978
|
+
default:
|
|
1979
|
+
fullPattern = `-${relPattern}->${targetNode}`;
|
|
1980
|
+
}
|
|
1981
|
+
const lastMatch = this.state.matchClauses[this.state.matchClauses.length - 1];
|
|
1982
|
+
const updatedMatch = lastMatch + fullPattern;
|
|
1983
|
+
const newMatchClauses = [...this.state.matchClauses.slice(0, -1), updatedMatch];
|
|
1984
|
+
return this.clone({
|
|
1985
|
+
matchClauses: newMatchClauses,
|
|
1986
|
+
currentNode: alias,
|
|
1987
|
+
pendingRel: void 0
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Add a WHERE clause
|
|
1992
|
+
*
|
|
1993
|
+
* @param condition - WHERE condition
|
|
1994
|
+
* @param params - Optional parameters
|
|
1995
|
+
*/
|
|
1996
|
+
where(condition, params) {
|
|
1997
|
+
const newParams = params ? { ...this.state.params, ...params } : this.state.params;
|
|
1998
|
+
return this.clone({
|
|
1999
|
+
whereClauses: [...this.state.whereClauses, condition],
|
|
2000
|
+
whereOperators: [...this.state.whereOperators],
|
|
2001
|
+
params: newParams
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Add an AND WHERE clause
|
|
2006
|
+
*
|
|
2007
|
+
* @param condition - WHERE condition
|
|
2008
|
+
* @param params - Optional parameters
|
|
2009
|
+
*/
|
|
2010
|
+
andWhere(condition, params) {
|
|
2011
|
+
const newParams = params ? { ...this.state.params, ...params } : this.state.params;
|
|
2012
|
+
return this.clone({
|
|
2013
|
+
whereClauses: [...this.state.whereClauses, condition],
|
|
2014
|
+
whereOperators: [...this.state.whereOperators, "AND"],
|
|
2015
|
+
params: newParams
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Add an OR WHERE clause
|
|
2020
|
+
*
|
|
2021
|
+
* @param condition - WHERE condition
|
|
2022
|
+
* @param params - Optional parameters
|
|
2023
|
+
*/
|
|
2024
|
+
orWhere(condition, params) {
|
|
2025
|
+
const newParams = params ? { ...this.state.params, ...params } : this.state.params;
|
|
2026
|
+
return this.clone({
|
|
2027
|
+
whereClauses: [...this.state.whereClauses, condition],
|
|
2028
|
+
whereOperators: [...this.state.whereOperators, "OR"],
|
|
2029
|
+
params: newParams
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Add a vector NEAR clause for similarity search
|
|
2034
|
+
*
|
|
2035
|
+
* @param paramName - Parameter name (e.g., '$query', '$embedding')
|
|
2036
|
+
* @param vector - Vector data
|
|
2037
|
+
* @param options - NEAR options (topK)
|
|
2038
|
+
*/
|
|
2039
|
+
nearVector(paramName, vector, options) {
|
|
2040
|
+
const cleanParamName = paramName.startsWith("$") ? paramName.slice(1) : paramName;
|
|
2041
|
+
const topKSuffix = options?.topK ? ` TOP ${options.topK}` : "";
|
|
2042
|
+
const condition = `vector NEAR $${cleanParamName}${topKSuffix}`;
|
|
2043
|
+
const newParams = { ...this.state.params, [cleanParamName]: vector };
|
|
2044
|
+
if (this.state.whereClauses.length === 0) {
|
|
2045
|
+
return this.clone({
|
|
2046
|
+
whereClauses: [condition],
|
|
2047
|
+
params: newParams
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
return this.clone({
|
|
2051
|
+
whereClauses: [...this.state.whereClauses, condition],
|
|
2052
|
+
whereOperators: [...this.state.whereOperators, "AND"],
|
|
2053
|
+
params: newParams
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Add LIMIT clause
|
|
2058
|
+
*
|
|
2059
|
+
* @param value - Maximum number of results
|
|
2060
|
+
*/
|
|
2061
|
+
limit(value) {
|
|
2062
|
+
if (value < 0) {
|
|
2063
|
+
throw new Error("LIMIT must be non-negative");
|
|
2064
|
+
}
|
|
2065
|
+
return this.clone({ limitValue: value });
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Add OFFSET clause
|
|
2069
|
+
*
|
|
2070
|
+
* @param value - Number of results to skip
|
|
2071
|
+
*/
|
|
2072
|
+
offset(value) {
|
|
2073
|
+
if (value < 0) {
|
|
2074
|
+
throw new Error("OFFSET must be non-negative");
|
|
2075
|
+
}
|
|
2076
|
+
return this.clone({ offsetValue: value });
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Add ORDER BY clause
|
|
2080
|
+
*
|
|
2081
|
+
* @param field - Field to order by
|
|
2082
|
+
* @param direction - Sort direction (ASC or DESC)
|
|
2083
|
+
*/
|
|
2084
|
+
orderBy(field, direction) {
|
|
2085
|
+
const orderClause = direction ? `${field} ${direction}` : field;
|
|
2086
|
+
return this.clone({ orderByClause: orderClause });
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Add RETURN clause with specific fields
|
|
2090
|
+
*
|
|
2091
|
+
* @param fields - Fields to return (array or object with aliases)
|
|
2092
|
+
*/
|
|
2093
|
+
return(fields) {
|
|
2094
|
+
let returnClause;
|
|
2095
|
+
if (Array.isArray(fields)) {
|
|
2096
|
+
returnClause = fields.join(", ");
|
|
2097
|
+
} else {
|
|
2098
|
+
returnClause = Object.entries(fields).map(([field, alias]) => `${field} AS ${alias}`).join(", ");
|
|
2099
|
+
}
|
|
2100
|
+
return this.clone({ returnClause });
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Add RETURN * clause
|
|
2104
|
+
*/
|
|
2105
|
+
returnAll() {
|
|
2106
|
+
return this.clone({ returnClause: "*" });
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Set fusion strategy for hybrid queries
|
|
2110
|
+
*
|
|
2111
|
+
* @param strategy - Fusion strategy
|
|
2112
|
+
* @param options - Fusion parameters
|
|
2113
|
+
*/
|
|
2114
|
+
fusion(strategy, options) {
|
|
2115
|
+
return this.clone({
|
|
2116
|
+
fusionOptions: {
|
|
2117
|
+
strategy,
|
|
2118
|
+
...options
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Get the fusion options
|
|
2124
|
+
*/
|
|
2125
|
+
getFusionOptions() {
|
|
2126
|
+
return this.state.fusionOptions;
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Get all parameters
|
|
2130
|
+
*/
|
|
2131
|
+
getParams() {
|
|
2132
|
+
return { ...this.state.params };
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Build the VelesQL query string
|
|
2136
|
+
*/
|
|
2137
|
+
toVelesQL() {
|
|
2138
|
+
if (this.state.matchClauses.length === 0) {
|
|
2139
|
+
throw new Error("Query must have at least one MATCH clause");
|
|
2140
|
+
}
|
|
2141
|
+
const parts = [];
|
|
2142
|
+
parts.push(`MATCH ${this.state.matchClauses.join(", ")}`);
|
|
2143
|
+
if (this.state.whereClauses.length > 0) {
|
|
2144
|
+
const whereStr = this.buildWhereClause();
|
|
2145
|
+
parts.push(`WHERE ${whereStr}`);
|
|
2146
|
+
}
|
|
2147
|
+
if (this.state.orderByClause) {
|
|
2148
|
+
parts.push(`ORDER BY ${this.state.orderByClause}`);
|
|
2149
|
+
}
|
|
2150
|
+
if (this.state.limitValue !== void 0) {
|
|
2151
|
+
parts.push(`LIMIT ${this.state.limitValue}`);
|
|
2152
|
+
}
|
|
2153
|
+
if (this.state.offsetValue !== void 0) {
|
|
2154
|
+
parts.push(`OFFSET ${this.state.offsetValue}`);
|
|
2155
|
+
}
|
|
2156
|
+
if (this.state.returnClause) {
|
|
2157
|
+
parts.push(`RETURN ${this.state.returnClause}`);
|
|
2158
|
+
}
|
|
2159
|
+
if (this.state.fusionOptions) {
|
|
2160
|
+
parts.push(`/* FUSION ${this.state.fusionOptions.strategy} */`);
|
|
2161
|
+
}
|
|
2162
|
+
return parts.join(" ");
|
|
2163
|
+
}
|
|
2164
|
+
formatLabel(label) {
|
|
2165
|
+
if (!label) return "";
|
|
2166
|
+
if (Array.isArray(label)) {
|
|
2167
|
+
return label.map((l) => `:${l}`).join("");
|
|
2168
|
+
}
|
|
2169
|
+
return `:${label}`;
|
|
2170
|
+
}
|
|
2171
|
+
formatRelationship(type, alias, options) {
|
|
2172
|
+
const aliasStr = alias ? alias : "";
|
|
2173
|
+
const hopsStr = this.formatHops(options);
|
|
2174
|
+
if (alias) {
|
|
2175
|
+
return `[${aliasStr}:${type}${hopsStr}]`;
|
|
2176
|
+
}
|
|
2177
|
+
return `[:${type}${hopsStr}]`;
|
|
2178
|
+
}
|
|
2179
|
+
formatHops(options) {
|
|
2180
|
+
if (!options?.minHops && !options?.maxHops) return "";
|
|
2181
|
+
const min = options.minHops ?? 1;
|
|
2182
|
+
const max = options.maxHops ?? "";
|
|
2183
|
+
return `*${min}..${max}`;
|
|
2184
|
+
}
|
|
2185
|
+
buildWhereClause() {
|
|
2186
|
+
if (this.state.whereClauses.length === 0) return "";
|
|
2187
|
+
const first = this.state.whereClauses[0];
|
|
2188
|
+
if (!first) return "";
|
|
2189
|
+
let result = first;
|
|
2190
|
+
for (let i = 1; i < this.state.whereClauses.length; i++) {
|
|
2191
|
+
const operator = this.state.whereOperators[i - 1] ?? "AND";
|
|
2192
|
+
const clause = this.state.whereClauses[i];
|
|
2193
|
+
if (clause) {
|
|
2194
|
+
result += ` ${operator} ${clause}`;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return result;
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
function velesql() {
|
|
2201
|
+
return new VelesQLBuilder();
|
|
2202
|
+
}
|
|
1370
2203
|
export {
|
|
2204
|
+
BackpressureError,
|
|
1371
2205
|
ConnectionError,
|
|
1372
2206
|
NotFoundError,
|
|
1373
2207
|
RestBackend,
|
|
1374
2208
|
ValidationError,
|
|
1375
2209
|
VelesDB,
|
|
1376
2210
|
VelesDBError,
|
|
1377
|
-
|
|
2211
|
+
VelesQLBuilder,
|
|
2212
|
+
WasmBackend,
|
|
2213
|
+
velesql
|
|
1378
2214
|
};
|