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
@@ -1,9 +1,14 @@
1
- import { TypedValues, Types, withSession, Ydb as YdbRuntime, createBulkUpsertSettingsWithTimeout, } from "../../ydb/client.js";
1
+ import { TypedValues, Types, withSession, Ydb as YdbRuntime, createBulkUpsertSettingsWithTimeout, createExecuteQuerySettingsWithTimeout, } from "../../ydb/client.js";
2
2
  import { buildVectorBinaryParams } from "../../ydb/helpers.js";
3
- import { withRetry, isTransientYdbError } from "../../utils/retry.js";
4
- import { UPSERT_BATCH_SIZE } from "../../ydb/schema.js";
3
+ import { withRetry, isTransientYdbErrorInAcquiredSession, } from "../../utils/retry.js";
4
+ import { POINTS_BY_FILE_LOOKUP_TABLE, UPSERT_BATCH_SIZE, ensurePointsByFileTable, } from "../../ydb/schema.js";
5
5
  import { UPSERT_OPERATION_TIMEOUT_MS } from "../../config/env.js";
6
6
  import { logger } from "../../logging/logger.js";
7
+ import { elapsedMsSince, getMonotonicTimeNs, } from "../../logging/requestContext.js";
8
+ import { computePayloadSign } from "../../utils/PayloadSign.js";
9
+ import { extractPathPrefix } from "../../utils/pathPrefix.js";
10
+ import { expandPathPrefixes } from "../../utils/prefixExpansion.js";
11
+ import { isComputePoolEnabled, isComputePoolQueueAtLimitError, runPrepareUpsertBatch, } from "../../compute/ComputePool.js";
7
12
  function assertPointVectorsDimension(args) {
8
13
  for (const p of args.points) {
9
14
  const id = String(p.id);
@@ -12,7 +17,7 @@ function assertPointVectorsDimension(args) {
12
17
  const vectorPreview = previewLength > 0 ? p.vector.slice(0, previewLength) : [];
13
18
  logger.warn({
14
19
  tableName: args.tableName,
15
- uid: args.uid,
20
+ collection: args.collection,
16
21
  pointId: id,
17
22
  vectorLen: p.vector.length,
18
23
  expectedDimension: args.dimension,
@@ -22,64 +27,255 @@ function assertPointVectorsDimension(args) {
22
27
  }
23
28
  }
24
29
  }
25
- function buildBulkUpsertRowsValue(args) {
26
- const rowType = Types.struct({
27
- uid: Types.UTF8,
30
+ function createBulkUpsertRowType() {
31
+ return Types.struct({
32
+ collection: Types.UTF8,
28
33
  point_id: Types.UTF8,
29
34
  embedding: Types.BYTES,
30
- embedding_quantized: Types.BYTES,
31
35
  payload: Types.JSON_DOCUMENT,
36
+ payload_sign: Types.UTF8,
37
+ path_prefix: Types.optional(Types.UTF8),
38
+ });
39
+ }
40
+ function asBytesBuffer(value) {
41
+ // In worker_threads structured clone, Buffer arrives as Uint8Array.
42
+ // Wrap it into Buffer without copying again.
43
+ return Buffer.isBuffer(value)
44
+ ? value
45
+ : Buffer.from(value.buffer, value.byteOffset, value.byteLength);
46
+ }
47
+ function createPointsByFileLookupRowType() {
48
+ return Types.struct({
49
+ collection: Types.UTF8,
50
+ file_path: Types.UTF8,
51
+ point_id: Types.UTF8,
52
+ });
53
+ }
54
+ function buildPointsByFileLookupRows(args) {
55
+ return args.batch.flatMap((point) => {
56
+ const payloadObj = point.payload ?? {};
57
+ const filePath = extractPathPrefix(payloadObj);
58
+ if (!filePath) {
59
+ return [];
60
+ }
61
+ return expandPathPrefixes(filePath).map((prefix) => ({
62
+ collection: args.collection,
63
+ file_path: prefix,
64
+ point_id: String(point.id),
65
+ }));
32
66
  });
33
- return TypedValues.list(rowType, args.batch.map((p) => {
67
+ }
68
+ async function syncPointsByFileLookupRows(args) {
69
+ const rows = buildPointsByFileLookupRows(args);
70
+ if (rows.length === 0) {
71
+ return;
72
+ }
73
+ const rowsValue = TypedValues.list(createPointsByFileLookupRowType(), rows);
74
+ const upsertLookupYql = `
75
+ DECLARE $rows AS List<Struct<
76
+ collection: Utf8,
77
+ file_path: Utf8,
78
+ point_id: Utf8
79
+ >>;
80
+
81
+ UPSERT INTO ${POINTS_BY_FILE_LOOKUP_TABLE}
82
+ SELECT collection, file_path, point_id FROM AS_TABLE($rows);
83
+ `;
84
+ await withRetry(async () => {
85
+ const settings = createExecuteQuerySettingsWithTimeout({
86
+ timeoutMs: UPSERT_OPERATION_TIMEOUT_MS,
87
+ });
88
+ await args.session.executeQuery(upsertLookupYql, {
89
+ $rows: rowsValue,
90
+ }, undefined, settings);
91
+ }, {
92
+ isTransient: isTransientYdbErrorInAcquiredSession,
93
+ context: {
94
+ operation: "syncPointsByFileLookupRows",
95
+ collection: args.collection,
96
+ batchSize: args.batch.length,
97
+ lookupRowsCount: rows.length,
98
+ },
99
+ });
100
+ }
101
+ function buildBulkUpsertRowsValue(args) {
102
+ const apiKey = args.apiKey.trim();
103
+ return TypedValues.list(createBulkUpsertRowType(), args.batch.map((p) => {
34
104
  const binaries = buildVectorBinaryParams(p.vector);
35
- return {
36
- uid: args.uid,
105
+ const payloadObj = p.payload ?? {};
106
+ const row = {
107
+ collection: args.collection,
37
108
  point_id: String(p.id),
38
109
  embedding: binaries.float,
39
- embedding_quantized: binaries.bit,
40
- payload: JSON.stringify(p.payload ?? {}),
110
+ payload: JSON.stringify(payloadObj),
111
+ payload_sign: computePayloadSign({ apiKey, payload: payloadObj }),
112
+ path_prefix: extractPathPrefix(payloadObj),
41
113
  };
114
+ return row;
42
115
  }));
43
116
  }
44
- export async function upsertPointsOneTable(tableName, points, dimension, uid) {
117
+ async function buildBulkUpsertRowsValueWithWorkers(args) {
118
+ const prepared = await runPrepareUpsertBatch({
119
+ collection: args.collection,
120
+ apiKey: args.apiKey,
121
+ batch: args.batch,
122
+ });
123
+ return TypedValues.list(createBulkUpsertRowType(), prepared.map((row) => {
124
+ const mapped = {
125
+ collection: row.collection,
126
+ point_id: row.point_id,
127
+ embedding: asBytesBuffer(row.embedding),
128
+ payload: row.payload,
129
+ payload_sign: row.payload_sign,
130
+ path_prefix: row.path_prefix,
131
+ };
132
+ return mapped;
133
+ }));
134
+ }
135
+ export async function upsertPointsOneTable(tableName, points, dimension, collection, apiKey) {
45
136
  if (!tableName) {
46
137
  throw new Error("bulkUpsert: tableName is empty");
47
138
  }
48
- assertPointVectorsDimension({ tableName, uid, points, dimension });
49
- let upserted = 0;
50
- await withSession(async (s) => {
51
- const bulkSettings = createBulkUpsertSettingsWithTimeout({
52
- timeoutMs: UPSERT_OPERATION_TIMEOUT_MS,
53
- });
54
- for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
55
- const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
56
- const rowsValue = buildBulkUpsertRowsValue({
57
- uid,
58
- batch,
139
+ assertPointVectorsDimension({ tableName, collection, points, dimension });
140
+ await ensurePointsByFileTable();
141
+ const repoStartNs = getMonotonicTimeNs();
142
+ let batchCount = 0;
143
+ let currentBatchIndex = 0;
144
+ let prepareRowsMsTotal = 0;
145
+ let lookupSyncMsTotal = 0;
146
+ let bulkUpsertMsTotal = 0;
147
+ let usedWorkersAny = false;
148
+ try {
149
+ await withSession(async (s) => {
150
+ const bulkSettings = createBulkUpsertSettingsWithTimeout({
151
+ timeoutMs: UPSERT_OPERATION_TIMEOUT_MS,
59
152
  });
60
- if (logger.isLevelEnabled("debug")) {
61
- logger.debug({
62
- tableName,
63
- mode: "one_table_bulk_upsert_client_side_serialization",
64
- batchSize: batch.length,
65
- params: {
66
- rows: batch.map((p) => ({
67
- uid,
68
- point_id: String(p.id),
69
- vectorLength: p.vector.length,
70
- vectorPreview: p.vector.slice(0, 3),
71
- payload: p.payload ?? {},
72
- })),
153
+ for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
154
+ const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
155
+ currentBatchIndex = Math.floor(i / UPSERT_BATCH_SIZE) + 1;
156
+ const batchStartNs = getMonotonicTimeNs();
157
+ const workersEnabled = isComputePoolEnabled();
158
+ let usedWorkers = false;
159
+ let rowsValue;
160
+ const prepareRowsStartNs = getMonotonicTimeNs();
161
+ if (workersEnabled) {
162
+ try {
163
+ usedWorkers = true;
164
+ rowsValue = await buildBulkUpsertRowsValueWithWorkers({
165
+ collection,
166
+ batch,
167
+ apiKey,
168
+ });
169
+ }
170
+ catch (err) {
171
+ if (!isComputePoolQueueAtLimitError(err)) {
172
+ throw err;
173
+ }
174
+ // Backpressure: fall back to inline compute to avoid failing the request.
175
+ usedWorkers = false;
176
+ rowsValue = buildBulkUpsertRowsValue({
177
+ collection,
178
+ batch,
179
+ apiKey,
180
+ });
181
+ }
182
+ }
183
+ else {
184
+ rowsValue = buildBulkUpsertRowsValue({
185
+ collection,
186
+ batch,
187
+ apiKey,
188
+ });
189
+ }
190
+ const prepareRowsMs = elapsedMsSince(prepareRowsStartNs);
191
+ prepareRowsMsTotal += prepareRowsMs;
192
+ usedWorkersAny = usedWorkersAny || usedWorkers;
193
+ if (logger.isLevelEnabled("debug")) {
194
+ logger.debug({
195
+ tableName,
196
+ mode: usedWorkers
197
+ ? "one_table_bulk_upsert_worker_serialization"
198
+ : "one_table_bulk_upsert_client_side_serialization",
199
+ batchSize: batch.length,
200
+ params: {
201
+ rows: batch.map((p) => ({
202
+ collection,
203
+ point_id: String(p.id),
204
+ vectorLength: p.vector.length,
205
+ vectorPreview: p.vector.slice(0, 3),
206
+ payload: p.payload ?? {},
207
+ })),
208
+ },
209
+ }, "one_table upsert: executing BulkUpsert");
210
+ }
211
+ const lookupSyncStartNs = getMonotonicTimeNs();
212
+ await syncPointsByFileLookupRows({
213
+ session: s,
214
+ collection,
215
+ batch,
216
+ });
217
+ const lookupSyncMs = elapsedMsSince(lookupSyncStartNs);
218
+ lookupSyncMsTotal += lookupSyncMs;
219
+ const typedRows = YdbRuntime.TypedValue.create(rowsValue);
220
+ const bulkUpsertStartNs = getMonotonicTimeNs();
221
+ await withRetry(() => s.bulkUpsert(tableName, typedRows, bulkSettings), {
222
+ isTransient: isTransientYdbErrorInAcquiredSession,
223
+ context: {
224
+ tableName,
225
+ batchSize: batch.length,
226
+ mode: "bulkUpsert",
73
227
  },
74
- }, "one_table upsert: executing BulkUpsert");
228
+ });
229
+ const bulkUpsertMs = elapsedMsSince(bulkUpsertStartNs);
230
+ bulkUpsertMsTotal += bulkUpsertMs;
231
+ batchCount = currentBatchIndex;
232
+ if (logger.isLevelEnabled("debug")) {
233
+ logger.debug({
234
+ phase: "upsertRepoBatch",
235
+ tableName,
236
+ collection,
237
+ batchIndex: currentBatchIndex,
238
+ batchSize: batch.length,
239
+ prepareRowsMs,
240
+ lookupSyncMs,
241
+ bulkUpsertMs,
242
+ batchTotalMs: elapsedMsSince(batchStartNs),
243
+ usedWorkers,
244
+ }, "upsert: repo batch");
245
+ }
75
246
  }
76
- const typedRows = YdbRuntime.TypedValue.create(rowsValue);
77
- await withRetry(() => s.bulkUpsert(tableName, typedRows, bulkSettings), {
78
- isTransient: isTransientYdbError,
79
- context: { tableName, batchSize: batch.length, mode: "bulkUpsert" },
80
- });
81
- upserted += batch.length;
82
- }
83
- });
84
- return upserted;
247
+ });
248
+ }
249
+ catch (err) {
250
+ logger.error({
251
+ err: err instanceof Error ? err : new Error(String(err)),
252
+ phase: "upsertRepo",
253
+ tableName,
254
+ collection,
255
+ repoTotalMs: elapsedMsSince(repoStartNs),
256
+ batchCount,
257
+ failedBatchIndex: currentBatchIndex > batchCount ? currentBatchIndex : undefined,
258
+ pointCount: points.length,
259
+ prepareRowsMsTotal,
260
+ lookupSyncMsTotal,
261
+ bulkUpsertMsTotal,
262
+ usedWorkersAny,
263
+ }, "upsert: failed");
264
+ throw err;
265
+ }
266
+ logger.info({
267
+ phase: "upsertRepo",
268
+ tableName,
269
+ collection,
270
+ repoTotalMs: elapsedMsSince(repoStartNs),
271
+ batchCount,
272
+ pointCount: points.length,
273
+ prepareRowsMsTotal,
274
+ lookupSyncMsTotal,
275
+ bulkUpsertMsTotal,
276
+ usedWorkersAny,
277
+ }, "upsert: repo summary");
278
+ // BulkUpsert is idempotent for the same (collection, point_id) keys; we report the
279
+ // number of points accepted by the API rather than rows actually changed.
280
+ return points.length;
85
281
  }
@@ -1,3 +1,4 @@
1
1
  export { searchPointsOneTable } from "./pointsRepo.one-table/Search.js";
2
2
  export { upsertPointsOneTable } from "./pointsRepo.one-table/Upsert.js";
3
3
  export { deletePointsOneTable, deletePointsByPathSegmentsOneTable, } from "./pointsRepo.one-table/Delete.js";
4
+ export { retrievePointsByIdsOneTable } from "./pointsRepo.one-table/Retrieve.js";
@@ -1,3 +1,4 @@
1
1
  export { searchPointsOneTable } from "./pointsRepo.one-table/Search.js";
2
2
  export { upsertPointsOneTable } from "./pointsRepo.one-table/Upsert.js";
3
3
  export { deletePointsOneTable, deletePointsByPathSegmentsOneTable, } from "./pointsRepo.one-table/Delete.js";
4
+ export { retrievePointsByIdsOneTable } from "./pointsRepo.one-table/Retrieve.js";
@@ -2,39 +2,55 @@ import { Router } from "express";
2
2
  import { putCollectionIndex, createCollection, getCollection, deleteCollection, } from "../services/CollectionService.js";
3
3
  import { QdrantServiceError } from "../services/errors.js";
4
4
  import { logger } from "../logging/logger.js";
5
+ import { qdrantResponse } from "../utils/qdrantResponse.js";
6
+ import { isAnonymousIdentityError, resolveRequestNamespaceUserUid, resolveRequestSigningKey, } from "../utils/requestIdentity.js";
5
7
  export const collectionsRouter = Router();
8
+ function buildCollectionContext(req) {
9
+ const userUid = resolveRequestNamespaceUserUid(req);
10
+ return {
11
+ userUid,
12
+ collection: String(req.params.collection),
13
+ apiKey: resolveRequestSigningKey(req, userUid),
14
+ userAgent: req.header("User-Agent") ?? undefined,
15
+ };
16
+ }
17
+ function sendKnownRouteError(res, err) {
18
+ if (err instanceof QdrantServiceError) {
19
+ res.status(err.statusCode).json(err.payload);
20
+ return true;
21
+ }
22
+ if (isAnonymousIdentityError(err)) {
23
+ res.status(400).json({ status: "error", error: err.message });
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+ // Fix #8: PUT index → UpdateResult shape
6
29
  collectionsRouter.put("/:collection/index", async (req, res) => {
30
+ const start = process.hrtime();
7
31
  try {
8
- const result = await putCollectionIndex({
9
- tenant: req.header("X-Tenant-Id") ?? undefined,
10
- collection: String(req.params.collection),
11
- apiKey: req.header("api-key") ?? undefined,
12
- userAgent: req.header("User-Agent") ?? undefined,
13
- });
14
- res.json({ status: "ok", result });
32
+ await putCollectionIndex(buildCollectionContext(req));
33
+ res.json(qdrantResponse({ operation_id: 0, status: "completed" }, start));
15
34
  }
16
35
  catch (err) {
17
- if (err instanceof QdrantServiceError) {
18
- return res.status(err.statusCode).json(err.payload);
36
+ if (sendKnownRouteError(res, err)) {
37
+ return;
19
38
  }
20
39
  logger.error({ err }, "build index failed");
21
40
  const errorMessage = err instanceof Error ? err.message : String(err);
22
41
  res.status(500).json({ status: "error", error: errorMessage });
23
42
  }
24
43
  });
44
+ // Fix #4: createCollection → result: true
25
45
  collectionsRouter.put("/:collection", async (req, res) => {
46
+ const start = process.hrtime();
26
47
  try {
27
- const result = await createCollection({
28
- tenant: req.header("X-Tenant-Id") ?? undefined,
29
- collection: String(req.params.collection),
30
- apiKey: req.header("api-key") ?? undefined,
31
- userAgent: req.header("User-Agent") ?? undefined,
32
- }, req.body);
33
- res.json({ status: "ok", result });
48
+ await createCollection(buildCollectionContext(req), req.body);
49
+ res.json(qdrantResponse(true, start));
34
50
  }
35
51
  catch (err) {
36
- if (err instanceof QdrantServiceError) {
37
- return res.status(err.statusCode).json(err.payload);
52
+ if (sendKnownRouteError(res, err)) {
53
+ return;
38
54
  }
39
55
  logger.error({ err }, "create collection failed");
40
56
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -42,37 +58,30 @@ collectionsRouter.put("/:collection", async (req, res) => {
42
58
  }
43
59
  });
44
60
  collectionsRouter.get("/:collection", async (req, res) => {
61
+ const start = process.hrtime();
45
62
  try {
46
- const result = await getCollection({
47
- tenant: req.header("X-Tenant-Id") ?? undefined,
48
- collection: String(req.params.collection),
49
- apiKey: req.header("api-key") ?? undefined,
50
- userAgent: req.header("User-Agent") ?? undefined,
51
- });
52
- res.json({ status: "ok", result });
63
+ const result = await getCollection(buildCollectionContext(req));
64
+ res.json(qdrantResponse(result, start));
53
65
  }
54
66
  catch (err) {
55
- if (err instanceof QdrantServiceError) {
56
- return res.status(err.statusCode).json(err.payload);
67
+ if (sendKnownRouteError(res, err)) {
68
+ return;
57
69
  }
58
70
  logger.error({ err }, "get collection failed");
59
71
  const errorMessage = err instanceof Error ? err.message : String(err);
60
72
  res.status(500).json({ status: "error", error: errorMessage });
61
73
  }
62
74
  });
75
+ // Fix #5: deleteCollection → result: true
63
76
  collectionsRouter.delete("/:collection", async (req, res) => {
77
+ const start = process.hrtime();
64
78
  try {
65
- const result = await deleteCollection({
66
- tenant: req.header("X-Tenant-Id") ?? undefined,
67
- collection: String(req.params.collection),
68
- apiKey: req.header("api-key") ?? undefined,
69
- userAgent: req.header("User-Agent") ?? undefined,
70
- });
71
- res.json({ status: "ok", result });
79
+ await deleteCollection(buildCollectionContext(req));
80
+ res.json(qdrantResponse(true, start));
72
81
  }
73
82
  catch (err) {
74
- if (err instanceof QdrantServiceError) {
75
- return res.status(err.statusCode).json(err.payload);
83
+ if (sendKnownRouteError(res, err)) {
84
+ return;
76
85
  }
77
86
  logger.error({ err }, "delete collection failed");
78
87
  const errorMessage = err instanceof Error ? err.message : String(err);