@wiscale/velesdb-sdk 1.3.0 → 1.4.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/dist/index.mjs CHANGED
@@ -133,7 +133,7 @@ var WasmBackend = class {
133
133
  throw new NotFoundError(`Collection '${collectionName}'`);
134
134
  }
135
135
  for (const doc of docs) {
136
- const vectorLen = Array.isArray(doc.vector) ? doc.vector.length : doc.vector.length;
136
+ const vectorLen = doc.vector.length;
137
137
  if (vectorLen !== collection.config.dimension) {
138
138
  throw new VelesDBError(
139
139
  `Vector dimension mismatch for doc ${doc.id}: expected ${collection.config.dimension}, got ${vectorLen}`,
@@ -239,7 +239,7 @@ var WasmBackend = class {
239
239
  "NOT_SUPPORTED"
240
240
  );
241
241
  }
242
- async query(_queryString, _params) {
242
+ async query(_collection, _queryString, _params, _options) {
243
243
  throw new VelesDBError(
244
244
  "VelesQL queries are not supported in WASM backend. Use REST backend for query support.",
245
245
  "NOT_SUPPORTED"
@@ -410,6 +410,32 @@ var RestBackend = class {
410
410
  const message = typeof messageField === "string" ? messageField : void 0;
411
411
  return { code, message };
412
412
  }
413
+ /**
414
+ * Parse node ID safely to handle u64 values above Number.MAX_SAFE_INTEGER.
415
+ * Returns bigint for large values, number for safe values.
416
+ */
417
+ parseNodeId(value) {
418
+ if (value === null || value === void 0) {
419
+ return 0;
420
+ }
421
+ if (typeof value === "bigint") {
422
+ return value;
423
+ }
424
+ if (typeof value === "string") {
425
+ const num = Number(value);
426
+ if (num > Number.MAX_SAFE_INTEGER) {
427
+ return BigInt(value);
428
+ }
429
+ return num;
430
+ }
431
+ if (typeof value === "number") {
432
+ if (value > Number.MAX_SAFE_INTEGER) {
433
+ return value;
434
+ }
435
+ return value;
436
+ }
437
+ return 0;
438
+ }
413
439
  async request(method, path, body) {
414
440
  const url = `${this.baseUrl}${path}`;
415
441
  const headers = {
@@ -501,11 +527,13 @@ var RestBackend = class {
501
527
  const vector = doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector;
502
528
  const response = await this.request(
503
529
  "POST",
504
- `/collections/${encodeURIComponent(collection)}/vectors`,
530
+ `/collections/${encodeURIComponent(collection)}/points`,
505
531
  {
506
- id: doc.id,
507
- vector,
508
- payload: doc.payload
532
+ points: [{
533
+ id: doc.id,
534
+ vector,
535
+ payload: doc.payload
536
+ }]
509
537
  }
510
538
  );
511
539
  if (response.error) {
@@ -524,8 +552,8 @@ var RestBackend = class {
524
552
  }));
525
553
  const response = await this.request(
526
554
  "POST",
527
- `/collections/${encodeURIComponent(collection)}/vectors/batch`,
528
- { vectors }
555
+ `/collections/${encodeURIComponent(collection)}/points`,
556
+ { points: vectors }
529
557
  );
530
558
  if (response.error) {
531
559
  if (response.error.code === "NOT_FOUND") {
@@ -579,7 +607,7 @@ var RestBackend = class {
579
607
  this.ensureInitialized();
580
608
  const response = await this.request(
581
609
  "DELETE",
582
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
610
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(id))}`
583
611
  );
584
612
  if (response.error) {
585
613
  if (response.error.code === "NOT_FOUND") {
@@ -593,7 +621,7 @@ var RestBackend = class {
593
621
  this.ensureInitialized();
594
622
  const response = await this.request(
595
623
  "GET",
596
- `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
624
+ `/collections/${encodeURIComponent(collection)}/points/${encodeURIComponent(String(id))}`
597
625
  );
598
626
  if (response.error) {
599
627
  if (response.error.code === "NOT_FOUND") {
@@ -644,7 +672,7 @@ var RestBackend = class {
644
672
  }
645
673
  return response.data?.results ?? [];
646
674
  }
647
- async query(queryString, params) {
675
+ async query(collection, queryString, params, _options) {
648
676
  this.ensureInitialized();
649
677
  const response = await this.request(
650
678
  "POST",
@@ -655,9 +683,32 @@ var RestBackend = class {
655
683
  }
656
684
  );
657
685
  if (response.error) {
686
+ if (response.error.code === "NOT_FOUND") {
687
+ throw new NotFoundError(`Collection '${collection}'`);
688
+ }
658
689
  throw new VelesDBError(response.error.message, response.error.code);
659
690
  }
660
- return response.data?.results ?? [];
691
+ const rawData = response.data;
692
+ return {
693
+ results: (rawData?.results ?? []).map((r) => ({
694
+ // Server returns `id` (u64), map to nodeId with precision handling
695
+ nodeId: this.parseNodeId(r.id ?? r.node_id ?? r.nodeId),
696
+ // Server returns `score`, map to vectorScore (primary score for SELECT queries)
697
+ vectorScore: r.score ?? r.vector_score ?? r.vectorScore,
698
+ // graph_score not returned by SELECT queries, only by future MATCH queries
699
+ graphScore: r.graph_score ?? r.graphScore,
700
+ // Use score as fusedScore for compatibility
701
+ fusedScore: r.score ?? r.fused_score ?? r.fusedScore ?? 0,
702
+ // payload maps to bindings for compatibility
703
+ bindings: r.payload ?? r.bindings ?? {},
704
+ columnData: r.column_data ?? r.columnData
705
+ })),
706
+ stats: {
707
+ executionTimeMs: rawData?.timing_ms ?? 0,
708
+ strategy: "select",
709
+ scannedNodes: rawData?.rows_returned ?? 0
710
+ }
711
+ };
661
712
  }
662
713
  async multiQuerySearch(collection, vectors, options) {
663
714
  this.ensureInitialized();
@@ -1118,18 +1169,36 @@ var VelesDB = class {
1118
1169
  return this.backend.hybridSearch(collection, vector, textQuery, options);
1119
1170
  }
1120
1171
  /**
1121
- * Execute a VelesQL query
1172
+ * Execute a VelesQL multi-model query (EPIC-031 US-011)
1122
1173
  *
1174
+ * Supports hybrid vector + graph queries with VelesQL syntax.
1175
+ *
1176
+ * @param collection - Collection name
1123
1177
  * @param queryString - VelesQL query string
1124
- * @param params - Optional query parameters
1125
- * @returns Query results
1178
+ * @param params - Query parameters (vectors, scalars)
1179
+ * @param options - Query options (timeout, streaming)
1180
+ * @returns Query response with results and execution stats
1181
+ *
1182
+ * @example
1183
+ * ```typescript
1184
+ * const response = await db.query('docs', `
1185
+ * MATCH (d:Doc) WHERE vector NEAR $q LIMIT 20
1186
+ * `, { q: queryVector });
1187
+ *
1188
+ * for (const r of response.results) {
1189
+ * console.log(`Node ${r.nodeId}: ${r.fusedScore}`);
1190
+ * }
1191
+ * ```
1126
1192
  */
1127
- async query(queryString, params) {
1193
+ async query(collection, queryString, params, options) {
1128
1194
  this.ensureInitialized();
1195
+ if (!collection || typeof collection !== "string") {
1196
+ throw new ValidationError("Collection name must be a non-empty string");
1197
+ }
1129
1198
  if (!queryString || typeof queryString !== "string") {
1130
1199
  throw new ValidationError("Query string must be a non-empty string");
1131
1200
  }
1132
- return this.backend.query(queryString, params);
1201
+ return this.backend.query(collection, queryString, params, options);
1133
1202
  }
1134
1203
  /**
1135
1204
  * Multi-query fusion search combining results from multiple query vectors
@@ -1367,6 +1436,308 @@ var VelesDB = class {
1367
1436
  return this.backend.getNodeDegree(collection, nodeId);
1368
1437
  }
1369
1438
  };
1439
+
1440
+ // src/query-builder.ts
1441
+ var VelesQLBuilder = class _VelesQLBuilder {
1442
+ constructor(state) {
1443
+ this.state = {
1444
+ matchClauses: state?.matchClauses ?? [],
1445
+ whereClauses: state?.whereClauses ?? [],
1446
+ whereOperators: state?.whereOperators ?? [],
1447
+ params: state?.params ?? {},
1448
+ limitValue: state?.limitValue,
1449
+ offsetValue: state?.offsetValue,
1450
+ orderByClause: state?.orderByClause,
1451
+ returnClause: state?.returnClause,
1452
+ fusionOptions: state?.fusionOptions,
1453
+ currentNode: state?.currentNode,
1454
+ pendingRel: state?.pendingRel
1455
+ };
1456
+ }
1457
+ clone(updates) {
1458
+ return new _VelesQLBuilder({
1459
+ ...this.state,
1460
+ matchClauses: [...this.state.matchClauses],
1461
+ whereClauses: [...this.state.whereClauses],
1462
+ whereOperators: [...this.state.whereOperators],
1463
+ params: { ...this.state.params },
1464
+ ...updates
1465
+ });
1466
+ }
1467
+ /**
1468
+ * Start a MATCH clause with a node pattern
1469
+ *
1470
+ * @param alias - Node alias (e.g., 'n', 'person')
1471
+ * @param label - Optional node label(s)
1472
+ */
1473
+ match(alias, label) {
1474
+ const labelStr = this.formatLabel(label);
1475
+ const nodePattern = `(${alias}${labelStr})`;
1476
+ return this.clone({
1477
+ matchClauses: [...this.state.matchClauses, nodePattern],
1478
+ currentNode: alias
1479
+ });
1480
+ }
1481
+ /**
1482
+ * Add a relationship pattern
1483
+ *
1484
+ * @param type - Relationship type (e.g., 'KNOWS', 'FOLLOWS')
1485
+ * @param alias - Optional relationship alias
1486
+ * @param options - Relationship options (direction, hops)
1487
+ */
1488
+ rel(type, alias, options) {
1489
+ return this.clone({
1490
+ pendingRel: { type, alias, options }
1491
+ });
1492
+ }
1493
+ /**
1494
+ * Complete a relationship pattern with target node
1495
+ *
1496
+ * @param alias - Target node alias
1497
+ * @param label - Optional target node label(s)
1498
+ */
1499
+ to(alias, label) {
1500
+ if (!this.state.pendingRel) {
1501
+ throw new Error("to() must be called after rel()");
1502
+ }
1503
+ const { type, alias: relAlias, options } = this.state.pendingRel;
1504
+ const direction = options?.direction ?? "outgoing";
1505
+ const labelStr = this.formatLabel(label);
1506
+ const relPattern = this.formatRelationship(type, relAlias, options);
1507
+ const targetNode = `(${alias}${labelStr})`;
1508
+ let fullPattern;
1509
+ switch (direction) {
1510
+ case "incoming":
1511
+ fullPattern = `<-${relPattern}-${targetNode}`;
1512
+ break;
1513
+ case "both":
1514
+ fullPattern = `-${relPattern}-${targetNode}`;
1515
+ break;
1516
+ default:
1517
+ fullPattern = `-${relPattern}->${targetNode}`;
1518
+ }
1519
+ const lastMatch = this.state.matchClauses[this.state.matchClauses.length - 1];
1520
+ const updatedMatch = lastMatch + fullPattern;
1521
+ const newMatchClauses = [...this.state.matchClauses.slice(0, -1), updatedMatch];
1522
+ return this.clone({
1523
+ matchClauses: newMatchClauses,
1524
+ currentNode: alias,
1525
+ pendingRel: void 0
1526
+ });
1527
+ }
1528
+ /**
1529
+ * Add a WHERE clause
1530
+ *
1531
+ * @param condition - WHERE condition
1532
+ * @param params - Optional parameters
1533
+ */
1534
+ where(condition, params) {
1535
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1536
+ return this.clone({
1537
+ whereClauses: [...this.state.whereClauses, condition],
1538
+ whereOperators: [...this.state.whereOperators],
1539
+ params: newParams
1540
+ });
1541
+ }
1542
+ /**
1543
+ * Add an AND WHERE clause
1544
+ *
1545
+ * @param condition - WHERE condition
1546
+ * @param params - Optional parameters
1547
+ */
1548
+ andWhere(condition, params) {
1549
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1550
+ return this.clone({
1551
+ whereClauses: [...this.state.whereClauses, condition],
1552
+ whereOperators: [...this.state.whereOperators, "AND"],
1553
+ params: newParams
1554
+ });
1555
+ }
1556
+ /**
1557
+ * Add an OR WHERE clause
1558
+ *
1559
+ * @param condition - WHERE condition
1560
+ * @param params - Optional parameters
1561
+ */
1562
+ orWhere(condition, params) {
1563
+ const newParams = params ? { ...this.state.params, ...params } : this.state.params;
1564
+ return this.clone({
1565
+ whereClauses: [...this.state.whereClauses, condition],
1566
+ whereOperators: [...this.state.whereOperators, "OR"],
1567
+ params: newParams
1568
+ });
1569
+ }
1570
+ /**
1571
+ * Add a vector NEAR clause for similarity search
1572
+ *
1573
+ * @param paramName - Parameter name (e.g., '$query', '$embedding')
1574
+ * @param vector - Vector data
1575
+ * @param options - NEAR options (topK)
1576
+ */
1577
+ nearVector(paramName, vector, options) {
1578
+ const cleanParamName = paramName.startsWith("$") ? paramName.slice(1) : paramName;
1579
+ const topKSuffix = options?.topK ? ` TOP ${options.topK}` : "";
1580
+ const condition = `vector NEAR $${cleanParamName}${topKSuffix}`;
1581
+ const newParams = { ...this.state.params, [cleanParamName]: vector };
1582
+ if (this.state.whereClauses.length === 0) {
1583
+ return this.clone({
1584
+ whereClauses: [condition],
1585
+ params: newParams
1586
+ });
1587
+ }
1588
+ return this.clone({
1589
+ whereClauses: [...this.state.whereClauses, condition],
1590
+ whereOperators: [...this.state.whereOperators, "AND"],
1591
+ params: newParams
1592
+ });
1593
+ }
1594
+ /**
1595
+ * Add LIMIT clause
1596
+ *
1597
+ * @param value - Maximum number of results
1598
+ */
1599
+ limit(value) {
1600
+ if (value < 0) {
1601
+ throw new Error("LIMIT must be non-negative");
1602
+ }
1603
+ return this.clone({ limitValue: value });
1604
+ }
1605
+ /**
1606
+ * Add OFFSET clause
1607
+ *
1608
+ * @param value - Number of results to skip
1609
+ */
1610
+ offset(value) {
1611
+ if (value < 0) {
1612
+ throw new Error("OFFSET must be non-negative");
1613
+ }
1614
+ return this.clone({ offsetValue: value });
1615
+ }
1616
+ /**
1617
+ * Add ORDER BY clause
1618
+ *
1619
+ * @param field - Field to order by
1620
+ * @param direction - Sort direction (ASC or DESC)
1621
+ */
1622
+ orderBy(field, direction) {
1623
+ const orderClause = direction ? `${field} ${direction}` : field;
1624
+ return this.clone({ orderByClause: orderClause });
1625
+ }
1626
+ /**
1627
+ * Add RETURN clause with specific fields
1628
+ *
1629
+ * @param fields - Fields to return (array or object with aliases)
1630
+ */
1631
+ return(fields) {
1632
+ let returnClause;
1633
+ if (Array.isArray(fields)) {
1634
+ returnClause = fields.join(", ");
1635
+ } else {
1636
+ returnClause = Object.entries(fields).map(([field, alias]) => `${field} AS ${alias}`).join(", ");
1637
+ }
1638
+ return this.clone({ returnClause });
1639
+ }
1640
+ /**
1641
+ * Add RETURN * clause
1642
+ */
1643
+ returnAll() {
1644
+ return this.clone({ returnClause: "*" });
1645
+ }
1646
+ /**
1647
+ * Set fusion strategy for hybrid queries
1648
+ *
1649
+ * @param strategy - Fusion strategy
1650
+ * @param options - Fusion parameters
1651
+ */
1652
+ fusion(strategy, options) {
1653
+ return this.clone({
1654
+ fusionOptions: {
1655
+ strategy,
1656
+ ...options
1657
+ }
1658
+ });
1659
+ }
1660
+ /**
1661
+ * Get the fusion options
1662
+ */
1663
+ getFusionOptions() {
1664
+ return this.state.fusionOptions;
1665
+ }
1666
+ /**
1667
+ * Get all parameters
1668
+ */
1669
+ getParams() {
1670
+ return { ...this.state.params };
1671
+ }
1672
+ /**
1673
+ * Build the VelesQL query string
1674
+ */
1675
+ toVelesQL() {
1676
+ if (this.state.matchClauses.length === 0) {
1677
+ throw new Error("Query must have at least one MATCH clause");
1678
+ }
1679
+ const parts = [];
1680
+ parts.push(`MATCH ${this.state.matchClauses.join(", ")}`);
1681
+ if (this.state.whereClauses.length > 0) {
1682
+ const whereStr = this.buildWhereClause();
1683
+ parts.push(`WHERE ${whereStr}`);
1684
+ }
1685
+ if (this.state.orderByClause) {
1686
+ parts.push(`ORDER BY ${this.state.orderByClause}`);
1687
+ }
1688
+ if (this.state.limitValue !== void 0) {
1689
+ parts.push(`LIMIT ${this.state.limitValue}`);
1690
+ }
1691
+ if (this.state.offsetValue !== void 0) {
1692
+ parts.push(`OFFSET ${this.state.offsetValue}`);
1693
+ }
1694
+ if (this.state.returnClause) {
1695
+ parts.push(`RETURN ${this.state.returnClause}`);
1696
+ }
1697
+ if (this.state.fusionOptions) {
1698
+ parts.push(`/* FUSION ${this.state.fusionOptions.strategy} */`);
1699
+ }
1700
+ return parts.join(" ");
1701
+ }
1702
+ formatLabel(label) {
1703
+ if (!label) return "";
1704
+ if (Array.isArray(label)) {
1705
+ return label.map((l) => `:${l}`).join("");
1706
+ }
1707
+ return `:${label}`;
1708
+ }
1709
+ formatRelationship(type, alias, options) {
1710
+ const aliasStr = alias ? alias : "";
1711
+ const hopsStr = this.formatHops(options);
1712
+ if (alias) {
1713
+ return `[${aliasStr}:${type}${hopsStr}]`;
1714
+ }
1715
+ return `[:${type}${hopsStr}]`;
1716
+ }
1717
+ formatHops(options) {
1718
+ if (!options?.minHops && !options?.maxHops) return "";
1719
+ const min = options.minHops ?? 1;
1720
+ const max = options.maxHops ?? "";
1721
+ return `*${min}..${max}`;
1722
+ }
1723
+ buildWhereClause() {
1724
+ if (this.state.whereClauses.length === 0) return "";
1725
+ const first = this.state.whereClauses[0];
1726
+ if (!first) return "";
1727
+ let result = first;
1728
+ for (let i = 1; i < this.state.whereClauses.length; i++) {
1729
+ const operator = this.state.whereOperators[i - 1] ?? "AND";
1730
+ const clause = this.state.whereClauses[i];
1731
+ if (clause) {
1732
+ result += ` ${operator} ${clause}`;
1733
+ }
1734
+ }
1735
+ return result;
1736
+ }
1737
+ };
1738
+ function velesql() {
1739
+ return new VelesQLBuilder();
1740
+ }
1370
1741
  export {
1371
1742
  ConnectionError,
1372
1743
  NotFoundError,
@@ -1374,5 +1745,7 @@ export {
1374
1745
  ValidationError,
1375
1746
  VelesDB,
1376
1747
  VelesDBError,
1377
- WasmBackend
1748
+ VelesQLBuilder,
1749
+ WasmBackend,
1750
+ velesql
1378
1751
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wiscale/velesdb-sdk",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "VelesDB TypeScript SDK: The Local Vector Database for AI & RAG. Microsecond semantic search in Browser & Node.js.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -66,7 +66,7 @@
66
66
  "vitest": "^4.0.16"
67
67
  },
68
68
  "dependencies": {
69
- "@wiscale/velesdb-wasm": "^1.1.0"
69
+ "@wiscale/velesdb-wasm": "^1.4.0"
70
70
  },
71
71
  "engines": {
72
72
  "node": ">=18.0.0"