ydb-qdrant 6.0.0 → 7.0.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.
Files changed (56) hide show
  1. package/README.md +2 -2
  2. package/dist/config/env.d.ts +8 -3
  3. package/dist/config/env.js +15 -5
  4. package/dist/package/api.d.ts +2 -2
  5. package/dist/package/api.js +2 -2
  6. package/dist/qdrant/QdrantTypes.d.ts +19 -0
  7. package/dist/qdrant/QdrantTypes.js +1 -0
  8. package/dist/repositories/collectionsRepo.d.ts +2 -1
  9. package/dist/repositories/collectionsRepo.js +103 -62
  10. package/dist/repositories/collectionsRepo.one-table.js +47 -129
  11. package/dist/repositories/pointsRepo.d.ts +5 -7
  12. package/dist/repositories/pointsRepo.js +6 -3
  13. package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -0
  14. package/dist/repositories/pointsRepo.one-table/Delete.js +111 -0
  15. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +11 -0
  16. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +32 -0
  17. package/dist/repositories/pointsRepo.one-table/Search/Approximate.d.ts +18 -0
  18. package/dist/repositories/pointsRepo.one-table/Search/Approximate.js +119 -0
  19. package/dist/repositories/pointsRepo.one-table/Search/Exact.d.ts +17 -0
  20. package/dist/repositories/pointsRepo.one-table/Search/Exact.js +101 -0
  21. package/dist/repositories/pointsRepo.one-table/Search/index.d.ts +8 -0
  22. package/dist/repositories/pointsRepo.one-table/Search/index.js +30 -0
  23. package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -0
  24. package/dist/repositories/pointsRepo.one-table/Upsert.js +100 -0
  25. package/dist/repositories/pointsRepo.one-table.d.ts +3 -13
  26. package/dist/repositories/pointsRepo.one-table.js +3 -403
  27. package/dist/routes/collections.js +61 -7
  28. package/dist/routes/points.js +71 -3
  29. package/dist/server.d.ts +1 -0
  30. package/dist/server.js +70 -2
  31. package/dist/services/CollectionService.d.ts +9 -0
  32. package/dist/services/CollectionService.js +9 -0
  33. package/dist/services/PointsService.d.ts +8 -10
  34. package/dist/services/PointsService.js +78 -4
  35. package/dist/types.d.ts +72 -8
  36. package/dist/types.js +43 -17
  37. package/dist/utils/normalization.d.ts +1 -0
  38. package/dist/utils/normalization.js +15 -13
  39. package/dist/utils/retry.js +29 -19
  40. package/dist/utils/typeGuards.d.ts +1 -0
  41. package/dist/utils/typeGuards.js +3 -0
  42. package/dist/utils/vectorBinary.js +88 -9
  43. package/dist/ydb/QueryDiagnostics.d.ts +6 -0
  44. package/dist/ydb/QueryDiagnostics.js +52 -0
  45. package/dist/ydb/SessionPool.d.ts +36 -0
  46. package/dist/ydb/SessionPool.js +248 -0
  47. package/dist/ydb/bulkUpsert.d.ts +6 -0
  48. package/dist/ydb/bulkUpsert.js +52 -0
  49. package/dist/ydb/client.d.ts +17 -16
  50. package/dist/ydb/client.js +427 -62
  51. package/dist/ydb/helpers.d.ts +0 -2
  52. package/dist/ydb/helpers.js +0 -7
  53. package/dist/ydb/schema.js +171 -77
  54. package/package.json +12 -7
  55. package/dist/repositories/collectionsRepo.shared.d.ts +0 -2
  56. package/dist/repositories/collectionsRepo.shared.js +0 -26
@@ -1,104 +1,198 @@
1
- import { withSession, TableDescription, Column, Types, Ydb } from "./client.js";
1
+ import { withSession } from "./client.js";
2
2
  import { logger } from "../logging/logger.js";
3
- import { GLOBAL_POINTS_AUTOMIGRATE_ENABLED } from "../config/env.js";
3
+ import { STARTUP_PROBE_SESSION_TIMEOUT_MS } from "../config/env.js";
4
4
  export const GLOBAL_POINTS_TABLE = "qdrant_all_points";
