ydb-qdrant 8.1.0 → 9.0.3

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.
Files changed (87) hide show
  1. package/README.md +20 -18
  2. package/dist/SmokeTest.js +2 -2
  3. package/dist/compute/ComputePool.d.ts +5 -0
  4. package/dist/compute/ComputePool.js +64 -0
  5. package/dist/compute/ComputeWorker.d.ts +36 -0
  6. package/dist/compute/ComputeWorker.js +84 -0
  7. package/dist/config/env.d.ts +24 -7
  8. package/dist/config/env.js +65 -35
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +92 -2
  11. package/dist/logging/DeployLogFormatter.d.ts +2 -0
  12. package/dist/logging/DeployLogFormatter.js +131 -0
  13. package/dist/logging/logger.js +13 -1
  14. package/dist/logging/requestContext.d.ts +17 -0
  15. package/dist/logging/requestContext.js +43 -0
  16. package/dist/middleware/requestLogger.js +134 -6
  17. package/dist/middleware/upsertBodyPhase.d.ts +6 -0
  18. package/dist/middleware/upsertBodyPhase.js +184 -0
  19. package/dist/middleware/upsertRequestTimeout.d.ts +16 -0
  20. package/dist/middleware/upsertRequestTimeout.js +158 -0
  21. package/dist/package/api.d.ts +20 -12
  22. package/dist/package/api.js +57 -28
  23. package/dist/qdrant/QdrantRestTypes.d.ts +4 -0
  24. package/dist/qdrant/Requests.d.ts +97 -0
  25. package/dist/qdrant/Requests.js +72 -0
  26. package/dist/repositories/collectionsRepo.d.ts +18 -2
  27. package/dist/repositories/collectionsRepo.js +103 -7
  28. package/dist/repositories/collectionsRepo.one-table.d.ts +4 -3
  29. package/dist/repositories/collectionsRepo.one-table.js +99 -36
  30. package/dist/repositories/collectionsRepo.shared.d.ts +2 -2
  31. package/dist/repositories/collectionsRepo.shared.js +9 -4
  32. package/dist/repositories/pointsRepo.d.ts +6 -4
  33. package/dist/repositories/pointsRepo.js +8 -7
  34. package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -2
  35. package/dist/repositories/pointsRepo.one-table/Delete.js +157 -60
  36. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +7 -5
  37. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +44 -13
  38. package/dist/repositories/pointsRepo.one-table/Retrieve.d.ts +6 -0
  39. package/dist/repositories/pointsRepo.one-table/Retrieve.js +69 -0
  40. package/dist/repositories/pointsRepo.one-table/Search.d.ts +2 -3
  41. package/dist/repositories/pointsRepo.one-table/Search.js +102 -124
  42. package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
  43. package/dist/repositories/pointsRepo.one-table/Upsert.js +244 -48
  44. package/dist/repositories/pointsRepo.one-table.d.ts +1 -0
  45. package/dist/repositories/pointsRepo.one-table.js +1 -0
  46. package/dist/routes/collections.js +45 -36
  47. package/dist/routes/points.js +145 -56
  48. package/dist/server.js +42 -6
  49. package/dist/services/CollectionService.d.ts +7 -5
  50. package/dist/services/CollectionService.js +12 -9
  51. package/dist/services/CollectionService.one-table.js +1 -2
  52. package/dist/services/CollectionService.shared.d.ts +6 -5
  53. package/dist/services/CollectionService.shared.js +28 -12
  54. package/dist/services/PointsService.d.ts +8 -0
  55. package/dist/services/PointsService.js +132 -15
  56. package/dist/types.d.ts +4 -94
  57. package/dist/types.js +1 -54
  58. package/dist/utils/EnvParsers.d.ts +5 -0
  59. package/dist/utils/EnvParsers.js +30 -0
  60. package/dist/utils/PayloadSign.d.ts +4 -0
  61. package/dist/utils/PayloadSign.js +18 -0
  62. package/dist/utils/distance.d.ts +1 -12
  63. package/dist/utils/distance.js +0 -21
  64. package/dist/utils/pathPrefix.d.ts +3 -0
  65. package/dist/utils/pathPrefix.js +47 -0
  66. package/dist/utils/prefixExpansion.d.ts +1 -0
  67. package/dist/utils/prefixExpansion.js +11 -0
  68. package/dist/utils/qdrantResponse.d.ts +13 -0
  69. package/dist/utils/qdrantResponse.js +12 -0
  70. package/dist/utils/requestIdentity.d.ts +8 -0
  71. package/dist/utils/requestIdentity.js +52 -0
  72. package/dist/utils/retry.d.ts +2 -0
  73. package/dist/utils/retry.js +55 -11
  74. package/dist/utils/tenant.d.ts +12 -6
  75. package/dist/utils/tenant.js +41 -32
  76. package/dist/utils/vectorBinary.d.ts +0 -1
  77. package/dist/utils/vectorBinary.js +0 -98
  78. package/dist/utils/ydbErrors.d.ts +1 -0
  79. package/dist/utils/ydbErrors.js +14 -0
  80. package/dist/ydb/bootstrapMetaTable.js +14 -2
  81. package/dist/ydb/client.d.ts +10 -2
  82. package/dist/ydb/client.js +83 -24
  83. package/dist/ydb/helpers.d.ts +0 -1
  84. package/dist/ydb/helpers.js +1 -2
  85. package/dist/ydb/schema.d.ts +2 -0
  86. package/dist/ydb/schema.js +84 -7
  87. package/package.json +10 -5
@@ -0,0 +1,69 @@
1
+ import { Types, TypedValues, withSession, createExecuteQuerySettings, } from "../../ydb/client.js";
2
+ import { logger } from "../../logging/logger.js";
3
+ import { computePayloadSign } from "../../utils/PayloadSign.js";
4
+ import { isTransientYdbError, withRetry } from "../../utils/retry.js";
5
+ const RETRIEVE_RETRY_MAX_RETRIES = 6;
6
+ const RETRIEVE_RETRY_BASE_DELAY_MS = 250;
7
+ const RETRIEVE_RETRY_MAX_BACKOFF_MS = 1500;
8
+ export async function retrievePointsByIdsOneTable(tableName, ids, uid, apiKey, withPayload) {
9
+ const stringIds = ids.map(String);
10
+ const qry = `
11
+ DECLARE $collection AS Utf8;
12
+ DECLARE $ids AS List<Utf8>;
13
+ SELECT point_id, payload, payload_sign
14
+ FROM ${tableName}
15
+ WHERE collection = $collection AND point_id IN $ids;
16
+ `;
17
+ const res = await withRetry(async () => await withSession(async (s) => {
18
+ const settings = createExecuteQuerySettings();
19
+ return await s.executeQuery(qry, {
20
+ $collection: TypedValues.utf8(uid),
21
+ $ids: TypedValues.list(Types.UTF8, stringIds),
22
+ }, undefined, settings);
23
+ }), {
24
+ isTransient: isTransientYdbError,
25
+ maxRetries: RETRIEVE_RETRY_MAX_RETRIES,
26
+ baseDelayMs: RETRIEVE_RETRY_BASE_DELAY_MS,
27
+ maxBackoffMs: RETRIEVE_RETRY_MAX_BACKOFF_MS,
28
+ context: {
29
+ operation: "retrievePointsByIdsOneTable",
30
+ tableName,
31
+ collection: uid,
32
+ idCount: ids.length,
33
+ },
34
+ });
35
+ const rowset = res.resultSets?.[0];
36
+ const rows = (rowset?.rows ?? []);
37
+ const trimmedApiKey = apiKey.trim();
38
+ const out = [];
39
+ for (const row of rows) {
40
+ const id = row.items?.[0]?.textValue;
41
+ if (typeof id !== "string")
42
+ continue;
43
+ const payloadText = row.items?.[1]?.textValue;
44
+ const payloadSign = row.items?.[2]?.textValue;
45
+ let payload = null;
46
+ if (payloadText) {
47
+ try {
48
+ payload = JSON.parse(payloadText);
49
+ }
50
+ catch {
51
+ payload = null;
52
+ }
53
+ }
54
+ if (!payload || typeof payloadSign !== "string" || payloadSign === "") {
55
+ logger.warn({ collection: uid, pointId: id }, "retrieve: payload signature mismatch: missing payload or signature");
56
+ continue;
57
+ }
58
+ const expected = computePayloadSign({ apiKey: trimmedApiKey, payload });
59
+ if (expected !== payloadSign) {
60
+ logger.warn({ collection: uid, pointId: id }, "retrieve: payload signature mismatch: dropping point from results");
61
+ continue;
62
+ }
63
+ out.push({
64
+ id,
65
+ payload: withPayload ? payload : null,
66
+ });
67
+ }
68
+ return out;
69
+ }
@@ -1,4 +1,3 @@
1
- import type { DistanceKind } from "../../types.js";
2
- import { SearchMode } from "../../config/env.js";
1
+ import type { DistanceKind } from "../../qdrant/QdrantRestTypes.js";
3
2
  import type { YdbQdrantScoredPoint } from "../../qdrant/QdrantRestTypes.js";
