ydb-qdrant 6.0.0 → 7.0.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.
Files changed (56) hide show
  1. package/README.md +2 -2
  2. package/dist/config/env.d.ts +8 -3
  3. package/dist/config/env.js +15 -5
  4. package/dist/package/api.d.ts +2 -2
  5. package/dist/package/api.js +2 -2
  6. package/dist/qdrant/QdrantTypes.d.ts +19 -0
  7. package/dist/qdrant/QdrantTypes.js +1 -0
  8. package/dist/repositories/collectionsRepo.d.ts +2 -1
  9. package/dist/repositories/collectionsRepo.js +103 -62
  10. package/dist/repositories/collectionsRepo.one-table.js +47 -129
  11. package/dist/repositories/pointsRepo.d.ts +5 -7
  12. package/dist/repositories/pointsRepo.js +6 -3
  13. package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -0
  14. package/dist/repositories/pointsRepo.one-table/Delete.js +111 -0
  15. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +11 -0
  16. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +32 -0
  17. package/dist/repositories/pointsRepo.one-table/Search/Approximate.d.ts +18 -0
  18. package/dist/repositories/pointsRepo.one-table/Search/Approximate.js +119 -0
  19. package/dist/repositories/pointsRepo.one-table/Search/Exact.d.ts +17 -0
  20. package/dist/repositories/pointsRepo.one-table/Search/Exact.js +101 -0
  21. package/dist/repositories/pointsRepo.one-table/Search/index.d.ts +8 -0
  22. package/dist/repositories/pointsRepo.one-table/Search/index.js +30 -0
  23. package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -0
  24. package/dist/repositories/pointsRepo.one-table/Upsert.js +100 -0
  25. package/dist/repositories/pointsRepo.one-table.d.ts +3 -13
  26. package/dist/repositories/pointsRepo.one-table.js +3 -403
  27. package/dist/routes/collections.js +61 -7
  28. package/dist/routes/points.js +71 -3
  29. package/dist/server.d.ts +1 -0
  30. package/dist/server.js +70 -2
  31. package/dist/services/CollectionService.d.ts +9 -0
  32. package/dist/services/CollectionService.js +9 -0
  33. package/dist/services/PointsService.d.ts +8 -10
  34. package/dist/services/PointsService.js +78 -4
  35. package/dist/types.d.ts +72 -8
  36. package/dist/types.js +43 -17
  37. package/dist/utils/normalization.d.ts +1 -0
  38. package/dist/utils/normalization.js +15 -13
  39. package/dist/utils/retry.js +29 -19
  40. package/dist/utils/typeGuards.d.ts +1 -0
  41. package/dist/utils/typeGuards.js +3 -0
  42. package/dist/utils/vectorBinary.js +88 -9
  43. package/dist/ydb/QueryDiagnostics.d.ts +6 -0
  44. package/dist/ydb/QueryDiagnostics.js +52 -0
  45. package/dist/ydb/SessionPool.d.ts +36 -0
  46. package/dist/ydb/SessionPool.js +248 -0
  47. package/dist/ydb/bulkUpsert.d.ts +6 -0
  48. package/dist/ydb/bulkUpsert.js +52 -0
  49. package/dist/ydb/client.d.ts +17 -16
  50. package/dist/ydb/client.js +427 -62
  51. package/dist/ydb/helpers.d.ts +0 -2
  52. package/dist/ydb/helpers.js +0 -7
  53. package/dist/ydb/schema.js +171 -77
  54. package/package.json +12 -7
  55. package/dist/repositories/collectionsRepo.shared.d.ts +0 -2
  56. package/dist/repositories/collectionsRepo.shared.js +0 -26
