ydb-qdrant 4.7.2 → 4.8.0
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.
|
@@ -231,195 +231,113 @@ async function searchPointsOneTableApproximate(tableName, queryVector, top, with
|
|
|
231
231
|
const rawCandidateLimit = safeTop * overfetchMultiplier;
|
|
232
232
|
const candidateLimit = Math.max(safeTop, rawCandidateLimit);
|
|
233
233
|
const results = await withSession(async (s) => {
|
|
234
|
+
let yql;
|
|
235
|
+
let params;
|
|
234
236
|
if (CLIENT_SIDE_SERIALIZATION_ENABLED) {
|
|
235
237
|
const binaries = buildVectorBinaryParams(queryVector);
|
|
236
|
-
|
|
237
|
-
const phase1Query = `
|
|
238
|
+
yql = `
|
|
238
239
|
DECLARE $qbin_bit AS String;
|
|
239
|
-
DECLARE $
|
|
240
|
+
DECLARE $qbinf AS String;
|
|
241
|
+
DECLARE $candidateLimit AS Uint32;
|
|
242
|
+
DECLARE $safeTop AS Uint32;
|
|
240
243
|
DECLARE $uid AS Utf8;
|
|
241
|
-
|
|
244
|
+
|
|
245
|
+
$candidates = (
|
|
246
|
+
SELECT point_id
|
|
247
|
+
FROM ${tableName}
|
|
248
|
+
WHERE uid = $uid AND embedding_quantized IS NOT NULL
|
|
249
|
+
ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
|
|
250
|
+
LIMIT $candidateLimit
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
|
|
242
254
|
FROM ${tableName}
|
|
243
|
-
WHERE uid = $uid
|
|
244
|
-
|
|
245
|
-
|
|
255
|
+
WHERE uid = $uid
|
|
256
|
+
AND point_id IN $candidates
|
|
257
|
+
ORDER BY score ${order}
|
|
258
|
+
LIMIT $safeTop;
|
|
246
259
|
`;
|
|
260
|
+
params = {
|
|
261
|
+
$qbin_bit: typeof TypedValues.bytes === "function"
|
|
262
|
+
? TypedValues.bytes(binaries.bit)
|
|
263
|
+
: binaries.bit,
|
|
264
|
+
$qbinf: typeof TypedValues.bytes === "function"
|
|
265
|
+
? TypedValues.bytes(binaries.float)
|
|
266
|
+
: binaries.float,
|
|
267
|
+
$candidateLimit: TypedValues.uint32(candidateLimit),
|
|
268
|
+
$safeTop: TypedValues.uint32(safeTop),
|
|
269
|
+
$uid: TypedValues.utf8(uid),
|
|
270
|
+
};
|
|
247
271
|
logger.debug({
|
|
248
272
|
tableName,
|
|
249
273
|
distance,
|
|
250
274
|
top,
|
|
251
275
|
safeTop,
|
|
252
276
|
candidateLimit,
|
|
253
|
-
mode: "
|
|
254
|
-
yql
|
|
277
|
+
mode: "one_table_approximate_client_side_serialization",
|
|
278
|
+
yql,
|
|
255
279
|
params: {
|
|
256
280
|
uid,
|
|
257
|
-
|
|
281
|
+
safeTop,
|
|
282
|
+
candidateLimit,
|
|
258
283
|
vectorLength: queryVector.length,
|
|
259
284
|
vectorPreview: queryVector.slice(0, 3),
|
|
260
285
|
},
|
|
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;
|
|
286
|
+
}, "one_table search (approximate): executing YQL with client-side serialization");
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const qf = buildVectorParam(queryVector);
|
|
290
|
+
yql = `
|
|
291
|
+
DECLARE $qf AS List<Float>;
|
|
292
|
+
DECLARE $candidateLimit AS Uint32;
|
|
293
|
+
DECLARE $safeTop AS Uint32;
|
|
282
294
|
DECLARE $uid AS Utf8;
|
|
283
|
-
|
|
295
|
+
|
|
296
|
+
$qbin_bit = Knn::ToBinaryStringBit($qf);
|
|
297
|
+
$qbinf = Knn::ToBinaryStringFloat($qf);
|
|
298
|
+
|
|
299
|
+
$candidates = (
|
|
300
|
+
SELECT point_id
|
|
301
|
+
FROM ${tableName}
|
|
302
|
+
WHERE uid = $uid AND embedding_quantized IS NOT NULL
|
|
303
|
+
ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
|
|
304
|
+
LIMIT $candidateLimit
|
|
305
|
+
);
|
|
306
|
+
|
|
284
307
|
SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
|
|
285
308
|
FROM ${tableName}
|
|
286
|
-
WHERE uid = $uid
|
|
309
|
+
WHERE uid = $uid
|
|
310
|
+
AND point_id IN $candidates
|
|
287
311
|
ORDER BY score ${order}
|
|
288
|
-
LIMIT $
|
|
312
|
+
LIMIT $safeTop;
|
|
289
313
|
`;
|
|
314
|
+
params = {
|
|
315
|
+
$qf: qf,
|
|
316
|
+
$candidateLimit: TypedValues.uint32(candidateLimit),
|
|
317
|
+
$safeTop: TypedValues.uint32(safeTop),
|
|
318
|
+
$uid: TypedValues.utf8(uid),
|
|
319
|
+
};
|
|
290
320
|
logger.debug({
|
|
291
321
|
tableName,
|
|
292
322
|
distance,
|
|
293
323
|
top,
|
|
294
324
|
safeTop,
|
|
295
|
-
|
|
296
|
-
mode: "
|
|
297
|
-
yql
|
|
325
|
+
candidateLimit,
|
|
326
|
+
mode: "one_table_approximate",
|
|
327
|
+
yql,
|
|
298
328
|
params: {
|
|
299
329
|
uid,
|
|
300
|
-
|
|
330
|
+
safeTop,
|
|
331
|
+
candidateLimit,
|
|
301
332
|
vectorLength: queryVector.length,
|
|
302
333
|
vectorPreview: queryVector.slice(0, 3),
|
|
303
|
-
ids: candidateIds,
|
|
304
334
|
},
|
|
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
|
-
});
|
|
340
|
-
}
|
|
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 [];
|
|
335
|
+
}, "one_table search (approximate): executing YQL");
|
|
382
336
|
}
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
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) => {
|
|
337
|
+
const rs = await s.executeQuery(yql, params);
|
|
338
|
+
const rowset = rs.resultSets?.[0];
|
|
339
|
+
const rows = (rowset?.rows ?? []);
|
|
340
|
+
return rows.map((row) => {
|
|
423
341
|
const id = row.items?.[0]?.textValue;
|
|
424
342
|
if (typeof id !== "string") {
|
|
425
343
|
throw new Error("point_id is missing in YDB search result");
|
|
@@ -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" };
|