ydb-qdrant 4.7.2 → 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) => {
@@ -231,195 +233,114 @@ async function searchPointsOneTableApproximate(tableName, queryVector, top, with
231
233
  const rawCandidateLimit = safeTop * overfetchMultiplier;
232
234
  const candidateLimit = Math.max(safeTop, rawCandidateLimit);
233
235
  const results = await withSession(async (s) => {
236
+ let yql;
237
+ let params;
234
238
  if (CLIENT_SIDE_SERIALIZATION_ENABLED) {
235
239
  const binaries = buildVectorBinaryParams(queryVector);
236
- // Phase 1: approximate candidate selection using embedding_quantized
237
- const phase1Query = `
240
+ yql = `
238
241
  DECLARE $qbin_bit AS String;
239
- DECLARE $k AS Uint32;
242
+ DECLARE $qbinf AS String;
243
+ DECLARE $candidateLimit AS Uint32;
244
+ DECLARE $safeTop AS Uint32;
240
245
  DECLARE $uid AS Utf8;
241
- SELECT point_id
246
+
247
+ $candidates = (
248
+ SELECT point_id
249
+ FROM ${tableName}
250
+ WHERE uid = $uid AND embedding_quantized IS NOT NULL
251
+ ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
252
+ LIMIT $candidateLimit
253
+ );
254
+
255
+ SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
242
256
  FROM ${tableName}
243
- WHERE uid = $uid AND embedding_quantized IS NOT NULL
244
- ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
245
- LIMIT $k;
257
+ WHERE uid = $uid
258
+ AND point_id IN $candidates
259
+ ORDER BY score ${order}
260
+ LIMIT $safeTop;
246
261
  `;
262
+ params = {
263
+ $qbin_bit: typeof TypedValues.bytes === "function"
264
+ ? TypedValues.bytes(binaries.bit)
265
+ : binaries.bit,
266
+ $qbinf: typeof TypedValues.bytes === "function"
267
+ ? TypedValues.bytes(binaries.float)
268
+ : binaries.float,
269
+ $candidateLimit: TypedValues.uint32(candidateLimit),
270
+ $safeTop: TypedValues.uint32(safeTop),
271
+ $uid: TypedValues.utf8(uid),
272
+ };
247
273
  logger.debug({
248
274
  tableName,
249
275
  distance,
250
276
  top,
251
277
  safeTop,
252
278
  candidateLimit,
253
- mode: "one_table_approximate_phase1_client_side_serialization",
254
- yql: phase1Query,
279
+ mode: "one_table_approximate_client_side_serialization",
280
+ yql,
255
281
  params: {
256
282
  uid,
257
- top: candidateLimit,
283
+ safeTop,
284
+ candidateLimit,
258
285
  vectorLength: queryVector.length,
259
286
  vectorPreview: queryVector.slice(0, 3),
260
287
  },
261
- }, "one_table search (approximate, phase 1): executing YQL");
262
- const phase1Params = {
263
- $qbin_bit: typeof TypedValues.bytes === "function"
264
- ? TypedValues.bytes(binaries.bit)
265
- : binaries.bit,
266
- $k: TypedValues.uint32(candidateLimit),
267
- $uid: TypedValues.utf8(uid),
268
- };
269
- const rs1 = await s.executeQuery(phase1Query, phase1Params);
270
- const rowset1 = rs1.resultSets?.[0];
271
- const rows1 = (rowset1?.rows ?? []);
272
- const candidateIds = rows1
273
- .map((row) => row.items?.[0]?.textValue)
274
- .filter((id) => typeof id === "string");
275
- if (candidateIds.length === 0) {
276
- return [];
277
- }
278
- // Phase 2: exact re-ranking on full-precision embedding for candidates only
279
- const phase2Query = `
280
- DECLARE $qbinf AS String;
281
- DECLARE $k AS Uint32;
288
+ }, "one_table search (approximate): executing YQL with client-side serialization");
289
+ }
290
+ else {
291
+ const qf = buildVectorParam(queryVector);
292
+ yql = `
293
+ DECLARE $qf AS List<Float>;
294
+ DECLARE $candidateLimit AS Uint32;
295
+ DECLARE $safeTop AS Uint32;
282
296
  DECLARE $uid AS Utf8;
283
- DECLARE $ids AS List<Utf8>;
297
+
298
+ $qbin_bit = Knn::ToBinaryStringBit($qf);
299
+ $qbinf = Knn::ToBinaryStringFloat($qf);
300
+
301
+ $candidates = (
302
+ SELECT point_id
303
+ FROM ${tableName}
304
+ WHERE uid = $uid AND embedding_quantized IS NOT NULL
305
+ ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
306
+ LIMIT $candidateLimit
307
+ );
308
+
284
309
  SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
285
310
  FROM ${tableName}
286
- WHERE uid = $uid AND point_id IN $ids
311
+ WHERE uid = $uid
312
+ AND point_id IN $candidates
287
313
  ORDER BY score ${order}
288
- LIMIT $k;
314
+ LIMIT $safeTop;
289
315
  `;
316
+ params = {
317
+ $qf: qf,
318
+ $candidateLimit: TypedValues.uint32(candidateLimit),
319
+ $safeTop: TypedValues.uint32(safeTop),
320
+ $uid: TypedValues.utf8(uid),
321
+ };
290
322
  logger.debug({
291
323
  tableName,
292
324
  distance,
293
325
  top,
294
326
  safeTop,
295
- candidateCount: candidateIds.length,
296
- mode: "one_table_approximate_phase2_client_side_serialization",
297
- yql: phase2Query,
327
+ candidateLimit,
328
+ mode: "one_table_approximate",
329
+ yql,
298
330
  params: {
299
331
  uid,
300
- top: safeTop,
332
+ safeTop,
333
+ candidateLimit,
301
334
  vectorLength: queryVector.length,
302
335
  vectorPreview: queryVector.slice(0, 3),
303
- ids: candidateIds,
304
336
  },
305
- }, "one_table search (approximate, phase 2): executing YQL");
306
- const idsParam = TypedValues.list(Types.UTF8, candidateIds);
307
- const phase2Params = {
308
- $qbinf: typeof TypedValues.bytes === "function"
309
- ? TypedValues.bytes(binaries.float)
310
- : binaries.float,
311
- $k: TypedValues.uint32(safeTop),
312
- $uid: TypedValues.utf8(uid),
313
- $ids: idsParam,
314
- };
315
- const rs2 = await s.executeQuery(phase2Query, phase2Params);
316
- const rowset2 = rs2.resultSets?.[0];
317
- const rows2 = (rowset2?.rows ?? []);
318
- return rows2.map((row) => {
319
- const id = row.items?.[0]?.textValue;
320
- if (typeof id !== "string") {
321
- throw new Error("point_id is missing in YDB search result");
322
- }
323
- let payload;
324
- let scoreIdx = 1;
325
- if (withPayload) {
326
- const payloadText = row.items?.[1]?.textValue;
327
- if (payloadText) {
328
- try {
329
- payload = JSON.parse(payloadText);
330
- }
331
- catch {
332
- payload = undefined;
333
- }
334
- }
335
- scoreIdx = 2;
336
- }
337
- const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
338
- return { id, score, ...(payload ? { payload } : {}) };
339
- });
337
+ }, "one_table search (approximate): executing YQL");
340
338
  }