5
5
  // Shared YDB-related constants for repositories.
6
6
  export { UPSERT_BATCH_SIZE } from "../config/env.js";
7
+ const SCHEMA_DDL_TIMEOUT_MS = 5000;
8
+ let metaTableReady = false;
9
+ let metaTableReadyInFlight = null;
7
10
  let globalPointsTableReady = false;
11
+ function collectIssueMessages(err) {
12
+ const out = [];
13
+ const seen = new Set();
14
+ // Hard guardrails to guarantee termination even for pathological error graphs.
15
+ const MAX_DEPTH = 8;
16
+ const MAX_NODES = 500;
17
+ const queue = [{ v: err, depth: 0 }];
18
+ let visited = 0;
19
+ while (queue.length > 0 && visited < MAX_NODES) {
20
+ const next = queue.shift();
21
+ if (!next)
22
+ break;
23
+ const { v, depth } = next;
24
+ if (depth > MAX_DEPTH)
25
+ continue;
26
+ if (v === null || typeof v !== "object")
27
+ continue;
28
+ if (seen.has(v))
29
+ continue;
30
+ seen.add(v);
31
+ visited++;
32
+ const maybeMessage = v.message;
33
+ if (typeof maybeMessage === "string" && maybeMessage.length > 0) {
34
+ out.push(maybeMessage);
35
+ }
36
+ const maybeIssues = v.issues;
37
+ if (Array.isArray(maybeIssues)) {
38
+ for (const child of maybeIssues) {
39
+ queue.push({ v: child, depth: depth + 1 });
40
+ }
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+ function isAlreadyExistsError(err) {
46
+ const msg = err instanceof Error ? err.message : String(err);
47
+ if (/already exists/i.test(msg) || /path exists/i.test(msg)) {
48
+ return true;
49
+ }
50
+ // YDBError often carries the useful text in nested `issues`, while `message`
51
+ // is a generic wrapper like "Type annotation".
52
+ const issueMsgs = collectIssueMessages(err).join("\n");
53
+ return (/already exists/i.test(issueMsgs) ||
54
+ /path exists/i.test(issueMsgs) ||
55
+ /table name conflict/i.test(issueMsgs));
56
+ }
57
+ function isUnknownColumnError(err) {
58
+ const msg = err instanceof Error ? err.message : String(err);
59
+ const re = /unknown column|cannot resolve|member not found/i;
60
+ if (re.test(msg)) {
61
+ return true;
62
+ }
63
+ // YDBError may carry the real message in nested `issues`.
64
+ const issueMsgs = collectIssueMessages(err).join("\n");
65
+ return re.test(issueMsgs);
66
+ }
8
67
  function throwMigrationRequired(message) {
9
68
  logger.error(message);
10
69
  throw new Error(message);
11
70
  }
12
- export async function ensureMetaTable() {
13
- try {
14
- await withSession(async (s) => {
15
- // If table exists, describeTable will succeed
16
- try {
17
- const tableDescription = await s.describeTable("qdr__collections");
18
- const columns = tableDescription.columns ?? [];
19
- const hasLastAccessedAt = columns.some((col) => col.name === "last_accessed_at");
20
- if (!hasLastAccessedAt) {
21
- const alterDdl = `
22
- ALTER TABLE qdr__collections
23
- ADD COLUMN last_accessed_at Timestamp;
24
- `;
25
- // NOTE: ydb-sdk's public TableSession type does not surface executeSchemeQuery,
26
- // but the underlying implementation provides it. This cast relies on the
27
- // current ydb-sdk internals (tested with ydb-sdk v5.11.1) to run ALTER TABLE
28
- // as a scheme query. If the SDK changes its internal API, this may need to be
29
- // revisited or replaced with an officially supported migration mechanism.
30
- const rawSession = s;
31
- await rawSession.api.executeSchemeQuery({
32
- sessionId: rawSession.sessionId,
33
- yqlText: alterDdl,
34
- });
35
- logger.info("added last_accessed_at column to metadata table qdr__collections");
71
+ async function ensureMetaTableOnce() {
72
+ await withSession(async (sql, signal) => {
73
+ try {
74
+ await sql `
75
+ CREATE TABLE qdr__collections (
76
+ collection Utf8,
77
+ table_name Utf8,
78
+ vector_dimension Uint32,
79
+ distance Utf8,
80
+ vector_type Utf8,
81
+ created_at Timestamp,
82
+ last_accessed_at Timestamp,
83
+ PRIMARY KEY (collection)
84
+ );
85
+ `
86
+ .idempotent(true)
87
+ .timeout(SCHEMA_DDL_TIMEOUT_MS)
88
+ .signal(signal);
89
+ logger.info("created metadata table qdr__collections");
90
+ }
91
+ catch (err) {
92
+ if (!isAlreadyExistsError(err)) {
93
+ // YDB may return non-"already exists" errors for concurrent CREATE TABLE attempts
94
+ // or name resolution conflicts. Probe existence before failing startup.
95
+ try {
96
+ await sql `SELECT collection FROM qdr__collections LIMIT 0;`
97
+ .idempotent(true)
98
+ .timeout(STARTUP_PROBE_SESSION_TIMEOUT_MS)
99
+ .signal(signal);
100
+ logger.warn({ err }, "CREATE TABLE qdr__collections failed, but the table appears to exist; continuing");
101
+ }
102
+ catch {
103
+ throw err;
36
104
  }
37
- return;
38
105
  }
39
- catch {
40
- // create via schema API
41
- const desc = new TableDescription()
42
- .withColumns(new Column("collection", Types.UTF8), new Column("table_name", Types.UTF8), new Column("vector_dimension", Types.UINT32), new Column("distance", Types.UTF8), new Column("vector_type", Types.UTF8), new Column("created_at", Types.TIMESTAMP), new Column("last_accessed_at", Types.TIMESTAMP))
43
- .withPrimaryKey("collection");
44
- await s.createTable("qdr__collections", desc);
45
- logger.info("created metadata table qdr__collections");
106
+ }
107
+ // Fail fast if schema is old/mismatched: we do not auto-migrate tables.
108
+ try {
109
+ await sql `SELECT last_accessed_at FROM qdr__collections LIMIT 0;`
110
+ .idempotent(true)
111
+ .timeout(STARTUP_PROBE_SESSION_TIMEOUT_MS)
112
+ .signal(signal);
113
+ }
114
+ catch (err) {
115
+ if (!isUnknownColumnError(err)) {
116
+ throw err;
46
117
  }
47
- });
118
+ throwMigrationRequired("Metadata table qdr__collections is missing required column last_accessed_at; apply a manual migration (ALTER TABLE qdr__collections ADD COLUMN last_accessed_at Timestamp).");
119
+ }
120
+ });
121
+ metaTableReady = true;
122
+ }
123
+ export async function ensureMetaTable() {
124
+ if (metaTableReady) {
125
+ return;
126
+ }
127
+ if (metaTableReadyInFlight) {
128
+ await metaTableReadyInFlight;
129
+ return;
130
+ }
131
+ metaTableReadyInFlight = ensureMetaTableOnce();
132
+ try {
133
+ await metaTableReadyInFlight;
48
134
  }
49
- catch (err) {
50
- logger.warn({ err }, "ensureMetaTable: failed to verify or migrate qdr__collections; subsequent operations may fail if schema is incomplete");
135
+ finally {
136
+ metaTableReadyInFlight = null;
51
137
  }
52
138
  }
53
139
  export async function ensureGlobalPointsTable() {
54
140
  if (globalPointsTableReady) {
55
141
  return;
56
142
  }
57
- await withSession(async (s) => {
58
- let tableDescription = null;
143
+ await withSession(async (sql, signal) => {
59
144
  try {
60
- tableDescription = await s.describeTable(GLOBAL_POINTS_TABLE);
61
- }
62
- catch {
63
- // Table doesn't exist, create it with all columns using the new schema and
64
- // auto-partitioning enabled.
65
- const desc = new TableDescription()
66
- .withColumns(new Column("uid", Types.UTF8), new Column("point_id", Types.UTF8), new Column("embedding", Types.BYTES), new Column("embedding_quantized", Types.BYTES), new Column("payload", Types.JSON_DOCUMENT))
67
- .withPrimaryKeys("uid", "point_id");
68
- desc.withPartitioningSettings({
69
- partitioningByLoad: Ydb.FeatureFlag.Status.ENABLED,
70
- partitioningBySize: Ydb.FeatureFlag.Status.ENABLED,
71
- partitionSizeMb: 100,
72
- });
73
- await s.createTable(GLOBAL_POINTS_TABLE, desc);
74
- globalPointsTableReady = true;
145
+ await sql `
146
+ CREATE TABLE ${sql.identifier(GLOBAL_POINTS_TABLE)} (
147
+ uid Utf8,
148
+ point_id Utf8,
149
+ embedding String,
150
+ embedding_quantized String,
151
+ payload JsonDocument,
152
+ PRIMARY KEY (uid, point_id)
153
+ )
154
+ WITH (
155
+ AUTO_PARTITIONING_BY_LOAD = ENABLED,
156
+ AUTO_PARTITIONING_BY_SIZE = ENABLED,
157
+ AUTO_PARTITIONING_PARTITION_SIZE_MB = 100
158
+ );
159
+ `
160
+ .idempotent(true)
161
+ .timeout(SCHEMA_DDL_TIMEOUT_MS)
162
+ .signal(signal);
75
163
  logger.info(`created global points table ${GLOBAL_POINTS_TABLE}`);
76
- return;
77
164
  }
78
- // Table exists, require the new embedding_quantized column.
79
- const columns = tableDescription.columns ?? [];
80
- const hasEmbeddingQuantized = columns.some((col) => col.name === "embedding_quantized");
81
- if (!hasEmbeddingQuantized) {
82
- if (!GLOBAL_POINTS_AUTOMIGRATE_ENABLED) {
83
- throwMigrationRequired(`Global points table ${GLOBAL_POINTS_TABLE} is missing required column embedding_quantized; apply the migration (e.g., ALTER TABLE ${GLOBAL_POINTS_TABLE} RENAME COLUMN embedding_bit TO embedding_quantized) or set YDB_QDRANT_GLOBAL_POINTS_AUTOMIGRATE=true after backup to allow automatic migration.`);
165
+ catch (err) {
166
+ if (!isAlreadyExistsError(err)) {
167
+ // YDB may return non-"already exists" errors for concurrent CREATE TABLE attempts
168
+ // or name resolution conflicts. Probe existence before failing startup.
169
+ try {
170
+ await sql `SELECT uid FROM ${sql.identifier(GLOBAL_POINTS_TABLE)} LIMIT 0;`
171
+ .idempotent(true)
172
+ .timeout(STARTUP_PROBE_SESSION_TIMEOUT_MS)
173
+ .signal(signal);
174
+ logger.warn({ err }, `CREATE TABLE ${GLOBAL_POINTS_TABLE} failed, but the table appears to exist; continuing`);
175
+ }
176
+ catch {
177
+ // If the table doesn't exist but CREATE TABLE failed for another reason,
178
+ // let the error surface; callers depend on the table being present.
179
+ throw err;
180
+ }
181
+ }
182
+ }
183
+ // Fail fast if schema is old/mismatched: we do not auto-migrate tables.
184
+ try {
185
+ await sql `SELECT embedding_quantized FROM ${sql.identifier(GLOBAL_POINTS_TABLE)} LIMIT 0;`
186
+ .idempotent(true)
187
+ .timeout(STARTUP_PROBE_SESSION_TIMEOUT_MS)
188
+ .signal(signal);
189
+ }
190
+ catch (err) {
191
+ if (!isUnknownColumnError(err)) {
192
+ throw err;
84
193
  }
85
- const alterDdl = `
86
- ALTER TABLE ${GLOBAL_POINTS_TABLE}
87
- ADD COLUMN embedding_quantized String;
88
- `;
89
- // NOTE: Same rationale as in ensureMetaTable: executeSchemeQuery is not part of
90
- // the public TableSession TypeScript surface, so we reach into the underlying
91
- // ydb-sdk implementation (verified with ydb-sdk v5.11.1) to apply schema changes.
92
- // If future SDK versions alter this shape, this cast and migration path must be
93
- // updated accordingly.
94
- const rawSession = s;
95
- await rawSession.api.executeSchemeQuery({
96
- sessionId: rawSession.sessionId,
97
- yqlText: alterDdl,
98
- });
99
- logger.info(`added embedding_quantized column to existing table ${GLOBAL_POINTS_TABLE}`);
194
+ throwMigrationRequired(`Global points table ${GLOBAL_POINTS_TABLE} is missing required column embedding_quantized; apply a manual migration (ALTER TABLE ${GLOBAL_POINTS_TABLE} ADD COLUMN embedding_quantized String). If your legacy schema used embedding_bit, rename it or recreate the table.`);
100
195
  }
101
- // Mark table ready after schema checks/migrations succeed.
102
196
  globalPointsTableReady = true;
103
197
  });
104
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-qdrant",
3
- "version": "6.0.0",
3
+ "version": "7.0.1",
4
4
  "main": "dist/package/api.js",
5
5
  "types": "dist/package/api.d.ts",
6
6
  "exports": {
@@ -65,16 +65,18 @@
65
65
  "dependencies": {
66
66
  "@bufbuild/protobuf": "^2.10.0",
67
67
  "@grpc/grpc-js": "^1.14.0",
68
+ "@qdrant/js-client-rest": "^1.16.2",
68
69
  "@yandex-cloud/nodejs-sdk": "^2.9.0",
69
- "@ydbjs/api": "^6.0.4",
70
- "@ydbjs/core": "^6.0.4",
71
- "@ydbjs/query": "^6.0.4",
72
- "@ydbjs/value": "^6.0.4",
70
+ "@ydbjs/api": "^6.0.5",
71
+ "@ydbjs/auth": "^6.0.5",
72
+ "@ydbjs/core": "^6.0.7",
73
+ "@ydbjs/query": "^6.0.7",
74
+ "@ydbjs/retry": "^6.0.5",
75
+ "@ydbjs/value": "^6.0.5",
73
76
  "dotenv": "^17.2.3",
74
77
  "express": "^5.1.0",
75
78
  "nice-grpc": "^2.1.13",
76
79
  "pino": "^10.1.0",
77
- "ydb-sdk": "^5.11.1",
78
80
  "zod": "^4.1.12"
79
81
  },
80
82
  "devDependencies": {
@@ -90,5 +92,8 @@
90
92
  "typescript": "^5.9.3",
91
93
  "typescript-eslint": "^8.47.0",
92
94
  "vitest": "^4.0.12"
95
+ },
96
+ "engines": {
97
+ "node": ">=20.19.0"
93
98
  }
94
- }
99
+ }
@@ -1,2 +0,0 @@
1
- import type { DistanceKind, VectorType } from "../types";
2
- export declare function upsertCollectionMeta(metaKey: string, dim: number, distance: DistanceKind, vectorType: VectorType, tableName: string): Promise<void>;
@@ -1,26 +0,0 @@
1
- import { TypedValues, withSession } from "../ydb/client.js";
2
- export async function upsertCollectionMeta(metaKey, dim, distance, vectorType, tableName) {
3
- const now = new Date();
4
- const upsertMeta = `
5
- DECLARE $collection AS Utf8;
6
- DECLARE $table AS Utf8;
7
- DECLARE $dim AS Uint32;
8
- DECLARE $distance AS Utf8;
9
- DECLARE $vtype AS Utf8;
10
- DECLARE $created AS Timestamp;
11
- DECLARE $last_accessed AS Timestamp;
12
- UPSERT INTO qdr__collections (collection, table_name, vector_dimension, distance, vector_type, created_at, last_accessed_at)
13
- VALUES ($collection, $table, $dim, $distance, $vtype, $created, $last_accessed);
14
- `;
15
- await withSession(async (s) => {
16
- await s.executeQuery(upsertMeta, {
17
- $collection: TypedValues.utf8(metaKey),
18
- $table: TypedValues.utf8(tableName),
19
- $dim: TypedValues.uint32(dim),
20
- $distance: TypedValues.utf8(distance),
21
- $vtype: TypedValues.utf8(vectorType),
22
- $created: TypedValues.timestamp(now),
23
- $last_accessed: TypedValues.timestamp(now),
24
- });
25
- });
26
- }