ydb-qdrant 4.8.0 → 4.8.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.
@@ -1,4 +1,4 @@
1
- import { TypedValues, withSession } from "../ydb/client.js";
1
+ import { TypedValues, withSession, createExecuteQuerySettings, } from "../ydb/client.js";
2
2
  import { mapDistanceToIndexParam } from "../utils/distance.js";
3
3
  import { COLLECTION_STORAGE_MODE, isOneTableMode, } from "../config/env.js";
4
4
  import { GLOBAL_POINTS_TABLE } from "../ydb/schema.js";
@@ -20,9 +20,10 @@ export async function getCollectionMeta(metaKey) {
20
20
  WHERE collection = $collection;
21
21
  `;
22
22
  const res = await withSession(async (s) => {
23
+ const settings = createExecuteQuerySettings();
23
24
  return await s.executeQuery(qry, {
24
25
  $collection: TypedValues.utf8(metaKey),
25
- });
26
+ }, undefined, settings);
26
27
  });
27
28
  const rowset = res.resultSets?.[0];
28
29
  if (!rowset || rowset.rows?.length !== 1)
@@ -1,4 +1,4 @@
1
- import { Types, TypedValues, withSession, TableDescription, Column, } from "../ydb/client.js";
1
+ import { Types, TypedValues, withSession, TableDescription, Column, createExecuteQuerySettings, } from "../ydb/client.js";
2
2
  import { upsertCollectionMeta } from "./collectionsRepo.shared.js";
3
3
  export async function createCollectionMultiTable(metaKey, dim, distance, vectorType, tableName) {
4
4
  await withSession(async (s) => {
@@ -18,6 +18,7 @@ export async function deleteCollectionMultiTable(metaKey, tableName) {
18
18
  DELETE FROM qdr__collections WHERE collection = $collection;
19
19
  `;
20
20
  await withSession(async (s) => {
21
- await s.executeQuery(delMeta, { $collection: TypedValues.utf8(metaKey) });
21
+ const settings = createExecuteQuerySettings();
22
+ await s.executeQuery(delMeta, { $collection: TypedValues.utf8(metaKey) }, undefined, settings);
22
23
  });
23
24
  }
@@ -1,6 +1,63 @@
1
- import { TypedValues, withSession } from "../ydb/client.js";
1
+ import { TypedValues, Types, withSession, createExecuteQuerySettings, } from "../ydb/client.js";
2
2
  import { GLOBAL_POINTS_TABLE, ensureGlobalPointsTable } from "../ydb/schema.js";
3
3
  import { upsertCollectionMeta } from "./collectionsRepo.shared.js";
4
+ import { withRetry, isTransientYdbError } from "../utils/retry.js";
5
+ const DELETE_COLLECTION_BATCH_SIZE = 10000;
6
+ function isOutOfBufferMemoryYdbError(error) {
7
+ const msg = error instanceof Error ? error.message : String(error);
8
+ if (/Out of buffer memory/i.test(msg)) {
9
+ return true;
10
+ }
11
+ if (typeof error === "object" && error !== null) {
12
+ const issues = error.issues;
13
+ if (issues !== undefined) {
14
+ const issuesText = typeof issues === "string" ? issues : JSON.stringify(issues);
15
+ return /Out of buffer memory/i.test(issuesText);
16
+ }
17
+ }
18
+ return false;
19
+ }
20
+ async function deletePointsForUidInChunks(s, uid) {
21
+ const selectYql = `
22
+ DECLARE $uid AS Utf8;
23
+ DECLARE $limit AS Uint32;
24
+ SELECT point_id
25
+ FROM ${GLOBAL_POINTS_TABLE}
26
+ WHERE uid = $uid
27
+ LIMIT $limit;
28
+ `;
29
+ const deleteBatchYql = `
30
+ DECLARE $uid AS Utf8;
31
+ DECLARE $ids AS List<Utf8>;
32
+ DELETE FROM ${GLOBAL_POINTS_TABLE}
33
+ WHERE uid = $uid AND point_id IN $ids;
34
+ `;
35
+ // Best‑effort loop: stop when there are no more rows for this uid.
36
+ // Each iteration only touches a limited number of rows to avoid
37
+ // hitting YDB's per‑operation buffer limits.
38
+ let iterations = 0;
39
+ const MAX_ITERATIONS = 1000;
40
+ const settings = createExecuteQuerySettings();
41
+ while (iterations++ < MAX_ITERATIONS) {
42
+ const rs = (await s.executeQuery(selectYql, {
43
+ $uid: TypedValues.utf8(uid),
44
+ $limit: TypedValues.uint32(DELETE_COLLECTION_BATCH_SIZE),
45
+ }, undefined, settings));
46
+ const rowset = rs.resultSets?.[0];
47
+ const rows = rowset?.rows ?? [];
48
+ const ids = rows
49
+ .map((row) => row.items?.[0]?.textValue)
50
+ .filter((id) => typeof id === "string");
51
+ if (ids.length === 0) {
52
+ break;
53
+ }
54
+ const idsValue = TypedValues.list(Types.UTF8, ids);
55
+ await s.executeQuery(deleteBatchYql, {
56
+ $uid: TypedValues.utf8(uid),
57
+ $ids: idsValue,
58
+ }, undefined, settings);
59
+ }
60
+ }
4
61
  export async function createCollectionOneTable(metaKey, dim, distance, vectorType) {
5
62
  await upsertCollectionMeta(metaKey, dim, distance, vectorType, GLOBAL_POINTS_TABLE);
6
63
  }