package/dist/server.js CHANGED
@@ -22,18 +22,86 @@ export async function healthHandler(_req, res) {
22
22
  logger.error({ err }, isTimeout
23
23
  ? "YDB compilation timeout during health probe; scheduling process exit"
24
24
  : "YDB health probe failed; scheduling process exit");
25
- res.status(503).json({ status: "error", error: "YDB health probe failed" });
25
+ res.status(503).json({
26
+ status: "error",
27
+ error: "YDB health probe failed",
28
+ });
26
29
  scheduleExit(1);
27
30
  return;
28
31
  }
29
32
  res.json({ status: "ok" });
30
33
  }
34
+ export function rootHandler(_req, res) {
35
+ const version = process.env.npm_package_version ?? "unknown";
36
+ res.json({ title: "ydb-qdrant", version });
37
+ }
31
38
  export function buildServer() {
32
39
  const app = express();
33
- app.use(express.json({ limit: "20mb" }));
34
40
  app.use(requestLogger);
41
+ app.use(express.json({ limit: "20mb" }));
42
+ app.get("/", rootHandler);
35
43
  app.get("/health", healthHandler);
36
44
  app.use("/collections", collectionsRouter);
37
45
  app.use("/collections", pointsRouter);
46
+ app.use((err, req, res, next) => {
47
+ if (!isRequestAbortedError(err)) {
48
+ next(err);
49
+ return;
50
+ }
51
+ // Client closed the connection while the request body was being read.
52
+ // Avoid Express default handler printing a stacktrace to stderr.
53
+ if (res.headersSent || res.writableEnded) {
54
+ return;
55
+ }
56
+ res.status(400).json({ status: "error", error: "request aborted" });
57
+ });
58
+ // Catch-all error handler: avoid Express default handler printing stacktraces to stderr
59
+ // and provide consistent JSON error responses.
60
+ app.use((err, _req, res, _next) => {
61
+ logger.error({ err }, "Unhandled error in Express middleware");
62
+ void _next;
63
+ if (res.headersSent || res.writableEnded) {
64
+ return;
65
+ }
66
+ const statusCode = extractHttpStatusCode(err) ?? 500;
67
+ const errorMessage = err instanceof Error ? err.message : String(err);
68
+ res.status(statusCode).json({
69
+ status: "error",
70
+ error: errorMessage,
71
+ });
72
+ });
38
73
  return app;
39
74
  }
75
+ function isRequestAbortedError(err) {
76
+ if (!err || typeof err !== "object") {
77
+ return false;
78
+ }
79
+ const typeValue = "type" in err && typeof err.type === "string" ? err.type : undefined;
80
+ if (typeValue === "request.aborted") {
81
+ return true;
82
+ }
83
+ if ("message" in err && typeof err.message === "string") {
84
+ return err.message.includes("request aborted");
85
+ }
86
+ return false;
87
+ }
88
+ function extractHttpStatusCode(err) {
89
+ if (!err || typeof err !== "object") {
90
+ return undefined;
91
+ }
92
+ const obj = err;
93
+ let statusCodeValue;
94
+ if (typeof obj.statusCode === "number") {
95
+ statusCodeValue = obj.statusCode;
96
+ }
97
+ else if (typeof obj.status === "number") {
98
+ statusCodeValue = obj.status;
99
+ }
100
+ if (statusCodeValue === undefined ||
101
+ !Number.isInteger(statusCodeValue) ||
102
+ statusCodeValue < 400 ||
103
+ statusCodeValue > 599) {
104
+ return undefined;
105
+ }
106
+ return statusCodeValue;
107
+ }
@@ -24,6 +24,15 @@ export declare function getCollection(ctx: CollectionContextInput): Promise<{
24
24
  distance: DistanceKind;
25
25
  data_type: string;
26
26
  };
27
+ config: {
28
+ params: {
29
+ vectors: {
30
+ size: number;
31
+ distance: DistanceKind;
32
+ data_type: string;
33
+ };
34
+ };
35
+ };
27
36
  }>;