4
- export declare function searchPointsOneTable(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number, uid: string, mode: SearchMode | undefined, overfetchMultiplier: number, filterPaths?: Array<Array<string>>): Promise<YdbQdrantScoredPoint[]>;
3
+ export declare function searchPointsOneTable(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number, collection: string, apiKey: string, filterPaths?: Array<Array<string>>): Promise<YdbQdrantScoredPoint[]>;
@@ -1,9 +1,11 @@
1
1
  import { Types, TypedValues, withSession, createExecuteQuerySettingsWithTimeout, } from "../../ydb/client.js";
2
- import { buildVectorBinaryParams } from "../../ydb/helpers.js";
3
- import { mapDistanceToKnnFn, mapDistanceToBitKnnFn, } from "../../utils/distance.js";
2
+ import { mapDistanceToKnnFn } from "../../utils/distance.js";
3
+ import { vectorToFloatBinary } from "../../utils/vectorBinary.js";
4
4
  import { logger } from "../../logging/logger.js";
5
- import { SearchMode, SEARCH_OPERATION_TIMEOUT_MS } from "../../config/env.js";
6
- import { buildPathSegmentsFilter } from "./PathSegmentsFilter.js";
5
+ import { SEARCH_OPERATION_TIMEOUT_MS } from "../../config/env.js";
6
+ import { buildPrefixPathSegmentsFilter } from "./PathSegmentsFilter.js";
7
+ import { computePayloadSign } from "../../utils/PayloadSign.js";
8
+ import { isComputePoolEnabled, isComputePoolQueueAtLimitError, runVerifySearchRows, } from "../../compute/ComputePool.js";
7
9
  function assertVectorDimension(vector, dimension, messagePrefix = "Vector dimension mismatch") {
8
10
  if (vector.length !== dimension) {
9
11
  throw new Error(`${messagePrefix}: got ${vector.length}, expected ${dimension}`);
@@ -19,114 +21,132 @@ function typedBytesOrFallback(value) {
19
21
  }
20
22
  throw new Error("ydb-sdk does not support constructing BYTES typed parameters (TypedValues.bytes/fromNative missing); cannot execute vector search");
21
23
  }
22
- function parseSearchRows(rows, withPayload) {
23
- return rows.map((row) => {
24
+ function parseSearchRows(rows, args) {
25
+ const apiKey = args.apiKey.trim();
26
+ const out = [];
27
+ for (const row of rows) {
24
28
  const id = row.items?.[0]?.textValue;
25
29
  if (typeof id !== "string") {
26
30
  throw new Error("point_id is missing in YDB search result");
27
31
  }
32
+ const payloadText = row.items?.[1]?.textValue;
33
+ const payloadSign = row.items?.[2]?.textValue;
28
34
  let payload;
29
- let scoreIdx = 1;
30
- if (withPayload) {
31
- const payloadText = row.items?.[1]?.textValue;
32
- if (payloadText) {
33
- try {
34
- payload = JSON.parse(payloadText);
35
- }
36
- catch {
37
- payload = undefined;
38
- }
35
+ if (payloadText) {
36
+ try {
37
+ payload = JSON.parse(payloadText);
39
38
  }
40
- scoreIdx = 2;
39
+ catch {
40
+ payload = undefined;
41
+ }
42
+ }
43
+ if (!payload || typeof payloadSign !== "string" || payloadSign === "") {
44
+ logger.warn({ collection: args.collection, pointId: id }, "payload signature mismatch: missing payload or signature");
45
+ continue;
46
+ }
47
+ const expected = computePayloadSign({ apiKey, payload });
48
+ if (expected !== payloadSign) {
49
+ logger.warn({ collection: args.collection, pointId: id }, "payload signature mismatch: dropping point from search results");
50
+ continue;
51
+ }
52
+ const scoreIdx = 3;
53
+ const score = Number(row.items?.[scoreIdx]?.floatValue ??
54
+ row.items?.[scoreIdx]?.textValue);
55
+ const shouldReturnPayload = args.withPayload === true;
56
+ out.push({
57
+ id,
58
+ score,
59
+ ...(shouldReturnPayload && payload ? { payload } : {}),
60
+ });
61
+ }
62
+ return out;
63
+ }
64
+ async function parseSearchRowsWithWorkers(rows, args) {
65
+ const taskRows = rows.map((row) => {
66
+ const id = row.items?.[0]?.textValue;
67
+ if (typeof id !== "string") {
68
+ throw new Error("point_id is missing in YDB search result");
41
69
  }
42
- const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
43
- return { id, score, ...(payload ? { payload } : {}) };
70
+ const payloadText = row.items?.[1]?.textValue;
71
+ const payloadSign = row.items?.[2]?.textValue;
72
+ const scoreIdx = 3;
73
+ const scoreFloat = row.items?.[scoreIdx]?.floatValue;
74
+ const scoreText = row.items?.[scoreIdx]?.textValue;
75
+ return {
76
+ pointId: id,
77
+ payloadText,
78
+ payloadSign,
79
+ scoreFloat,
80
+ scoreText,
81
+ };
44
82
  });
83
+ const result = await runVerifySearchRows({
84
+ collection: args.collection,
85
+ apiKey: args.apiKey,
86
+ withPayload: args.withPayload,
87
+ rows: taskRows,
88
+ });
89
+ for (const d of result.dropped) {
90
+ if (d.reason === "missing_payload_or_signature") {
91
+ logger.warn({ collection: args.collection, pointId: d.pointId }, "payload signature mismatch: missing payload or signature");
92
+ continue;
93
+ }
94
+ logger.warn({ collection: args.collection, pointId: d.pointId }, "payload signature mismatch: dropping point from search results");
95
+ }
96
+ return result.points;
97
+ }
98
+ async function parseSearchRowsMaybeWithWorkers(rows, args) {
99
+ if (!isComputePoolEnabled()) {
100
+ return parseSearchRows(rows, args);
101
+ }
102
+ try {
103
+ return await parseSearchRowsWithWorkers(rows, args);
104
+ }
105
+ catch (err) {
106
+ if (!isComputePoolQueueAtLimitError(err)) {
107
+ throw err;
108
+ }
109
+ // Backpressure: fall back to inline compute to avoid failing the request.
110
+ return parseSearchRows(rows, args);
111
+ }
45
112
  }
46
113
  function buildExactSearchQueryAndParams(args) {
47
114
  const { fn, order } = mapDistanceToKnnFn(args.distance);
48
- const filter = buildPathSegmentsFilter(args.filterPaths);
115
+ const filter = buildPrefixPathSegmentsFilter(args.filterPaths, "path_prefix");
49
116
  const filterWhere = filter ? ` AND ${filter.whereSql}` : "";
50
- const binaries = buildVectorBinaryParams(args.queryVector);
117
+ const qbinf = vectorToFloatBinary(args.queryVector);
51
118
  const yql = `
52
119
  DECLARE $qbinf AS String;
53
120
  DECLARE $k AS Uint32;
54
- DECLARE $uid AS Utf8;
121
+ DECLARE $collection AS Utf8;
55
122
  ${filter?.whereParamDeclarations ?? ""}
56
- SELECT point_id, ${args.withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
123
+ SELECT point_id, payload, payload_sign, ${fn}(embedding, $qbinf) AS score
57
124
  FROM ${args.tableName}
58
- WHERE uid = $uid${filterWhere}
125
+ WHERE collection = $collection${filterWhere}
59
126
  ORDER BY score ${order}
60
127
  LIMIT $k;
61
128
  `;
62
129
  const params = {
63
130
  ...(filter?.whereParams ?? {}),
64
- $qbinf: typedBytesOrFallback(binaries.float),
131
+ $qbinf: typedBytesOrFallback(qbinf),
65
132
  $k: TypedValues.uint32(args.top),
66
- $uid: TypedValues.utf8(args.uid),
67
- };
68
- return { yql, params, modeLog: "one_table_exact_client_side_serialization" };
69
- }
70
- function buildApproxSearchQueryAndParams(args) {
71
- const { fn, order } = mapDistanceToKnnFn(args.distance);
72
- const { fn: bitFn, order: bitOrder } = mapDistanceToBitKnnFn(args.distance);
73
- const safeTop = args.top > 0 ? args.top : 1;
74
- const rawCandidateLimit = safeTop * args.overfetchMultiplier;
75
- const candidateLimit = Math.max(safeTop, rawCandidateLimit);
76
- const filter = buildPathSegmentsFilter(args.filterPaths);
77
- const filterWhere = filter ? ` AND ${filter.whereSql}` : "";
78
- const binaries = buildVectorBinaryParams(args.queryVector);
79
- const yql = `
80
- DECLARE $qbin_bit AS String;
81
- DECLARE $qbinf AS String;
82
- DECLARE $candidateLimit AS Uint32;
83
- DECLARE $safeTop AS Uint32;
84
- DECLARE $uid AS Utf8;
85
- ${filter?.whereParamDeclarations ?? ""}
86
-
87
- $candidates = (
88
- SELECT point_id
89
- FROM ${args.tableName}
90
- WHERE uid = $uid AND embedding_quantized IS NOT NULL
91
- ${filterWhere}
92
- ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
93
- LIMIT $candidateLimit
94
- );
95
-
96
- SELECT point_id, ${args.withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
97
- FROM ${args.tableName}
98
- WHERE uid = $uid
99
- AND point_id IN $candidates
100
- ${filterWhere}
101
- ORDER BY score ${order}
102
- LIMIT $safeTop;
103
- `;
104
- const params = {
105
- ...(filter?.whereParams ?? {}),
106
- $qbin_bit: typedBytesOrFallback(binaries.bit),
107
- $qbinf: typedBytesOrFallback(binaries.float),
108
- $candidateLimit: TypedValues.uint32(candidateLimit),
109
- $safeTop: TypedValues.uint32(safeTop),
110
- $uid: TypedValues.utf8(args.uid),
133
+ $collection: TypedValues.utf8(args.collection),
111
134
  };
112
135
  return {
113
136
  yql,
114
137
  params,
115
- safeTop,
116
- candidateLimit,
117
- modeLog: "one_table_approximate_client_side_serialization",
138
+ modeLog: "one_table_exact_client_side_serialization",
118
139
  };
119
140
  }
120
- async function searchPointsOneTableExact(tableName, queryVector, top, withPayload, distance, dimension, uid, filterPaths) {
141
+ async function searchPointsOneTableExact(tableName, queryVector, top, withPayload, apiKey, distance, dimension, collection, filterPaths) {
121
142
  assertVectorDimension(queryVector, dimension);
122
143
  const results = await withSession(async (s) => {
123
144
  const { yql, params, modeLog } = buildExactSearchQueryAndParams({
124
145
  tableName,
125
146
  queryVector,
126
147
  top,
127
- withPayload,
128
148
  distance,
129
- uid,
149
+ collection,
130
150
  filterPaths,
131
151
  });
132
152
  if (logger.isLevelEnabled("debug")) {
@@ -138,7 +158,7 @@ async function searchPointsOneTableExact(tableName, queryVector, top, withPayloa
138
158
  mode: modeLog,
139
159
  yql,
140
160
  params: {
141
- uid,
161
+ collection,
142
162
  top,
143
163
  vectorLength: queryVector.length,
144
164
  vectorPreview: queryVector.slice(0, 3),
@@ -153,56 +173,14 @@ async function searchPointsOneTableExact(tableName, queryVector, top, withPayloa
153
173
  const rs = await s.executeQuery(yql, params, undefined, settings);
154
174
  const rowset = rs.resultSets?.[0];
155
175
  const rows = (rowset?.rows ?? []);
156
- return parseSearchRows(rows, withPayload);
157
- });
158
- return results;
159
- }
160
- async function searchPointsOneTableApproximate(tableName, queryVector, top, withPayload, distance, dimension, uid, overfetchMultiplier, filterPaths) {
161
- assertVectorDimension(queryVector, dimension);
162
- const results = await withSession(async (s) => {
163
- const { yql, params, safeTop, candidateLimit, modeLog } = buildApproxSearchQueryAndParams({
164
- tableName,
165
- queryVector,
166
- top,
176
+ return await parseSearchRowsMaybeWithWorkers(rows, {
177
+ collection,
167
178
  withPayload,
168
- distance,
169
- uid,
170
- overfetchMultiplier,
171
- filterPaths,
179
+ apiKey,
172
180
  });
173
- if (logger.isLevelEnabled("debug")) {
174
- logger.debug({
175
- tableName,
176
- distance,
177
- top,
178
- safeTop,
179
- candidateLimit,
180
- mode: modeLog,
181
- yql,
182
- params: {
183
- uid,
184
- safeTop,
185
- candidateLimit,
186
- vectorLength: queryVector.length,
187
- vectorPreview: queryVector.slice(0, 3),
188
- },
189
- }, "one_table search (approximate): executing YQL");
190
- }
191
- const settings = createExecuteQuerySettingsWithTimeout({
192
- keepInCache: true,
193
- idempotent: true,
194
- timeoutMs: SEARCH_OPERATION_TIMEOUT_MS,
195
- });
196
- const rs = await s.executeQuery(yql, params, undefined, settings);
197
- const rowset = rs.resultSets?.[0];
198
- const rows = (rowset?.rows ?? []);
199
- return parseSearchRows(rows, withPayload);
200
181
  });
201
182
  return results;
202
183
  }
203
- export async function searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid, mode, overfetchMultiplier, filterPaths) {
204
- if (mode === SearchMode.Exact) {
205
- return await searchPointsOneTableExact(tableName, queryVector, top, withPayload, distance, dimension, uid, filterPaths);
206
- }
207
- return await searchPointsOneTableApproximate(tableName, queryVector, top, withPayload, distance, dimension, uid, overfetchMultiplier, filterPaths);
184
+ export async function searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, collection, apiKey, filterPaths) {
185
+ return await searchPointsOneTableExact(tableName, queryVector, top, withPayload, apiKey, distance, dimension, collection, filterPaths);
208
186
  }
@@ -1,2 +1,2 @@
1
- import type { UpsertPoint } from "../../types.js";
2
- export declare function upsertPointsOneTable(tableName: string, points: UpsertPoint[], dimension: number, uid: string): Promise<number>;
1
+ import type { UpsertPoint } from "../../qdrant/Requests.js";
2
+ export declare function upsertPointsOneTable(tableName: string, points: UpsertPoint[], dimension: number, collection: string, apiKey: string): Promise<number>;