@@ -10,16 +67,36 @@ export async function deleteCollectionOneTable(metaKey, uid) {
10
67
  DECLARE $uid AS Utf8;
11
68
  DELETE FROM ${GLOBAL_POINTS_TABLE} WHERE uid = $uid;
12
69
  `;
13
- await withSession(async (s) => {
14
- await s.executeQuery(deletePointsYql, {
15
- $uid: TypedValues.utf8(uid),
16
- });
70
+ await withRetry(() => withSession(async (s) => {
71
+ const settings = createExecuteQuerySettings();
72
+ try {
73
+ await s.executeQuery(deletePointsYql, {
74
+ $uid: TypedValues.utf8(uid),
75
+ }, undefined, settings);
76
+ }
77
+ catch (err) {
78
+ if (!isOutOfBufferMemoryYdbError(err)) {
79
+ throw err;
80
+ }
81
+ await deletePointsForUidInChunks(s, uid);
82
+ }
83
+ }), {
84
+ isTransient: isTransientYdbError,
85
+ context: {
86
+ operation: "deleteCollectionOneTable",
87
+ tableName: GLOBAL_POINTS_TABLE,
88
+ metaKey,
89
+ uid,
90
+ },
17
91
  });
18
92
  const delMeta = `
19
93
  DECLARE $collection AS Utf8;
20
94
  DELETE FROM qdr__collections WHERE collection = $collection;
21
95
  `;
22
96
  await withSession(async (s) => {
23
- await s.executeQuery(delMeta, { $collection: TypedValues.utf8(metaKey) });
97
+ const settings = createExecuteQuerySettings();
98
+ await s.executeQuery(delMeta, {
99
+ $collection: TypedValues.utf8(metaKey),
100
+ }, undefined, settings);
24
101
  });
25
102
  }
@@ -1,4 +1,4 @@
1
- import { Types, TypedValues, withSession } from "../ydb/client.js";
1
+ import { Types, TypedValues, withSession, createExecuteQuerySettings, } from "../ydb/client.js";
2
2
  import { buildVectorParam } from "../ydb/helpers.js";
3
3
  import { logger } from "../logging/logger.js";
4
4
  import { notifyUpsert } from "../indexing/IndexScheduler.js";
@@ -15,6 +15,7 @@ export async function upsertPointsMultiTable(tableName, points, dimension) {
15
15
  }
16
16
  let upserted = 0;
17
17
  await withSession(async (s) => {
18
+ const settings = createExecuteQuerySettings();
18
19
  for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
19
20
  const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
20
21
  const ddl = `
@@ -44,7 +45,7 @@ export async function upsertPointsMultiTable(tableName, points, dimension) {
44
45
  const params = {
45
46
  $rows: rowsValue,
46
47
  };
47
- await withRetry(() => s.executeQuery(ddl, params), {
48
+ await withRetry(() => s.executeQuery(ddl, params, undefined, settings), {
48
49
  isTransient: isTransientYdbError,
49
50
  context: { tableName, batchSize: batch.length },
50
51
  });
@@ -64,6 +65,7 @@ export async function searchPointsMultiTable(tableName, queryVector, top, withPa
64
65
  $qf: qf,
65
66
  $k2: TypedValues.uint32(top),
66
67
  };
68
+ const settings = createExecuteQuerySettings();
67
69
  const buildQuery = (useIndex) => `
68
70
  DECLARE $qf AS List<Float>;
69
71
  DECLARE $k2 AS Uint32;
@@ -77,7 +79,7 @@ export async function searchPointsMultiTable(tableName, queryVector, top, withPa
77
79
  if (VECTOR_INDEX_BUILD_ENABLED) {
78
80
  try {
79
81
  rs = await withSession(async (s) => {
80
- return await s.executeQuery(buildQuery(true), params);
82
+ return await s.executeQuery(buildQuery(true), params, undefined, settings);
81
83
  });
82
84
  logger.info({ tableName }, "vector index found; using index for search");
83
85
  }
@@ -87,7 +89,7 @@ export async function searchPointsMultiTable(tableName, queryVector, top, withPa
87
89
  if (indexUnavailable) {
88
90
  logger.info({ tableName }, "vector index not available (missing or building); falling back to table scan");
89
91
  rs = await withSession(async (s) => {
90
- return await s.executeQuery(buildQuery(false), params);
92
+ return await s.executeQuery(buildQuery(false), params, undefined, settings);
91
93
  });
92
94
  }
93
95
  else {
@@ -97,7 +99,7 @@ export async function searchPointsMultiTable(tableName, queryVector, top, withPa
97
99
  }
98
100
  else {
99
101
  rs = await withSession(async (s) => {
100
- return await s.executeQuery(buildQuery(false), params);
102
+ return await s.executeQuery(buildQuery(false), params, undefined, settings);
101
103
  });
102
104
  }
103
105
  const rowset = rs.resultSets?.[0];
@@ -128,6 +130,7 @@ export async function searchPointsMultiTable(tableName, queryVector, top, withPa
128
130
  export async function deletePointsMultiTable(tableName, ids) {
129
131
  let deleted = 0;
130
132
  await withSession(async (s) => {
133
+ const settings = createExecuteQuerySettings();
131
134
  for (const id of ids) {
132
135
  const yql = `
133
136
  DECLARE $id AS Utf8;
@@ -136,7 +139,7 @@ export async function deletePointsMultiTable(tableName, ids) {
136
139
  const params = {
137
140
  $id: TypedValues.utf8(String(id)),
138
141
  };
139
- await s.executeQuery(yql, params);
142
+ await s.executeQuery(yql, params, undefined, settings);
140
143
  deleted += 1;
141
144
  }
142
145
  });
@@ -1,4 +1,4 @@
1
- import { TypedValues, Types, withSession } from "../ydb/client.js";
1
+ import { TypedValues, Types, withSession, createExecuteQuerySettings, } from "../ydb/client.js";
2
2
  import { buildVectorParam, buildVectorBinaryParams } from "../ydb/helpers.js";
3
3
  import { notifyUpsert } from "../indexing/IndexScheduler.js";
4
4
  import { mapDistanceToKnnFn, mapDistanceToBitKnnFn, } from "../utils/distance.js";
@@ -25,6 +25,7 @@ export async function upsertPointsOneTable(tableName, points, dimension, uid) {
25
25
  }
26
26
  let upserted = 0;
27
27
  await withSession(async (s) => {
28
+ const settings = createExecuteQuerySettings();
28
29
  for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
29
30
  const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
30
31
  let ddl;
@@ -120,7 +121,7 @@ export async function upsertPointsOneTable(tableName, points, dimension, uid) {
120
121
  })),
121
122
  },
122
123
  }, "one_table upsert: executing YQL");
