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
- // Phase 1: approximate candidate selection using embedding_quantized
237
- const phase1Query = `
238
+ yql = `
238
239
  DECLARE $qbin_bit AS String;
239
- DECLARE $k AS Uint32;
240
+ DECLARE $qbinf AS String;
241
+ DECLARE $candidateLimit AS Uint32;
242
+ DECLARE $safeTop AS Uint32;
240
243
  DECLARE $uid AS Utf8;
241
- SELECT point_id
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 AND embedding_quantized IS NOT NULL
244
- ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
245
- LIMIT $k;
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: "one_table_approximate_phase1_client_side_serialization",
254
- yql: phase1Query,
277
+ mode: "one_table_approximate_client_side_serialization",
278
+ yql,
255
279
  params: {
256
280
  uid,
257
- top: candidateLimit,
281
+ safeTop,
282
+ candidateLimit,
258
283
  vectorLength: queryVector.length,
259
284
  vectorPreview: queryVector.slice(0, 3),
260
285
  },
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;
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
- DECLARE $ids AS List<Utf8>;
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 AND point_id IN $ids
309
+ WHERE uid = $uid
310
+ AND point_id IN $candidates
287
311
  ORDER BY score ${order}
288
- LIMIT $k;
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
- candidateCount: candidateIds.length,
296
- mode: "one_table_approximate_phase2_client_side_serialization",
297
- yql: phase2Query,
325
+ candidateLimit,
326
+ mode: "one_table_approximate",
327
+ yql,
298
328
  params: {
299
329
  uid,
300
- top: safeTop,
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, 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
- });
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
- // 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) => {
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
- ? 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" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-qdrant",
3
- "version": "4.7.2",
3
+ "version": "4.8.0",
4
4
  "main": "dist/package/api.js",
5
5
  "types": "dist/package/api.d.ts",
6
6
  "exports": {