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.
- package/README.md +2 -2
- package/dist/config/env.d.ts +8 -3
- package/dist/config/env.js +15 -5
- package/dist/package/api.d.ts +2 -2
- package/dist/package/api.js +2 -2
- package/dist/qdrant/QdrantTypes.d.ts +19 -0
- package/dist/qdrant/QdrantTypes.js +1 -0
- package/dist/repositories/collectionsRepo.d.ts +2 -1
- package/dist/repositories/collectionsRepo.js +103 -62
- package/dist/repositories/collectionsRepo.one-table.js +47 -129
- package/dist/repositories/pointsRepo.d.ts +5 -7
- package/dist/repositories/pointsRepo.js +6 -3
- package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -0
- package/dist/repositories/pointsRepo.one-table/Delete.js +111 -0
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +11 -0
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +32 -0
- package/dist/repositories/pointsRepo.one-table/Search/Approximate.d.ts +18 -0
- package/dist/repositories/pointsRepo.one-table/Search/Approximate.js +119 -0
- package/dist/repositories/pointsRepo.one-table/Search/Exact.d.ts +17 -0
- package/dist/repositories/pointsRepo.one-table/Search/Exact.js +101 -0
- package/dist/repositories/pointsRepo.one-table/Search/index.d.ts +8 -0
- package/dist/repositories/pointsRepo.one-table/Search/index.js +30 -0
- package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -0
- package/dist/repositories/pointsRepo.one-table/Upsert.js +100 -0
- package/dist/repositories/pointsRepo.one-table.d.ts +3 -13
- package/dist/repositories/pointsRepo.one-table.js +3 -403
- package/dist/routes/collections.js +61 -7
- package/dist/routes/points.js +71 -3
- package/dist/server.d.ts +1 -0
- package/dist/server.js +70 -2
- package/dist/services/CollectionService.d.ts +9 -0
- package/dist/services/CollectionService.js +9 -0
- package/dist/services/PointsService.d.ts +8 -10
- package/dist/services/PointsService.js +78 -4
- package/dist/types.d.ts +72 -8
- package/dist/types.js +43 -17
- package/dist/utils/normalization.d.ts +1 -0
- package/dist/utils/normalization.js +15 -13
- package/dist/utils/retry.js +29 -19
- package/dist/utils/typeGuards.d.ts +1 -0
- package/dist/utils/typeGuards.js +3 -0
- package/dist/utils/vectorBinary.js +88 -9
- package/dist/ydb/QueryDiagnostics.d.ts +6 -0
- package/dist/ydb/QueryDiagnostics.js +52 -0
- package/dist/ydb/SessionPool.d.ts +36 -0
- package/dist/ydb/SessionPool.js +248 -0
- package/dist/ydb/bulkUpsert.d.ts +6 -0
- package/dist/ydb/bulkUpsert.js +52 -0
- package/dist/ydb/client.d.ts +17 -16
- package/dist/ydb/client.js +427 -62
- package/dist/ydb/helpers.d.ts +0 -2
- package/dist/ydb/helpers.js +0 -7
- package/dist/ydb/schema.js +171 -77
- package/package.json +12 -7
- package/dist/repositories/collectionsRepo.shared.d.ts +0 -2
- 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({
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
export
|
|
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.
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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
|
|
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:
|
|
6
|
-
|
|
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:
|
|
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
|
|
29
|
-
points: z.array(
|
|
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 ||
|
|
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
|
|
16
|
+
const queryVector = query.vector;
|
|
16
17
|
if (isNumberArray(queryVector))
|
|
17
18
|
return queryVector;
|
|
18
|
-
const nearest = query
|
|
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
|
|
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
|
|
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
|
|
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
|
|
73
|
-
const rawLimit = b
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/utils/retry.js
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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:
|
|
40
|
-
?
|
|
41
|
-
: new Error(typeof
|
|
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
|
-
|
|
44
|
-
|
|
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>;
|