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.
- package/dist/repositories/collectionsRepo.js +3 -2
- package/dist/repositories/collectionsRepo.multi-table.js +3 -2
- package/dist/repositories/collectionsRepo.one-table.js +83 -6
- package/dist/repositories/pointsRepo.multi-table.js +9 -6
- package/dist/repositories/pointsRepo.one-table.js +82 -160
- package/dist/services/PointsService.js +14 -4
- package/dist/utils/distance.d.ts +3 -3
- package/dist/utils/distance.js +5 -6
- package/dist/utils/retry.js +13 -1
- package/dist/ydb/client.d.ts +7 -3
- package/dist/ydb/client.js +13 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
237
|
-
const phase1Query = `
|
|
240
|
+
yql = `
|
|
238
241
|
DECLARE $qbin_bit AS String;
|
|
239
|
-
DECLARE $
|
|
242
|
+
DECLARE $qbinf AS String;
|
|
243
|
+
DECLARE $candidateLimit AS Uint32;
|
|
244
|
+
DECLARE $safeTop AS Uint32;
|
|
240
245
|
DECLARE $uid AS Utf8;
|
|
241
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
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: "
|
|
254
|
-
yql
|
|
279
|
+
mode: "one_table_approximate_client_side_serialization",
|
|
280
|
+
yql,
|
|
255
281
|
params: {
|
|
256
282
|
uid,
|
|
257
|
-
|
|
283
|
+
safeTop,
|
|
284
|
+
candidateLimit,
|
|
258
285
|
vectorLength: queryVector.length,
|
|
259
286
|
vectorPreview: queryVector.slice(0, 3),
|
|
260
287
|
},
|
|
261
|
-
}, "one_table search (approximate
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
|
311
|
+
WHERE uid = $uid
|
|
312
|
+
AND point_id IN $candidates
|
|
287
313
|
ORDER BY score ${order}
|
|
288
|
-
LIMIT $
|
|
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
|
-
|
|
296
|
-
mode: "
|
|
297
|
-
yql
|
|
327
|
+
candidateLimit,
|
|
328
|
+
mode: "one_table_approximate",
|
|
329
|
+
yql,
|
|
298
330
|
params: {
|
|
299
331
|
uid,
|
|
300
|
-
|
|
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
|
|
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
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
?
|
|
123
|
-
:
|
|
124
|
-
|
|
125
|
-
|
|
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({
|
package/dist/utils/distance.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
};
|
package/dist/utils/distance.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function mapDistanceToKnnFn(distance) {
|
|
2
2
|
switch (distance) {
|
|
3
3
|
case "Cosine":
|
|
4
|
-
return { fn: "Knn::
|
|
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::
|
|
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
|
|
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
|
-
*
|
|
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::
|
|
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" };
|
package/dist/utils/retry.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/ydb/client.d.ts
CHANGED
|
@@ -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;
|
package/dist/ydb/client.js
CHANGED
|
@@ -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;
|