123
- await withRetry(() => s.executeQuery(ddl, params), {
124
+ await withRetry(() => s.executeQuery(ddl, params, undefined, settings), {
124
125
  isTransient: isTransientYdbError,
125
126
  context: { tableName, batchSize: batch.length },
126
127
  });
@@ -193,7 +194,8 @@ async function searchPointsOneTableExact(tableName, queryVector, top, withPayloa
193
194
  vectorPreview: queryVector.slice(0, 3),
194
195
  },
195
196
  }, "one_table search (exact): executing YQL");
196
- const rs = await s.executeQuery(yql, params);
197
+ const settings = createExecuteQuerySettings();
198
+ const rs = await s.executeQuery(yql, params, undefined, settings);
197
199
  const rowset = rs.resultSets?.[0];
198
200
  const rows = (rowset?.rows ?? []);
199
201
  return rows.map((row) => {
@@ -334,7 +336,8 @@ async function searchPointsOneTableApproximate(tableName, queryVector, top, with
334
336
  },
335
337
  }, "one_table search (approximate): executing YQL");
336
338
  }
337
- const rs = await s.executeQuery(yql, params);
339
+ const settings = createExecuteQuerySettings();
340
+ const rs = await s.executeQuery(yql, params, undefined, settings);
338
341
  const rowset = rs.resultSets?.[0];
339
342
  const rows = (rowset?.rows ?? []);
