ydb-qdrant 6.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/env.d.ts +0 -3
- package/dist/config/env.js +0 -17
- package/dist/package/api.d.ts +3 -0
- package/dist/qdrant/QdrantRestTypes.d.ts +35 -0
- package/dist/qdrant/QdrantRestTypes.js +1 -0
- package/dist/repositories/collectionsRepo.one-table.js +37 -63
- package/dist/repositories/collectionsRepo.shared.js +8 -2
- package/dist/repositories/pointsRepo.d.ts +5 -11
- 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 +166 -0
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +14 -0
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +33 -0
- package/dist/repositories/pointsRepo.one-table/Search.d.ts +4 -0
- package/dist/repositories/pointsRepo.one-table/Search.js +208 -0
- package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -0
- package/dist/repositories/pointsRepo.one-table/Upsert.js +85 -0
- package/dist/repositories/pointsRepo.one-table.d.ts +3 -13
- package/dist/repositories/pointsRepo.one-table.js +3 -403
- package/dist/routes/points.js +17 -4
- 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 +3 -10
- package/dist/services/PointsService.js +73 -3
- package/dist/types.d.ts +59 -5
- package/dist/types.js +27 -3
- package/dist/utils/normalization.d.ts +1 -0
- package/dist/utils/normalization.js +2 -1
- package/dist/utils/vectorBinary.js +94 -10
- package/dist/ydb/bootstrapMetaTable.d.ts +7 -0
- package/dist/ydb/bootstrapMetaTable.js +75 -0
- package/dist/ydb/client.d.ts +10 -3
- package/dist/ydb/client.js +26 -2
- package/dist/ydb/helpers.d.ts +0 -2
- package/dist/ydb/helpers.js +0 -7
- package/dist/ydb/schema.js +100 -66
- package/package.json +3 -6
|
@@ -1,403 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { withRetry, isTransientYdbError } from "../utils/retry.js";
|
|
5
|
-
import { UPSERT_BATCH_SIZE } from "../ydb/schema.js";
|
|
6
|
-
import { CLIENT_SIDE_SERIALIZATION_ENABLED, SearchMode, UPSERT_OPERATION_TIMEOUT_MS, SEARCH_OPERATION_TIMEOUT_MS, } from "../config/env.js";
|
|
7
|
-
import { logger } from "../logging/logger.js";
|
|
8
|
-
export async function upsertPointsOneTable(tableName, points, dimension, uid) {
|
|
9
|
-
for (const p of points) {
|
|
10
|
-
const id = String(p.id);
|
|
11
|
-
if (p.vector.length !== dimension) {
|
|
12
|
-
const previewLength = Math.min(16, p.vector.length);
|
|
13
|
-
const vectorPreview = previewLength > 0 ? p.vector.slice(0, previewLength) : [];
|
|
14
|
-
logger.warn({
|
|
15
|
-
tableName,
|
|
16
|
-
uid,
|
|
17
|
-
pointId: id,
|
|
18
|
-
vectorLen: p.vector.length,
|
|
19
|
-
expectedDimension: dimension,
|
|
20
|
-
vectorPreview,
|
|
21
|
-
}, "upsertPointsOneTable: vector dimension mismatch");
|
|
22
|
-
throw new Error(`Vector dimension mismatch for id=${id}: got ${p.vector.length}, expected ${dimension}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
let upserted = 0;
|
|
26
|
-
await withSession(async (s) => {
|
|
27
|
-
const settings = createExecuteQuerySettingsWithTimeout({
|
|
28
|
-
keepInCache: true,
|
|
29
|
-
idempotent: true,
|
|
30
|
-
timeoutMs: UPSERT_OPERATION_TIMEOUT_MS,
|
|
31
|
-
});
|
|
32
|
-
for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
|
|
33
|
-
const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
|
|
34
|
-
let ddl;
|
|
35
|
-
let params;
|
|
36
|
-
if (CLIENT_SIDE_SERIALIZATION_ENABLED) {
|
|
37
|
-
ddl = `
|
|
38
|
-
DECLARE $rows AS List<Struct<
|
|
39
|
-
uid: Utf8,
|
|
40
|
-
point_id: Utf8,
|
|
41
|
-
embedding: String,
|
|
42
|
-
embedding_quantized: String,
|
|
43
|
-
payload: JsonDocument
|
|
44
|
-
>>;
|
|
45
|
-
|
|
46
|
-
UPSERT INTO ${tableName} (uid, point_id, embedding, embedding_quantized, payload)
|
|
47
|
-
SELECT
|
|
48
|
-
uid,
|
|
49
|
-
point_id,
|
|
50
|
-
embedding,
|
|
51
|
-
embedding_quantized,
|
|
52
|
-
payload
|
|
53
|
-
FROM AS_TABLE($rows);
|
|
54
|
-
`;
|
|
55
|
-
const rowType = Types.struct({
|
|
56
|
-
uid: Types.UTF8,
|
|
57
|
-
point_id: Types.UTF8,
|
|
58
|
-
embedding: Types.BYTES,
|
|
59
|
-
embedding_quantized: Types.BYTES,
|
|
60
|
-
payload: Types.JSON_DOCUMENT,
|
|
61
|
-
});
|
|
62
|
-
const rowsValue = TypedValues.list(rowType, batch.map((p) => {
|
|
63
|
-
const binaries = buildVectorBinaryParams(p.vector);
|
|
64
|
-
return {
|
|
65
|
-
uid,
|
|
66
|
-
point_id: String(p.id),
|
|
67
|
-
embedding: binaries.float,
|
|
68
|
-
embedding_quantized: binaries.bit,
|
|
69
|
-
payload: JSON.stringify(p.payload ?? {}),
|
|
70
|
-
};
|
|
71
|
-
}));
|
|
72
|
-
params = {
|
|
73
|
-
$rows: rowsValue,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
ddl = `
|
|
78
|
-
DECLARE $rows AS List<Struct<
|
|
79
|
-
uid: Utf8,
|
|
80
|
-
point_id: Utf8,
|
|
81
|
-
vec: List<Float>,
|
|
82
|
-
payload: JsonDocument
|
|
83
|
-
>>;
|
|
84
|
-
|
|
85
|
-
UPSERT INTO ${tableName} (uid, point_id, embedding, embedding_quantized, payload)
|
|
86
|
-
SELECT
|
|
87
|
-
uid,
|
|
88
|
-
point_id,
|
|
89
|
-
Untag(Knn::ToBinaryStringFloat(vec), "FloatVector") AS embedding,
|
|
90
|
-
Untag(Knn::ToBinaryStringBit(vec), "BitVector") AS embedding_quantized,
|
|
91
|
-
payload
|
|
92
|
-
FROM AS_TABLE($rows);
|
|
93
|
-
`;
|
|
94
|
-
const rowType = Types.struct({
|
|
95
|
-
uid: Types.UTF8,
|
|
96
|
-
point_id: Types.UTF8,
|
|
97
|
-
vec: Types.list(Types.FLOAT),
|
|
98
|
-
payload: Types.JSON_DOCUMENT,
|
|
99
|
-
});
|
|
100
|
-
const rowsValue = TypedValues.list(rowType, batch.map((p) => ({
|
|
101
|
-
uid,
|
|
102
|
-
point_id: String(p.id),
|
|
103
|
-
vec: p.vector,
|
|
104
|
-
payload: JSON.stringify(p.payload ?? {}),
|
|
105
|
-
})));
|
|
106
|
-
params = {
|
|
107
|
-
$rows: rowsValue,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
logger.debug({
|
|
111
|
-
tableName,
|
|
112
|
-
mode: CLIENT_SIDE_SERIALIZATION_ENABLED
|
|
113
|
-
? "one_table_upsert_client_side_serialization"
|
|
114
|
-
: "one_table_upsert_server_side_knn",
|
|
115
|
-
batchSize: batch.length,
|
|
116
|
-
yql: ddl,
|
|
117
|
-
params: {
|
|
118
|
-
rows: batch.map((p) => ({
|
|
119
|
-
uid,
|
|
120
|
-
point_id: String(p.id),
|
|
121
|
-
vectorLength: p.vector.length,
|
|
122
|
-
vectorPreview: p.vector.slice(0, 3),
|
|
123
|
-
payload: p.payload ?? {},
|
|
124
|
-
})),
|
|
125
|
-
},
|
|
126
|
-
}, "one_table upsert: executing YQL");
|
|
127
|
-
await withRetry(() => s.executeQuery(ddl, params, undefined, settings), {
|
|
128
|
-
isTransient: isTransientYdbError,
|
|
129
|
-
context: { tableName, batchSize: batch.length },
|
|
130
|
-
});
|
|
131
|
-
upserted += batch.length;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
return upserted;
|
|
135
|
-
}
|
|
136
|
-
async function searchPointsOneTableExact(tableName, queryVector, top, withPayload, distance, dimension, uid) {
|
|
137
|
-
if (queryVector.length !== dimension) {
|
|
138
|
-
throw new Error(`Vector dimension mismatch: got ${queryVector.length}, expected ${dimension}`);
|
|
139
|
-
}
|
|
140
|
-
const { fn, order } = mapDistanceToKnnFn(distance);
|
|
141
|
-
const results = await withSession(async (s) => {
|
|
142
|
-
let yql;
|
|
143
|
-
let params;
|
|
144
|
-
if (CLIENT_SIDE_SERIALIZATION_ENABLED) {
|
|
145
|
-
const binaries = buildVectorBinaryParams(queryVector);
|
|
146
|
-
yql = `
|
|
147
|
-
DECLARE $qbinf AS String;
|
|
148
|
-
DECLARE $k AS Uint32;
|
|
149
|
-
DECLARE $uid AS Utf8;
|
|
150
|
-
SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
|
|
151
|
-
FROM ${tableName}
|
|
152
|
-
WHERE uid = $uid
|
|
153
|
-
ORDER BY score ${order}
|
|
154
|
-
LIMIT $k;
|
|
155
|
-
`;
|
|
156
|
-
params = {
|
|
157
|
-
$qbinf: typeof TypedValues.bytes === "function"
|
|
158
|
-
? TypedValues.bytes(binaries.float)
|
|
159
|
-
: binaries.float,
|
|
160
|
-
$k: TypedValues.uint32(top),
|
|
161
|
-
$uid: TypedValues.utf8(uid),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
const qf = buildVectorParam(queryVector);
|
|
166
|
-
yql = `
|
|
167
|
-
DECLARE $qf AS List<Float>;
|
|
168
|
-
DECLARE $k AS Uint32;
|
|
169
|
-
DECLARE $uid AS Utf8;
|
|
170
|
-
$qbinf = Knn::ToBinaryStringFloat($qf);
|
|
171
|
-
SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
|
|
172
|
-
FROM ${tableName}
|
|
173
|
-
WHERE uid = $uid
|
|
174
|
-
ORDER BY score ${order}
|
|
175
|
-
LIMIT $k;
|
|
176
|
-
`;
|
|
177
|
-
params = {
|
|
178
|
-
$qf: qf,
|
|
179
|
-
$k: TypedValues.uint32(top),
|
|
180
|
-
$uid: TypedValues.utf8(uid),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
logger.debug({
|
|
184
|
-
tableName,
|
|
185
|
-
distance,
|
|
186
|
-
top,
|
|
187
|
-
withPayload,
|
|
188
|
-
mode: CLIENT_SIDE_SERIALIZATION_ENABLED
|
|
189
|
-
? "one_table_exact_client_side_serialization"
|
|
190
|
-
: "one_table_exact",
|
|
191
|
-
yql,
|
|
192
|
-
params: {
|
|
193
|
-
uid,
|
|
194
|
-
top,
|
|
195
|
-
vectorLength: queryVector.length,
|
|
196
|
-
vectorPreview: queryVector.slice(0, 3),
|
|
197
|
-
},
|
|
198
|
-
}, "one_table search (exact): executing YQL");
|
|
199
|
-
const settings = createExecuteQuerySettingsWithTimeout({
|
|
200
|
-
keepInCache: true,
|
|
201
|
-
idempotent: true,
|
|
202
|
-
timeoutMs: SEARCH_OPERATION_TIMEOUT_MS,
|
|
203
|
-
});
|
|
204
|
-
const rs = await s.executeQuery(yql, params, undefined, settings);
|
|
205
|
-
const rowset = rs.resultSets?.[0];
|
|
206
|
-
const rows = (rowset?.rows ?? []);
|
|
207
|
-
return rows.map((row) => {
|
|
208
|
-
const id = row.items?.[0]?.textValue;
|
|
209
|
-
if (typeof id !== "string") {
|
|
210
|
-
throw new Error("point_id is missing in YDB search result");
|
|
211
|
-
}
|
|
212
|
-
let payload;
|
|
213
|
-
let scoreIdx = 1;
|
|
214
|
-
if (withPayload) {
|
|
215
|
-
const payloadText = row.items?.[1]?.textValue;
|
|
216
|
-
if (payloadText) {
|
|
217
|
-
try {
|
|
218
|
-
payload = JSON.parse(payloadText);
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
payload = undefined;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
scoreIdx = 2;
|
|
225
|
-
}
|
|
226
|
-
const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
|
|
227
|
-
return { id, score, ...(payload ? { payload } : {}) };
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
return results;
|
|
231
|
-
}
|
|
232
|
-
async function searchPointsOneTableApproximate(tableName, queryVector, top, withPayload, distance, dimension, uid, overfetchMultiplier) {
|
|
233
|
-
if (queryVector.length !== dimension) {
|
|
234
|
-
throw new Error(`Vector dimension mismatch: got ${queryVector.length}, expected ${dimension}`);
|
|
235
|
-
}
|
|
236
|
-
const { fn, order } = mapDistanceToKnnFn(distance);
|
|
237
|
-
const { fn: bitFn, order: bitOrder } = mapDistanceToBitKnnFn(distance);
|
|
238
|
-
const safeTop = top > 0 ? top : 1;
|
|
239
|
-
const rawCandidateLimit = safeTop * overfetchMultiplier;
|
|
240
|
-
const candidateLimit = Math.max(safeTop, rawCandidateLimit);
|
|
241
|
-
const results = await withSession(async (s) => {
|
|
242
|
-
let yql;
|
|
243
|
-
let params;
|
|
244
|
-
if (CLIENT_SIDE_SERIALIZATION_ENABLED) {
|
|
245
|
-
const binaries = buildVectorBinaryParams(queryVector);
|
|
246
|
-
yql = `
|
|
247
|
-
DECLARE $qbin_bit AS String;
|
|
248
|
-
DECLARE $qbinf AS String;
|
|
249
|
-
DECLARE $candidateLimit AS Uint32;
|
|
250
|
-
DECLARE $safeTop AS Uint32;
|
|
251
|
-
DECLARE $uid AS Utf8;
|
|
252
|
-
|
|
253
|
-
$candidates = (
|
|
254
|
-
SELECT point_id
|
|
255
|
-
FROM ${tableName}
|
|
256
|
-
WHERE uid = $uid AND embedding_quantized IS NOT NULL
|
|
257
|
-
ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
|
|
258
|
-
LIMIT $candidateLimit
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
|
|
262
|
-
FROM ${tableName}
|
|
263
|
-
WHERE uid = $uid
|
|
264
|
-
AND point_id IN $candidates
|
|
265
|
-
ORDER BY score ${order}
|
|
266
|
-
LIMIT $safeTop;
|
|
267
|
-
`;
|
|
268
|
-
params = {
|
|
269
|
-
$qbin_bit: typeof TypedValues.bytes === "function"
|
|
270
|
-
? TypedValues.bytes(binaries.bit)
|
|
271
|
-
: binaries.bit,
|
|
272
|
-
$qbinf: typeof TypedValues.bytes === "function"
|
|
273
|
-
? TypedValues.bytes(binaries.float)
|
|
274
|
-
: binaries.float,
|
|
275
|
-
$candidateLimit: TypedValues.uint32(candidateLimit),
|
|
276
|
-
$safeTop: TypedValues.uint32(safeTop),
|
|
277
|
-
$uid: TypedValues.utf8(uid),
|
|
278
|
-
};
|
|
279
|
-
logger.debug({
|
|
280
|
-
tableName,
|
|
281
|
-
distance,
|
|
282
|
-
top,
|
|
283
|
-
safeTop,
|
|
284
|
-
candidateLimit,
|
|
285
|
-
mode: "one_table_approximate_client_side_serialization",
|
|
286
|
-
yql,
|
|
287
|
-
params: {
|
|
288
|
-
uid,
|
|
289
|
-
safeTop,
|
|
290
|
-
candidateLimit,
|
|
291
|
-
vectorLength: queryVector.length,
|
|
292
|
-
vectorPreview: queryVector.slice(0, 3),
|
|
293
|
-
},
|
|
294
|
-
}, "one_table search (approximate): executing YQL with client-side serialization");
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
const qf = buildVectorParam(queryVector);
|
|
298
|
-
yql = `
|
|
299
|
-
DECLARE $qf AS List<Float>;
|
|
300
|
-
DECLARE $candidateLimit AS Uint32;
|
|
301
|
-
DECLARE $safeTop AS Uint32;
|
|
302
|
-
DECLARE $uid AS Utf8;
|
|
303
|
-
|
|
304
|
-
$qbin_bit = Knn::ToBinaryStringBit($qf);
|
|
305
|
-
$qbinf = Knn::ToBinaryStringFloat($qf);
|
|
306
|
-
|
|
307
|
-
$candidates = (
|
|
308
|
-
SELECT point_id
|
|
309
|
-
FROM ${tableName}
|
|
310
|
-
WHERE uid = $uid AND embedding_quantized IS NOT NULL
|
|
311
|
-
ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
|
|
312
|
-
LIMIT $candidateLimit
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
|
|
316
|
-
FROM ${tableName}
|
|
317
|
-
WHERE uid = $uid
|
|
318
|
-
AND point_id IN $candidates
|
|
319
|
-
ORDER BY score ${order}
|
|
320
|
-
LIMIT $safeTop;
|
|
321
|
-
`;
|
|
322
|
-
params = {
|
|
323
|
-
$qf: qf,
|
|
324
|
-
$candidateLimit: TypedValues.uint32(candidateLimit),
|
|
325
|
-
$safeTop: TypedValues.uint32(safeTop),
|
|
326
|
-
$uid: TypedValues.utf8(uid),
|
|
327
|
-
};
|
|
328
|
-
logger.debug({
|
|
329
|
-
tableName,
|
|
330
|
-
distance,
|
|
331
|
-
top,
|
|
332
|
-
safeTop,
|
|
333
|
-
candidateLimit,
|
|
334
|
-
mode: "one_table_approximate",
|
|
335
|
-
yql,
|
|
336
|
-
params: {
|
|
337
|
-
uid,
|
|
338
|
-
safeTop,
|
|
339
|
-
candidateLimit,
|
|
340
|
-
vectorLength: queryVector.length,
|
|
341
|
-
vectorPreview: queryVector.slice(0, 3),
|
|
342
|
-
},
|
|
343
|
-
}, "one_table search (approximate): executing YQL");
|
|
344
|
-
}
|
|
345
|
-
const settings = createExecuteQuerySettingsWithTimeout({
|
|
346
|
-
keepInCache: true,
|
|
347
|
-
idempotent: true,
|
|
348
|
-
timeoutMs: SEARCH_OPERATION_TIMEOUT_MS,
|
|
349
|
-
});
|
|
350
|
-
const rs = await s.executeQuery(yql, params, undefined, settings);
|
|
351
|
-
const rowset = rs.resultSets?.[0];
|
|
352
|
-
const rows = (rowset?.rows ?? []);
|
|
353
|
-
return rows.map((row) => {
|
|
354
|
-
const id = row.items?.[0]?.textValue;
|
|
355
|
-
if (typeof id !== "string") {
|
|
356
|
-
throw new Error("point_id is missing in YDB search result");
|
|
357
|
-
}
|
|
358
|
-
let payload;
|
|
359
|
-
let scoreIdx = 1;
|
|
360
|
-
if (withPayload) {
|
|
361
|
-
const payloadText = row.items?.[1]?.textValue;
|
|
362
|
-
if (payloadText) {
|
|
363
|
-
try {
|
|
364
|
-
payload = JSON.parse(payloadText);
|
|
365
|
-
}
|
|
366
|
-
catch {
|
|
367
|
-
payload = undefined;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
scoreIdx = 2;
|
|
371
|
-
}
|
|
372
|
-
const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
|
|
373
|
-
return { id, score, ...(payload ? { payload } : {}) };
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
return results;
|
|
377
|
-
}
|
|
378
|
-
export async function searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid, mode, overfetchMultiplier) {
|
|
379
|
-
if (mode === SearchMode.Exact) {
|
|
380
|
-
return await searchPointsOneTableExact(tableName, queryVector, top, withPayload, distance, dimension, uid);
|
|
381
|
-
}
|
|
382
|
-
return await searchPointsOneTableApproximate(tableName, queryVector, top, withPayload, distance, dimension, uid, overfetchMultiplier);
|
|
383
|
-
}
|
|
384
|
-
export async function deletePointsOneTable(tableName, ids, uid) {
|
|
385
|
-
let deleted = 0;
|
|
386
|
-
await withSession(async (s) => {
|
|
387
|
-
const settings = createExecuteQuerySettings();
|
|
388
|
-
for (const id of ids) {
|
|
389
|
-
const yql = `
|
|
390
|
-
DECLARE $uid AS Utf8;
|
|
391
|
-
DECLARE $id AS Utf8;
|
|
392
|
-
DELETE FROM ${tableName} WHERE uid = $uid AND point_id = $id;
|
|
393
|
-
`;
|
|
394
|
-
const params = {
|
|
395
|
-
$uid: TypedValues.utf8(uid),
|
|
396
|
-
$id: TypedValues.utf8(String(id)),
|
|
397
|
-
};
|
|
398
|
-
await s.executeQuery(yql, params, undefined, settings);
|
|
399
|
-
deleted += 1;
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
return deleted;
|
|
403
|
-
}
|
|
1
|
+
export { searchPointsOneTable } from "./pointsRepo.one-table/Search.js";
|
|
2
|
+
export { upsertPointsOneTable } from "./pointsRepo.one-table/Upsert.js";
|
|
3
|
+
export { deletePointsOneTable, deletePointsByPathSegmentsOneTable, } from "./pointsRepo.one-table/Delete.js";
|
package/dist/routes/points.js
CHANGED
|
@@ -5,6 +5,19 @@ import { logger } from "../logging/logger.js";
|
|
|
5
5
|
import { isCompilationTimeoutError } from "../ydb/client.js";
|
|
6
6
|
import { scheduleExit } from "../utils/exit.js";
|
|
7
7
|
export const pointsRouter = Router();
|
|
8
|
+
function toQdrantScoredPoint(p) {
|
|
9
|
+
// We don't currently track per-point versions or return vectors/shard keys,
|
|
10
|
+
// but many Qdrant clients expect these fields to exist in the response.
|
|
11
|
+
return {
|
|
12
|
+
id: p.id,
|
|
13
|
+
version: 0,
|
|
14
|
+
score: p.score,
|
|
15
|
+
payload: p.payload ?? null,
|
|
16
|
+
vector: null,
|
|
17
|
+
shard_key: null,
|
|
18
|
+
order_value: null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
8
21
|
// Qdrant-compatible: PUT /collections/:collection/points (upsert)
|
|
9
22
|
pointsRouter.put("/:collection/points", async (req, res) => {
|
|
10
23
|
try {
|
|
@@ -58,13 +71,13 @@ pointsRouter.post("/:collection/points/upsert", async (req, res) => {
|
|
|
58
71
|
});
|
|
59
72
|
pointsRouter.post("/:collection/points/search", async (req, res) => {
|
|
60
73
|
try {
|
|
61
|
-
const
|
|
74
|
+
const { points } = await searchPoints({
|
|
62
75
|
tenant: req.header("X-Tenant-Id") ?? undefined,
|
|
63
76
|
collection: String(req.params.collection),
|
|
64
77
|
apiKey: req.header("api-key") ?? undefined,
|
|
65
78
|
userAgent: req.header("User-Agent") ?? undefined,
|
|
66
79
|
}, req.body);
|
|
67
|
-
res.json({ status: "ok", result });
|
|
80
|
+
res.json({ status: "ok", result: points.map(toQdrantScoredPoint) });
|
|
68
81
|
}
|
|
69
82
|
catch (err) {
|
|
70
83
|
if (err instanceof QdrantServiceError) {
|
|
@@ -84,13 +97,13 @@ pointsRouter.post("/:collection/points/search", async (req, res) => {
|
|
|
84
97
|
// Compatibility: some clients call POST /collections/:collection/points/query
|
|
85
98
|
pointsRouter.post("/:collection/points/query", async (req, res) => {
|
|
86
99
|
try {
|
|
87
|
-
const
|
|
100
|
+
const { points } = await queryPoints({
|
|
88
101
|
tenant: req.header("X-Tenant-Id") ?? undefined,
|
|
89
102
|
collection: String(req.params.collection),
|
|
90
103
|
apiKey: req.header("api-key") ?? undefined,
|
|
91
104
|
userAgent: req.header("User-Agent") ?? undefined,
|
|
92
105
|
}, req.body);
|
|
93
|
-
res.json({ status: "ok", result });
|
|
106
|
+
res.json({ status: "ok", result: points.map(toQdrantScoredPoint) });
|
|
94
107
|
}
|
|
95
108
|
catch (err) {
|
|
96
109
|
if (err instanceof QdrantServiceError) {
|
package/dist/server.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Request, Response } from "express";
|
|
2
2
|
export declare function healthHandler(_req: Request, res: Response): Promise<void>;
|
|
3
|
+
export declare function rootHandler(_req: Request, res: Response): void;
|
|
3
4
|
export declare function buildServer(): import("express-serve-static-core").Express;
|
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,14 @@
|
|
|
1
1
|
import { type CollectionContextInput } from "./CollectionService.js";
|
|
2
|
+
import type { YdbQdrantScoredPoint } from "../qdrant/QdrantRestTypes.js";
|
|
2
3
|
type PointsContextInput = CollectionContextInput;
|
|
3
4
|
export declare function upsertPoints(ctx: PointsContextInput, body: unknown): Promise<{
|
|
4
5
|
upserted: number;
|
|
5
6
|
}>;
|
|
6
7
|
export declare function searchPoints(ctx: PointsContextInput, body: unknown): Promise<{
|
|
7
|
-
points:
|
|
8
|
-
id: string;
|
|
9
|
-
score: number;
|
|
10
|
-
payload?: Record<string, unknown>;
|
|
11
|
-
}>;
|
|
8
|
+
points: YdbQdrantScoredPoint[];
|
|
12
9
|
}>;
|
|
13
10
|
export declare function queryPoints(ctx: PointsContextInput, body: unknown): Promise<{
|
|
14
|
-
points:
|
|
15
|
-
id: string;
|
|
16
|
-
score: number;
|
|
17
|
-
payload?: Record<string, unknown>;
|
|
18
|
-
}>;
|
|
11
|
+
points: YdbQdrantScoredPoint[];
|
|
19
12
|
}>;
|
|
20
13
|
export declare function deletePoints(ctx: PointsContextInput, body: unknown): Promise<{
|
|
21
14
|
deleted: number;
|