341
- const qf = buildVectorParam(queryVector);
342
- // Phase 1: approximate candidate selection using embedding_quantized
343
- const phase1Query = `
344
- DECLARE $qf AS List<Float>;
345
- DECLARE $k AS Uint32;
346
- DECLARE $uid AS Utf8;
347
- $qbin_bit = Knn::ToBinaryStringBit($qf);
348
- SELECT point_id
349
- FROM ${tableName}
350
- WHERE uid = $uid AND embedding_quantized IS NOT NULL
351
- ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
352
- LIMIT $k;
353
- `;
354
- logger.debug({
355
- tableName,
356
- distance,
357
- top,
358
- safeTop,
359
- candidateLimit,
360
- mode: "one_table_approximate_phase1",
361
- yql: phase1Query,
362
- params: {
363
- uid,
364
- top: candidateLimit,
365
- vectorLength: queryVector.length,
366
- vectorPreview: queryVector.slice(0, 3),
367
- },
368
- }, "one_table search (approximate, phase 1): executing YQL");
369
- const phase1Params = {
370
- $qf: qf,
371
- $k: TypedValues.uint32(candidateLimit),
372
- $uid: TypedValues.utf8(uid),
373
- };
374
- const rs1 = await s.executeQuery(phase1Query, phase1Params);
375
- const rowset1 = rs1.resultSets?.[0];
376
- const rows1 = (rowset1?.rows ?? []);
377
- const candidateIds = rows1
378
- .map((row) => row.items?.[0]?.textValue)
379
- .filter((id) => typeof id === "string");
380
- if (candidateIds.length === 0) {
381
- return [];
382
- }
383
- // Phase 2: exact re-ranking on full-precision embedding for candidates only
384
- const phase2Query = `
385
- DECLARE $qf AS List<Float>;
386
- DECLARE $k AS Uint32;
387
- DECLARE $uid AS Utf8;
388
- DECLARE $ids AS List<Utf8>;
389
- $qbinf = Knn::ToBinaryStringFloat($qf);
390
- SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
391
- FROM ${tableName}
392
- WHERE uid = $uid AND point_id IN $ids
393
- ORDER BY score ${order}
394
- LIMIT $k;
395
- `;
396
- logger.debug({
397
- tableName,
398
- distance,
399
- top,
400
- safeTop,
401
- candidateCount: candidateIds.length,
402
- mode: "one_table_approximate_phase2",
403
- yql: phase2Query,
404
- params: {
405
- uid,
406
- top: safeTop,
407
- vectorLength: queryVector.length,
408
- vectorPreview: queryVector.slice(0, 3),
409
- ids: candidateIds,
410
- },
411
- }, "one_table search (approximate, phase 2): executing YQL");
412
- const idsParam = TypedValues.list(Types.UTF8, candidateIds);
413
- const phase2Params = {
414
- $qf: qf,
415
- $k: TypedValues.uint32(safeTop),
416
- $uid: TypedValues.utf8(uid),
417
- $ids: idsParam,
418
- };
419
- const rs2 = await s.executeQuery(phase2Query, phase2Params);
420
- const rowset2 = rs2.resultSets?.[0];
421
- const rows2 = (rowset2?.rows ?? []);
422
- return rows2.map((row) => {
339
+ const settings = createExecuteQuerySettings();
340
+ const rs = await s.executeQuery(yql, params, undefined, settings);
341
+ const rowset = rs.resultSets?.[0];
342
+ const rows = (rowset?.rows ?? []);
343
+ return rows.map((row) => {
423
344
  const id = row.items?.[0]?.textValue;
424
345
  if (typeof id !== "string") {
425
346
  throw new Error("point_id is missing in YDB search result");
@@ -453,6 +374,7 @@ export async function searchPointsOneTable(tableName, queryVector, top, withPayl
453
374
  export async function deletePointsOneTable(tableName, ids, uid) {
454
375
  let deleted = 0;
455
376
  await withSession(async (s) => {
377
+ const settings = createExecuteQuerySettings();
456
378
  for (const id of ids) {
457
379
  const yql = `
458
380
  DECLARE $uid AS Utf8;
@@ -463,7 +385,7 @@ export async function deletePointsOneTable(tableName, ids, uid) {
463
385
  $uid: TypedValues.utf8(uid),
464
386
  $id: TypedValues.utf8(String(id)),
465
387
  };
466
- await s.executeQuery(yql, params);
388
+ await s.executeQuery(yql, params, undefined, settings);
467
389
  deleted += 1;
468
390
  }
469
391
  });
@@ -118,13 +118,23 @@ async function executeSearch(ctx, normalizedSearch, source) {
118
118
  throw err;
119
119
  }
120
120
  const threshold = normalizedSearch.scoreThreshold;
121
+ // For Cosine, repository hits use distance scores; convert to a
122
+ // similarity-like score so API consumers and IDE thresholds see
123
+ // "higher is better". This keeps ranking identical (monotonic 1 - d).
124
+ const normalizedHits = meta.distance === "Cosine"
125
+ ? hits.map((hit) => ({
126
+ ...hit,
127
+ score: 1 - hit.score,
128
+ }))
129
+ : hits;
121
130
  const filtered = threshold === undefined
122
- ? hits
123
- : hits.filter((hit) => {
124
- const isSimilarity = meta.distance === "Cosine" || meta.distance === "Dot";
125
- if (isSimilarity) {
131
+ ? normalizedHits
132
+ : normalizedHits.filter((hit) => {
133
+ if (meta.distance === "Dot" || meta.distance === "Cosine") {
134
+ // Similarity metrics: threshold is minimum similarity.
126
135
  return hit.score >= threshold;
127
136
  }
137
+ // Euclid / Manhattan: pure distance metrics; threshold is max distance.
128
138
  return hit.score <= threshold;
129
139
  });
130
140
  logger.info({
@@ -5,13 +5,13 @@ export declare function mapDistanceToKnnFn(distance: DistanceKind): {
5
5
  };
6
6
  export declare function mapDistanceToIndexParam(distance: DistanceKind): string;
7
7
  /**
8
- * Maps a user-specified distance metric to a YDB Knn distance function
8
+ * Maps a user-specified distance metric to a YDB Knn function
9
9
  * suitable for bit-quantized vectors (Phase 1 approximate candidate selection).
10
- * Always returns a distance function (lower is better, ASC ordering).
10
+ * Cosine uses similarity (DESC); other metrics use distance (ASC).
11
11
  * For Dot, falls back to CosineDistance as a proxy since there is no
12
12
  * direct distance equivalent for inner product.
13
13
  */
14
14
  export declare function mapDistanceToBitKnnFn(distance: DistanceKind): {
15
15
  fn: string;
16
- order: "ASC";
16
+ order: "ASC" | "DESC";
17
17
  };
@@ -1,7 +1,7 @@
1
1
  export function mapDistanceToKnnFn(distance) {
2
2
  switch (distance) {
3
3
  case "Cosine":
4
- return { fn: "Knn::CosineSimilarity", order: "DESC" };
4
+ return { fn: "Knn::CosineDistance", order: "ASC" };
5
5
  case "Dot":
6
6
  return { fn: "Knn::InnerProductSimilarity", order: "DESC" };
7
7
  case "Euclid":
@@ -9,7 +9,7 @@ export function mapDistanceToKnnFn(distance) {
9
9
  case "Manhattan":
10
10
  return { fn: "Knn::ManhattanDistance", order: "ASC" };
11
11
  default:
12
- return { fn: "Knn::CosineSimilarity", order: "DESC" };
12
+ return { fn: "Knn::CosineDistance", order: "ASC" };
13
13
  }
14
14
  }
15
15
  export function mapDistanceToIndexParam(distance) {
@@ -27,18 +27,17 @@ export function mapDistanceToIndexParam(distance) {
27
27
  }
28
28
  }
29
29
  /**
30
- * Maps a user-specified distance metric to a YDB Knn distance function
30
+ * Maps a user-specified distance metric to a YDB Knn function
31
31
  * suitable for bit-quantized vectors (Phase 1 approximate candidate selection).
32
- * Always returns a distance function (lower is better, ASC ordering).
32
+ * Cosine uses similarity (DESC); other metrics use distance (ASC).
33
33
  * For Dot, falls back to CosineDistance as a proxy since there is no
34
34
  * direct distance equivalent for inner product.
35
35
  */
36
36
  export function mapDistanceToBitKnnFn(distance) {
37
37
  switch (distance) {
38
38
  case "Cosine":
39
- return { fn: "Knn::CosineDistance", order: "ASC" };
39
+ return { fn: "Knn::CosineSimilarity", order: "DESC" };
40
40
  case "Dot":
41
- // No direct distance equivalent; use Cosine as proxy
42
41
  return { fn: "Knn::CosineDistance", order: "ASC" };
43
42
  case "Euclid":
44
43
  return { fn: "Knn::EuclideanDistance", order: "ASC" };
@@ -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.7.2",
3
+ "version": "4.8.1",
4
4
  "main": "dist/package/api.js",
5
5
  "types": "dist/package/api.d.ts",
6
6
  "exports": {