340
343
  return rows.map((row) => {
@@ -371,6 +374,7 @@ export async function searchPointsOneTable(tableName, queryVector, top, withPayl
371
374
  export async function deletePointsOneTable(tableName, ids, uid) {
372
375
  let deleted = 0;
373
376
  await withSession(async (s) => {
377
+ const settings = createExecuteQuerySettings();
374
378
  for (const id of ids) {
375
379
  const yql = `
376
380
  DECLARE $uid AS Utf8;
@@ -381,7 +385,7 @@ export async function deletePointsOneTable(tableName, ids, uid) {
381
385
  $uid: TypedValues.utf8(uid),
382
386
  $id: TypedValues.utf8(String(id)),
383
387
  };
384
- await s.executeQuery(yql, params);
388
+ await s.executeQuery(yql, params, undefined, settings);
385
389
  deleted += 1;
386
390
  }
387
391
  });
@@ -3,7 +3,19 @@ const DEFAULT_MAX_RETRIES = 6;
3
3
  const DEFAULT_BASE_DELAY_MS = 250;
4
4
  export function isTransientYdbError(error) {
5
5
  const msg = error instanceof Error ? error.message : String(error);
6
- return /Aborted|schema version mismatch|Table metadata loading|Failed to load metadata/i.test(msg);
6
+ if (/Aborted|schema version mismatch|Table metadata loading|Failed to load metadata|overloaded|is in process of split|wrong shard state|Rejecting data TxId/i.test(msg)) {
7
+ return true;
8
+ }
9
+ if (typeof error === "object" && error !== null) {
10
+ const issues = error.issues;
11
+ if (issues !== undefined) {
12
+ const issuesText = typeof issues === "string" ? issues : JSON.stringify(issues);
13
+ if (/overloaded|is in process of split|wrong shard state|Rejecting data TxId/i.test(issuesText)) {
14
+ return true;
15
+ }
16
+ }
17
+ }
18
+ return false;
7
19
  }
8
20
  export async function withRetry(fn, options = {}) {
9
21
  const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
@@ -1,6 +1,10 @@
1
- import type { Session, IAuthService } from "ydb-sdk";
2
- declare const Types: typeof import("ydb-sdk").Types, TypedValues: typeof import("ydb-sdk").TypedValues, TableDescription: typeof import("ydb-sdk").TableDescription, Column: typeof import("ydb-sdk").Column;
3
- export { Types, TypedValues, TableDescription, Column };
1
+ import type { Session, IAuthService, ExecuteQuerySettings as YdbExecuteQuerySettings } from "ydb-sdk";
2
+ declare const Types: typeof import("ydb-sdk").Types, TypedValues: typeof import("ydb-sdk").TypedValues, TableDescription: typeof import("ydb-sdk").TableDescription, Column: typeof import("ydb-sdk").Column, ExecuteQuerySettings: typeof YdbExecuteQuerySettings;
3
+ export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings };
4
+ export declare function createExecuteQuerySettings(options?: {
5
+ keepInCache?: boolean;
6
+ idempotent?: boolean;
7
+ }): YdbExecuteQuerySettings;
4
8
  type DriverConfig = {
5
9
  endpoint?: string;
6
10
  database?: string;
@@ -2,8 +2,19 @@ import { createRequire } from "module";
2
2
  import { YDB_DATABASE, YDB_ENDPOINT, SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, } from "../config/env.js";
3
3
  import { logger } from "../logging/logger.js";
4
4
  const require = createRequire(import.meta.url);
5
- const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, } = require("ydb-sdk");
6
- export { Types, TypedValues, TableDescription, Column };
5
+ const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, } = require("ydb-sdk");
6
+ export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings };
7
+ export function createExecuteQuerySettings(options) {
8
+ const { keepInCache = true, idempotent = true } = options ?? {};
9
+ const settings = new ExecuteQuerySettings();
10
+ if (keepInCache) {
11
+ settings.withKeepInCache(true);
12
+ }
13
+ if (idempotent) {
14
+ settings.withIdempotent(true);
15
+ }
16
+ return settings;
17
+ }
7
18
  const DRIVER_READY_TIMEOUT_MS = 15000;
8
19
  const TABLE_SESSION_TIMEOUT_MS = 20000;
9
20
  const YDB_HEALTHCHECK_READY_TIMEOUT_MS = 5000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-qdrant",
3
- "version": "4.8.0",
3
+ "version": "4.8.1",
4
4
  "main": "dist/package/api.js",
5
5
  "types": "dist/package/api.d.ts",
6
6
  "exports": {