28
37
  export declare function deleteCollection(ctx: CollectionContextInput): Promise<{
29
38
  acknowledged: boolean;
@@ -64,6 +64,15 @@ export async function getCollection(ctx) {
64
64
  distance: meta.distance,
65
65
  data_type: meta.vectorType,
66
66
  },
67
+ config: {
68
+ params: {
69
+ vectors: {
70
+ size: meta.dimension,
71
+ distance: meta.distance,
72
+ data_type: meta.vectorType,
73
+ },
74
+ },
75
+ },
67
76
  };
68
77
  }
69
78
  export async function deleteCollection(ctx) {
@@ -1,21 +1,19 @@
1
1
  import { type CollectionContextInput } from "./CollectionService.js";
2
+ import type { QdrantPayload } from "../qdrant/QdrantTypes.js";
2
3
  type PointsContextInput = CollectionContextInput;
4
+ type InternalScoredPoint = {
5
+ id: string;
6
+ score: number;
7
+ payload?: QdrantPayload;
8
+ };
3
9
  export declare function upsertPoints(ctx: PointsContextInput, body: unknown): Promise<{
4
10
  upserted: number;
5
11
  }>;
6
12
  export declare function searchPoints(ctx: PointsContextInput, body: unknown): Promise<{
7
- points: Array<{
8
- id: string;
9
- score: number;
10
- payload?: Record<string, unknown>;
11
- }>;
13
+ points: InternalScoredPoint[];
12
14
  }>;
13
15
  export declare function queryPoints(ctx: PointsContextInput, body: unknown): Promise<{
14
- points: Array<{
15
- id: string;
16
- score: number;
17
- payload?: Record<string, unknown>;
18
- }>;
16
+ points: InternalScoredPoint[];
19
17
  }>;
20
18
  export declare function deletePoints(ctx: PointsContextInput, body: unknown): Promise<{
21
19
  deleted: number;
@@ -1,12 +1,70 @@
1
1
  import { UpsertPointsReq, SearchReq, DeletePointsReq } from "../types.js";
2
2
  import { ensureMetaTable } from "../ydb/schema.js";
3
3
  import { getCollectionMeta, touchCollectionLastAccess, } from "../repositories/collectionsRepo.js";
4
- import { deletePoints as repoDeletePoints, searchPoints as repoSearchPoints, upsertPoints as repoUpsertPoints, } from "../repositories/pointsRepo.js";
4
+ import { deletePoints as repoDeletePoints, deletePointsByPathSegments as repoDeletePointsByPathSegments, searchPoints as repoSearchPoints, upsertPoints as repoUpsertPoints, } from "../repositories/pointsRepo.js";
5
5
  import { logger } from "../logging/logger.js";
6
6
  import { QdrantServiceError, isVectorDimensionMismatchError, } from "./errors.js";
7
7
  import { normalizeCollectionContextShared } from "./CollectionService.shared.js";
8
8
  import { resolvePointsTableAndUidOneTable } from "./CollectionService.one-table.js";
9
9
  import { normalizeSearchBodyForSearch, normalizeSearchBodyForQuery, } from "../utils/normalization.js";
10
+ import { isRecord } from "../utils/typeGuards.js";
11
+ function parsePathSegmentsFilterToPaths(filter) {
12
+ const extractMust = (must) => {
13
+ if (!Array.isArray(must) || must.length === 0)
14
+ return null;
15
+ const pairs = [];
16
+ for (const cond of must) {
17
+ if (!isRecord(cond))
18
+ return null;
19
+ const key = cond.key;
20
+ if (typeof key !== "string")
21
+ return null;
22
+ const m = /^pathSegments\.(\d+)$/.exec(key);
23
+ if (!m)
24
+ return null;
25
+ const idx = Number(m[1]);
26
+ if (!Number.isInteger(idx) || idx < 0)
27
+ return null;
28
+ const match = cond.match;
29
+ if (!isRecord(match))
30
+ return null;
31
+ const value = match.value;
32
+ if (typeof value !== "string")
33
+ return null;
34
+ pairs.push({ idx, value });
35
+ }
36
+ pairs.sort((a, b) => a.idx - b.idx);
37
+ // Require contiguous indexes starting from 0 to avoid ambiguous matches.
38
+ for (let i = 0; i < pairs.length; i += 1) {
39
+ if (pairs[i].idx !== i)
40
+ return null;
41
+ }
42
+ return pairs.map((p) => p.value);
43
+ };
44
+ if (!isRecord(filter))
45
+ return null;
46
+ const must = filter.must;
47
+ if (must !== undefined) {
48
+ const path = extractMust(must);
49
+ return path ? [path] : null;
50
+ }
51
+ const should = filter.should;
52
+ if (should !== undefined) {
53
+ if (!Array.isArray(should) || should.length === 0)
54
+ return null;
55
+ const paths = [];
56
+ for (const g of should) {
57
+ if (!isRecord(g))
58
+ return null;
59
+ const path = extractMust(g.must);
60
+ if (!path)
61
+ return null;
62
+ paths.push(path);
63
+ }
64
+ return paths;
65
+ }
66
+ return null;
67
+ }
10
68
  export async function upsertPoints(ctx, body) {
11
69
  await ensureMetaTable();
12
70
  const normalized = normalizeCollectionContextShared(ctx.tenant, ctx.collection, ctx.apiKey, ctx.userAgent);
@@ -27,7 +85,9 @@ export async function upsertPoints(ctx, body) {
27
85
  const { tableName, uid } = await resolvePointsTableAndUidOneTable(normalized);
28
86
  let upserted;
29
87
  try {
30
- upserted = await repoUpsertPoints(tableName, parsed.data.points, meta.dimension, uid);
88
+ // Narrow Qdrant OpenAPI types to the dense-vector subset we support.
89
+ const points = parsed.data.points;
90
+ upserted = await repoUpsertPoints(tableName, points, meta.dimension, uid);
31
91
  }
32
92
  catch (err) {
33
93
  if (isVectorDimensionMismatchError(err)) {
@@ -89,9 +149,10 @@ async function executeSearch(ctx, normalizedSearch, source) {
89
149
  distance: meta.distance,
90
150
  vectorType: meta.vectorType,
91
151
  }, `${source}: executing`);
152
+ const filterPaths = parsePathSegmentsFilterToPaths(normalizedSearch.filter);
92
153
  let hits;
93
154
  try {
94
- hits = await repoSearchPoints(tableName, parsed.data.vector, parsed.data.top, parsed.data.with_payload, meta.distance, meta.dimension, uid);
155
+ hits = await repoSearchPoints(tableName, parsed.data.vector, parsed.data.top, parsed.data.with_payload, meta.distance, meta.dimension, uid, filterPaths ?? undefined);
95
156
  }
96
157
  catch (err) {
97
158
  if (isVectorDimensionMismatchError(err)) {
@@ -163,7 +224,20 @@ export async function deletePoints(ctx, body) {
163
224
  });
164
225
  }
165
226
  const { tableName, uid } = await resolvePointsTableAndUidOneTable(normalized);
166
- const deleted = await repoDeletePoints(tableName, parsed.data.points, uid);
227
+ let deleted;
228
+ if ("points" in parsed.data) {
229
+ deleted = await repoDeletePoints(tableName, parsed.data.points, uid);
230
+ }
231
+ else {
232
+ const paths = parsePathSegmentsFilterToPaths(parsed.data.filter);
233
+ if (!paths) {
234
+ throw new QdrantServiceError(400, {
235
+ status: "error",
236
+ error: "unsupported delete filter: only pathSegments.N match filters with must/should are supported",
237
+ });
238
+ }
239
+ deleted = await repoDeletePointsByPathSegments(tableName, uid, paths);
240
+ }
167
241
  await touchCollectionLastAccess(normalized.metaKey);
168
242
  return { deleted };
169
243
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  import { z } from "zod";
2
- export type DistanceKind = "Cosine" | "Euclid" | "Dot" | "Manhattan";
3
- export type VectorType = "float";
2
+ import type { QdrantDistance, QdrantPointId, QdrantDenseVector } from "./qdrant/QdrantTypes.js";
3
+ export declare const DistanceKindSchema: z.ZodEnum<{
4
+ Cosine: "Cosine";
5
+ Euclid: "Euclid";
6
+ Dot: "Dot";
7
+ Manhattan: "Manhattan";
8
+ }>;
9
+ export type DistanceKind = QdrantDistance;
10
+ export declare const VectorTypeSchema: z.ZodLiteral<"float">;
11
+ export type VectorType = z.infer<typeof VectorTypeSchema>;
12
+ export declare const PointIdSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
13
+ export type PointId = QdrantPointId;
14
+ export declare const DenseVectorSchema: z.ZodArray<z.ZodNumber>;
15
+ export type DenseVector = QdrantDenseVector;
4
16
  /**
5
17
  * Collection metadata from qdr__collections table.
6
18
  *
@@ -17,17 +29,25 @@ export interface CollectionMeta {
17
29
  export declare const CreateCollectionReq: z.ZodObject<{
18
30
  vectors: z.ZodObject<{
19
31
  size: z.ZodNumber;
20
- distance: z.ZodType<DistanceKind>;
21
- data_type: z.ZodOptional<z.ZodEnum<{
22
- float: "float";
23
- }>>;
32
+ distance: z.ZodEnum<{
33
+ Cosine: "Cosine";
34
+ Euclid: "Euclid";
35
+ Dot: "Dot";
36
+ Manhattan: "Manhattan";
37
+ }>;
38
+ data_type: z.ZodOptional<z.ZodLiteral<"float">>;
24
39
  }, z.core.$strip>;
25
40
  }, z.core.$strip>;
41
+ export declare const UpsertPointSchema: z.ZodObject<{
42
+ id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
43
+ vector: z.ZodArray<z.ZodNumber>;
44
+ payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
45
+ }, z.core.$strip>;
26
46
  export declare const UpsertPointsReq: z.ZodObject<{
27
47
  points: z.ZodArray<z.ZodObject<{
28
48
  id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
29
49
  vector: z.ZodArray<z.ZodNumber>;
30
- payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
50
+ payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
31
51
  }, z.core.$strip>>;
32
52
  }, z.core.$strip>;
33
53
  export declare const SearchReq: z.ZodObject<{
@@ -35,6 +55,50 @@ export declare const SearchReq: z.ZodObject<{
35
55
  top: z.ZodNumber;
36
56
  with_payload: z.ZodOptional<z.ZodBoolean>;
37
57
  }, z.core.$strip>;
38
- export declare const DeletePointsReq: z.ZodObject<{
58
+ export declare const DeletePointsByIdsReq: z.ZodObject<{
39
59
  points: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
40
60
  }, z.core.$strip>;
61
+ export declare const DeletePointsByFilterReq: z.ZodObject<{
62
+ filter: z.ZodUnion<readonly [z.ZodObject<{
63
+ must: z.ZodArray<z.ZodObject<{
64
+ key: z.ZodString;
65
+ match: z.ZodObject<{
66
+ value: z.ZodString;
67
+ }, z.core.$strip>;
68
+ }, z.core.$strip>>;
69
+ }, z.core.$strip>, z.ZodObject<{
70
+ should: z.ZodArray<z.ZodObject<{
71
+ must: z.ZodArray<z.ZodObject<{
72
+ key: z.ZodString;
73
+ match: z.ZodObject<{
74
+ value: z.ZodString;
75
+ }, z.core.$strip>;
76
+ }, z.core.$strip>>;
77
+ }, z.core.$strip>>;
78
+ }, z.core.$strip>]>;
79
+ }, z.core.$strip>;
80
+ export declare const DeletePointsReq: z.ZodUnion<readonly [z.ZodObject<{
81
+ points: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
82
+ }, z.core.$strip>, z.ZodObject<{
83
+ filter: z.ZodUnion<readonly [z.ZodObject<{
84
+ must: z.ZodArray<z.ZodObject<{
85
+ key: z.ZodString;
86
+ match: z.ZodObject<{
87
+ value: z.ZodString;
88
+ }, z.core.$strip>;
89
+ }, z.core.$strip>>;
90
+ }, z.core.$strip>, z.ZodObject<{
91
+ should: z.ZodArray<z.ZodObject<{
92
+ must: z.ZodArray<z.ZodObject<{
93
+ key: z.ZodString;
94
+ match: z.ZodObject<{
95
+ value: z.ZodString;
96
+ }, z.core.$strip>;
97
+ }, z.core.$strip>>;
98
+ }, z.core.$strip>>;
99
+ }, z.core.$strip>]>;
100
+ }, z.core.$strip>]>;
101
+ export type CreateCollectionBody = z.infer<typeof CreateCollectionReq>;
102
+ export type UpsertPointsBody = z.infer<typeof UpsertPointsReq>;
103
+ export type SearchBody = z.infer<typeof SearchReq>;
104
+ export type DeletePointsBody = z.infer<typeof DeletePointsReq>;
package/dist/types.js CHANGED
@@ -1,30 +1,56 @@
1
1
  import { z } from "zod";
2
+ const DISTANCE_KIND_VALUES = [
3
+ "Cosine",
4
+ "Euclid",
5
+ "Dot",
6
+ "Manhattan",
7
+ ];
8
+ export const DistanceKindSchema = z.enum(DISTANCE_KIND_VALUES);
9
+ export const VectorTypeSchema = z.literal("float");
10
+ export const PointIdSchema = z.union([z.string(), z.number()]);
11
+ export const DenseVectorSchema = z.array(z.number());
2
12
  export const CreateCollectionReq = z.object({
3
13
  vectors: z.object({
4
14
  size: z.number().int().positive(),
5
- distance: z.enum([
6
- "Cosine",
7
- "Euclid",
8
- "Dot",
9
- "Manhattan",
10
- ]),
11
- data_type: z.enum(["float"]).optional(),
15
+ distance: DistanceKindSchema,
16
+ data_type: VectorTypeSchema.optional(),
12
17
  }),
13
18
  });
19
+ export const UpsertPointSchema = z.object({
20
+ id: PointIdSchema,
21
+ vector: DenseVectorSchema,
22
+ payload: z.record(z.string(), z.unknown()).optional(),
23
+ });
14
24
  export const UpsertPointsReq = z.object({
15
- points: z
16
- .array(z.object({
17
- id: z.union([z.string(), z.number()]),
18
- vector: z.array(z.number()),
19
- payload: z.record(z.string(), z.any()).optional(),
20
- }))
21
- .min(1),
25
+ points: z.array(UpsertPointSchema).min(1),
22
26
  });
23
27
  export const SearchReq = z.object({
24
- vector: z.array(z.number()).min(1),
28
+ vector: DenseVectorSchema.min(1),
25
29
  top: z.number().int().positive().max(1000),
26
30
  with_payload: z.boolean().optional(),
27
31
  });
28
- export const DeletePointsReq = z.object({
29
- points: z.array(z.union([z.string(), z.number()])).min(1),
32
+ export const DeletePointsByIdsReq = z.object({
33
+ points: z.array(PointIdSchema).min(1),
34
+ });
35
+ const DeletePointsFilterCondition = z.object({
36
+ key: z.string(),
37
+ match: z.object({
38
+ value: z.string(),
39
+ }),
40
+ });
41
+ const DeletePointsFilterMust = z.object({
42
+ must: z.array(DeletePointsFilterCondition).min(1),
43
+ });
44
+ const DeletePointsFilter = z.union([
45
+ DeletePointsFilterMust,
46
+ z.object({
47
+ should: z.array(DeletePointsFilterMust).min(1),
48
+ }),
49
+ ]);
50
+ export const DeletePointsByFilterReq = z.object({
51
+ filter: DeletePointsFilter,
30
52
  });
53
+ export const DeletePointsReq = z.union([
54
+ DeletePointsByIdsReq,
55
+ DeletePointsByFilterReq,
56
+ ]);
@@ -3,6 +3,7 @@ export interface SearchNormalizationResult {
3
3
  top: number | undefined;
4
4
  withPayload: boolean | undefined;
5
5
  scoreThreshold: number | undefined;
6
+ filter?: unknown;
6
7
  }
7
8
  export declare function isNumberArray(value: unknown): value is number[];
8
9
  export declare function extractVectorLoose(body: unknown, depth?: number): number[] | undefined;
@@ -1,8 +1,9 @@
1
+ import { isRecord } from "./typeGuards.js";
1
2
  export function isNumberArray(value) {
2
3
  return Array.isArray(value) && value.every((x) => typeof x === "number");
3
4
  }
4
5
  export function extractVectorLoose(body, depth = 0) {
5
- if (!body || typeof body !== "object" || depth > 3) {
6
+ if (!isRecord(body) || depth > 3) {
6
7
  return undefined;
7
8
  }
8
9
  const obj = body;
@@ -10,17 +11,17 @@ export function extractVectorLoose(body, depth = 0) {
10
11
  return obj.vector;
11
12
  if (isNumberArray(obj.embedding))
12
13
  return obj.embedding;
13
- const query = obj.query;
14
+ const query = isRecord(obj.query) ? obj.query : undefined;
14
15
  if (query) {
15
- const queryVector = query["vector"];
16
+ const queryVector = query.vector;
16
17
  if (isNumberArray(queryVector))
17
18
  return queryVector;
18
- const nearest = query["nearest"];
19
+ const nearest = isRecord(query.nearest) ? query.nearest : undefined;
19
20
  if (nearest && isNumberArray(nearest.vector)) {
20
21
  return nearest.vector;
21
22
  }
22
23
  }
23
- const nearest = obj.nearest;
24
+ const nearest = isRecord(obj.nearest) ? obj.nearest : undefined;
24
25
  if (nearest && isNumberArray(nearest.vector)) {
25
26
  return nearest.vector;
26
27
  }
@@ -42,7 +43,7 @@ export function extractVectorLoose(body, depth = 0) {
42
43
  return undefined;
43
44
  }
44
45
  export function normalizeSearchBodyForSearch(body) {
45
- if (!body || typeof body !== "object") {
46
+ if (!isRecord(body)) {
46
47
  return {
47
48
  vector: undefined,
48
49
  top: undefined,
@@ -51,12 +52,12 @@ export function normalizeSearchBodyForSearch(body) {
51
52
  };
52
53
  }
53
54
  const b = body;
54
- const rawVector = b["vector"];
55
+ const rawVector = b.vector;
55
56
  const vector = isNumberArray(rawVector) ? rawVector : undefined;
56
57
  return normalizeSearchCommon(b, vector);
57
58
  }
58
59
  export function normalizeSearchBodyForQuery(body) {
59
- if (!body || typeof body !== "object") {
60
+ if (!isRecord(body)) {
60
61
  return {
61
62
  vector: undefined,
62
63
  top: undefined,
@@ -69,13 +70,14 @@ export function normalizeSearchBodyForQuery(body) {
69
70
  return normalizeSearchCommon(b, vector);
70
71
  }
71
72
  function normalizeSearchCommon(b, vector) {
72
- const rawTop = b["top"];
73
- const rawLimit = b["limit"];
73
+ const rawTop = b.top;
74
+ const rawLimit = b.limit;
74
75
  const topFromTop = typeof rawTop === "number" ? rawTop : undefined;
75
76
  const topFromLimit = typeof rawLimit === "number" ? rawLimit : undefined;
76
77
  const top = topFromTop ?? topFromLimit;
78
+ const filter = b.filter;
77
79
  let withPayload;
78
- const rawWithPayload = b["with_payload"];
80
+ const rawWithPayload = b.with_payload;
79
81
  if (typeof rawWithPayload === "boolean") {
80
82
  withPayload = rawWithPayload;
81
83
  }
@@ -83,10 +85,10 @@ function normalizeSearchCommon(b, vector) {
83
85
  typeof rawWithPayload === "object") {
84
86
  withPayload = true;
85
87
  }
86
- const thresholdRaw = b["score_threshold"];
88
+ const thresholdRaw = b.score_threshold;
87
89
  const thresholdValue = typeof thresholdRaw === "number" ? thresholdRaw : Number(thresholdRaw);
88
90
  const scoreThreshold = Number.isFinite(thresholdValue)
89
91
  ? thresholdValue
90
92
  : undefined;
91
- return { vector, top, withPayload, scoreThreshold };
93
+ return { vector, top, withPayload, scoreThreshold, filter };
92
94
  }
@@ -1,4 +1,5 @@
1
1
  import { logger } from "../logging/logger.js";
2
+ import { retry as ydbRetry } from "@ydbjs/retry";
2
3
  const DEFAULT_MAX_RETRIES = 6;
3
4
  const DEFAULT_BASE_DELAY_MS = 250;
4
5
  export function isTransientYdbError(error) {
@@ -22,26 +23,35 @@ export async function withRetry(fn, options = {}) {
22
23
  const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
23
24
  const isTransient = options.isTransient ?? isTransientYdbError;
24
25
  const context = options.context ?? {};
25
- let attempt = 0;
26
- while (true) {
27
- try {
28
- return await fn();
29
- }
30
- catch (e) {
31
- if (!isTransient(e) || attempt >= maxRetries) {
32
- throw e;
33
- }
34
- const backoffMs = Math.floor(baseDelayMs * Math.pow(2, attempt) + Math.random() * 100);
26
+ // We keep the public API in terms of `maxRetries`, but @ydbjs/retry uses a budget
27
+ // in terms of total attempts. Convert retries→attempts.
28
+ const attemptsBudget = Math.max(0, maxRetries) + 1;
29
+ const delayByAttempt = new Map();
30
+ return await ydbRetry({
31
+ budget: attemptsBudget,
32
+ retry: (error) => isTransient(error),
33
+ strategy: (ctx) => {
34
+ // Preserve previous backoff shape: baseDelayMs * 2^attemptIndex + jitter(0..100)
35
+ // where attemptIndex started at 0 for the first retry.
36
+ const attemptIndex = Math.max(0, ctx.attempt - 1);
37
+ const delayMs = Math.floor(baseDelayMs * Math.pow(2, attemptIndex) + Math.random() * 100);
38
+ delayByAttempt.set(ctx.attempt, delayMs);
39
+ return delayMs;
40
+ },
41
+ onRetry: (ctx) => {
42
+ const attemptIndex = Math.max(0, ctx.attempt - 1);
35
43
  logger.warn({
36
44
  ...context,
37
- attempt,
38
- backoffMs,
39
- err: e instanceof Error
40
- ? e
41
- : new Error(typeof e === "string" ? e : JSON.stringify(e)),
45
+ attempt: attemptIndex,
46
+ backoffMs: delayByAttempt.get(ctx.attempt),
47
+ err: ctx.error instanceof Error
48
+ ? ctx.error
49
+ : new Error(typeof ctx.error === "string"
50
+ ? ctx.error
51
+ : JSON.stringify(ctx.error)),
42
52
  }, "operation aborted due to transient error; retrying");
43
- await new Promise((r) => setTimeout(r, backoffMs));
44
- attempt += 1;
45
- }
46
- }
53
+ },
54
+ }, async () => {
55
+ return await fn();
56
+ });
47
57
  }
@@ -0,0 +1 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,3 @@
1
+ export function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }