ydb-qdrant 5.2.1 → 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 +9 -3
- package/dist/config/env.js +16 -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 +12 -7
- package/dist/repositories/collectionsRepo.js +157 -39
- 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 +13 -1
- package/dist/services/CollectionService.shared.d.ts +1 -0
- package/dist/services/CollectionService.shared.js +3 -3
- package/dist/services/PointsService.d.ts +8 -10
- package/dist/services/PointsService.js +82 -5
- package/dist/types.d.ts +85 -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/tenant.d.ts +2 -2
- package/dist/utils/tenant.js +21 -6
- 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 +172 -54
- package/package.json +12 -7
- package/dist/repositories/collectionsRepo.shared.d.ts +0 -2
- package/dist/repositories/collectionsRepo.shared.js +0 -23
package/dist/ydb/schema.js
CHANGED
|
@@ -1,80 +1,198 @@
|
|
|
1
|
-
import { withSession
|
|
1
|
+
import { withSession } from "./client.js";
|
|
2
2
|
import { logger } from "../logging/logger.js";
|
|
3
|
-
import {
|
|
3
|
+
import { STARTUP_PROBE_SESSION_TIMEOUT_MS } from "../config/env.js";
|
|
4
4
|
export const GLOBAL_POINTS_TABLE = "qdrant_all_points";
|
|
5
5
|
// Shared YDB-related constants for repositories.
|
|
6
6
|
export { UPSERT_BATCH_SIZE } from "../config/env.js";
|
|
7
|
+
const SCHEMA_DDL_TIMEOUT_MS = 5000;
|
|
8
|
+
let metaTableReady = false;
|
|
9
|
+
let metaTableReadyInFlight = null;
|
|
7
10
|
let globalPointsTableReady = false;
|
|
11
|
+
function collectIssueMessages(err) {
|
|
12
|
+
const out = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
// Hard guardrails to guarantee termination even for pathological error graphs.
|
|
15
|
+
const MAX_DEPTH = 8;
|
|
16
|
+
const MAX_NODES = 500;
|
|
17
|
+
const queue = [{ v: err, depth: 0 }];
|
|
18
|
+
let visited = 0;
|
|
19
|
+
while (queue.length > 0 && visited < MAX_NODES) {
|
|
20
|
+
const next = queue.shift();
|
|
21
|
+
if (!next)
|
|
22
|
+
break;
|
|
23
|
+
const { v, depth } = next;
|
|
24
|
+
if (depth > MAX_DEPTH)
|
|
25
|
+
continue;
|
|
26
|
+
if (v === null || typeof v !== "object")
|
|
27
|
+
continue;
|
|
28
|
+
if (seen.has(v))
|
|
29
|
+
continue;
|
|
30
|
+
seen.add(v);
|
|
31
|
+
visited++;
|
|
32
|
+
const maybeMessage = v.message;
|
|
33
|
+
if (typeof maybeMessage === "string" && maybeMessage.length > 0) {
|
|
34
|
+
out.push(maybeMessage);
|
|
35
|
+
}
|
|
36
|
+
const maybeIssues = v.issues;
|
|
37
|
+
if (Array.isArray(maybeIssues)) {
|
|
38
|
+
for (const child of maybeIssues) {
|
|
39
|
+
queue.push({ v: child, depth: depth + 1 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function isAlreadyExistsError(err) {
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
+
if (/already exists/i.test(msg) || /path exists/i.test(msg)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// YDBError often carries the useful text in nested `issues`, while `message`
|
|
51
|
+
// is a generic wrapper like "Type annotation".
|
|
52
|
+
const issueMsgs = collectIssueMessages(err).join("\n");
|
|
53
|
+
return (/already exists/i.test(issueMsgs) ||
|
|
54
|
+
/path exists/i.test(issueMsgs) ||
|
|
55
|
+
/table name conflict/i.test(issueMsgs));
|
|
56
|
+
}
|
|
57
|
+
function isUnknownColumnError(err) {
|
|
58
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
59
|
+
const re = /unknown column|cannot resolve|member not found/i;
|
|
60
|
+
if (re.test(msg)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
// YDBError may carry the real message in nested `issues`.
|
|
64
|
+
const issueMsgs = collectIssueMessages(err).join("\n");
|
|
65
|
+
return re.test(issueMsgs);
|
|
66
|
+
}
|
|
8
67
|
function throwMigrationRequired(message) {
|
|
9
68
|
logger.error(message);
|
|
10
69
|
throw new Error(message);
|
|
11
70
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
71
|
+
async function ensureMetaTableOnce() {
|
|
72
|
+
await withSession(async (sql, signal) => {
|
|
73
|
+
try {
|
|
74
|
+
await sql `
|
|
75
|
+
CREATE TABLE qdr__collections (
|
|
76
|
+
collection Utf8,
|
|
77
|
+
table_name Utf8,
|
|
78
|
+
vector_dimension Uint32,
|
|
79
|
+
distance Utf8,
|
|
80
|
+
vector_type Utf8,
|
|
81
|
+
created_at Timestamp,
|
|
82
|
+
last_accessed_at Timestamp,
|
|
83
|
+
PRIMARY KEY (collection)
|
|
84
|
+
);
|
|
85
|
+
`
|
|
86
|
+
.idempotent(true)
|
|
87
|
+
.timeout(SCHEMA_DDL_TIMEOUT_MS)
|
|
88
|
+
.signal(signal);
|
|
89
|
+
logger.info("created metadata table qdr__collections");
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (!isAlreadyExistsError(err)) {
|
|
93
|
+
// YDB may return non-"already exists" errors for concurrent CREATE TABLE attempts
|
|
94
|
+
// or name resolution conflicts. Probe existence before failing startup.
|
|
95
|
+
try {
|
|
96
|
+
await sql `SELECT collection FROM qdr__collections LIMIT 0;`
|
|
97
|
+
.idempotent(true)
|
|
98
|
+
.timeout(STARTUP_PROBE_SESSION_TIMEOUT_MS)
|
|
99
|
+
.signal(signal);
|
|
100
|
+
logger.warn({ err }, "CREATE TABLE qdr__collections failed, but the table appears to exist; continuing");
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
19
105
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
106
|
+
}
|
|
107
|
+
// Fail fast if schema is old/mismatched: we do not auto-migrate tables.
|
|
108
|
+
try {
|
|
109
|
+
await sql `SELECT last_accessed_at FROM qdr__collections LIMIT 0;`
|
|
110
|
+
.idempotent(true)
|
|
111
|
+
.timeout(STARTUP_PROBE_SESSION_TIMEOUT_MS)
|
|
112
|
+
.signal(signal);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (!isUnknownColumnError(err)) {
|
|
116
|
+
throw err;
|
|
27
117
|
}
|
|
28
|
-
|
|
118
|
+
throwMigrationRequired("Metadata table qdr__collections is missing required column last_accessed_at; apply a manual migration (ALTER TABLE qdr__collections ADD COLUMN last_accessed_at Timestamp).");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
metaTableReady = true;
|
|
122
|
+
}
|
|
123
|
+
export async function ensureMetaTable() {
|
|
124
|
+
if (metaTableReady) {
|
|
125
|
+
return;
|
|
29
126
|
}
|
|
30
|
-
|
|
31
|
-
|
|
127
|
+
if (metaTableReadyInFlight) {
|
|
128
|
+
await metaTableReadyInFlight;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
metaTableReadyInFlight = ensureMetaTableOnce();
|
|
132
|
+
try {
|
|
133
|
+
await metaTableReadyInFlight;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
metaTableReadyInFlight = null;
|
|
32
137
|
}
|
|
33
138
|
}
|
|
34
139
|
export async function ensureGlobalPointsTable() {
|
|
35
140
|
if (globalPointsTableReady) {
|
|
36
141
|
return;
|
|
37
142
|
}
|
|
38
|
-
await withSession(async (
|
|
39
|
-
let tableDescription = null;
|
|
143
|
+
await withSession(async (sql, signal) => {
|
|
40
144
|
try {
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
145
|
+
await sql `
|
|
146
|
+
CREATE TABLE ${sql.identifier(GLOBAL_POINTS_TABLE)} (
|
|
147
|
+
uid Utf8,
|
|
148
|
+
point_id Utf8,
|
|
149
|
+
embedding String,
|
|
150
|
+
embedding_quantized String,
|
|
151
|
+
payload JsonDocument,
|
|
152
|
+
PRIMARY KEY (uid, point_id)
|
|
153
|
+
)
|
|
154
|
+
WITH (
|
|
155
|
+
AUTO_PARTITIONING_BY_LOAD = ENABLED,
|
|
156
|
+
AUTO_PARTITIONING_BY_SIZE = ENABLED,
|
|
157
|
+
AUTO_PARTITIONING_PARTITION_SIZE_MB = 100
|
|
158
|
+
);
|
|
159
|
+
`
|
|
160
|
+
.idempotent(true)
|
|
161
|
+
.timeout(SCHEMA_DDL_TIMEOUT_MS)
|
|
162
|
+
.signal(signal);
|
|
56
163
|
logger.info(`created global points table ${GLOBAL_POINTS_TABLE}`);
|
|
57
|
-
return;
|
|
58
164
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (!isAlreadyExistsError(err)) {
|
|
167
|
+
// YDB may return non-"already exists" errors for concurrent CREATE TABLE attempts
|
|
168
|
+
// or name resolution conflicts. Probe existence before failing startup.
|
|
169
|
+
try {
|
|
170
|
+
await sql `SELECT uid FROM ${sql.identifier(GLOBAL_POINTS_TABLE)} LIMIT 0;`
|
|
171
|
+
.idempotent(true)
|
|
172
|
+
.timeout(STARTUP_PROBE_SESSION_TIMEOUT_MS)
|
|
173
|
+
.signal(signal);
|
|
174
|
+
logger.warn({ err }, `CREATE TABLE ${GLOBAL_POINTS_TABLE} failed, but the table appears to exist; continuing`);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// If the table doesn't exist but CREATE TABLE failed for another reason,
|
|
178
|
+
// let the error surface; callers depend on the table being present.
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Fail fast if schema is old/mismatched: we do not auto-migrate tables.
|
|
184
|
+
try {
|
|
185
|
+
await sql `SELECT embedding_quantized FROM ${sql.identifier(GLOBAL_POINTS_TABLE)} LIMIT 0;`
|
|
186
|
+
.idempotent(true)
|
|
187
|
+
.timeout(STARTUP_PROBE_SESSION_TIMEOUT_MS)
|
|
188
|
+
.signal(signal);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (!isUnknownColumnError(err)) {
|
|
192
|
+
throw err;
|
|
65
193
|
}
|
|
66
|
-
|
|
67
|
-
ALTER TABLE ${GLOBAL_POINTS_TABLE}
|
|
68
|
-
ADD COLUMN embedding_quantized String;
|
|
69
|
-
`;
|
|
70
|
-
const rawSession = s;
|
|
71
|
-
await rawSession.api.executeSchemeQuery({
|
|
72
|
-
sessionId: rawSession.sessionId,
|
|
73
|
-
yqlText: alterDdl,
|
|
74
|
-
});
|
|
75
|
-
logger.info(`added embedding_quantized column to existing table ${GLOBAL_POINTS_TABLE}`);
|
|
194
|
+
throwMigrationRequired(`Global points table ${GLOBAL_POINTS_TABLE} is missing required column embedding_quantized; apply a manual migration (ALTER TABLE ${GLOBAL_POINTS_TABLE} ADD COLUMN embedding_quantized String). If your legacy schema used embedding_bit, rename it or recreate the table.`);
|
|
76
195
|
}
|
|
77
|
-
// Mark table ready after schema checks/migrations succeed.
|
|
78
196
|
globalPointsTableReady = true;
|
|
79
197
|
});
|
|
80
198
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ydb-qdrant",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.1",
|
|
4
4
|
"main": "dist/package/api.js",
|
|
5
5
|
"types": "dist/package/api.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -65,16 +65,18 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@bufbuild/protobuf": "^2.10.0",
|
|
67
67
|
"@grpc/grpc-js": "^1.14.0",
|
|
68
|
+
"@qdrant/js-client-rest": "^1.16.2",
|
|
68
69
|
"@yandex-cloud/nodejs-sdk": "^2.9.0",
|
|
69
|
-
"@ydbjs/api": "^6.0.
|
|
70
|
-
"@ydbjs/
|
|
71
|
-
"@ydbjs/
|
|
72
|
-
"@ydbjs/
|
|
70
|
+
"@ydbjs/api": "^6.0.5",
|
|
71
|
+
"@ydbjs/auth": "^6.0.5",
|
|
72
|
+
"@ydbjs/core": "^6.0.7",
|
|
73
|
+
"@ydbjs/query": "^6.0.7",
|
|
74
|
+
"@ydbjs/retry": "^6.0.5",
|
|
75
|
+
"@ydbjs/value": "^6.0.5",
|
|
73
76
|
"dotenv": "^17.2.3",
|
|
74
77
|
"express": "^5.1.0",
|
|
75
78
|
"nice-grpc": "^2.1.13",
|
|
76
79
|
"pino": "^10.1.0",
|
|
77
|
-
"ydb-sdk": "^5.11.1",
|
|
78
80
|
"zod": "^4.1.12"
|
|
79
81
|
},
|
|
80
82
|
"devDependencies": {
|
|
@@ -90,5 +92,8 @@
|
|
|
90
92
|
"typescript": "^5.9.3",
|
|
91
93
|
"typescript-eslint": "^8.47.0",
|
|
92
94
|
"vitest": "^4.0.12"
|
|
95
|
+
},
|
|
96
|
+
"engines": {
|
|
97
|
+
"node": ">=20.19.0"
|
|
93
98
|
}
|
|
94
|
-
}
|
|
99
|
+
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { TypedValues, withSession } from "../ydb/client.js";
|
|
2
|
-
export async function upsertCollectionMeta(metaKey, dim, distance, vectorType, tableName) {
|
|
3
|
-
const upsertMeta = `
|
|
4
|
-
DECLARE $collection AS Utf8;
|
|
5
|
-
DECLARE $table AS Utf8;
|
|
6
|
-
DECLARE $dim AS Uint32;
|
|
7
|
-
DECLARE $distance AS Utf8;
|
|
8
|
-
DECLARE $vtype AS Utf8;
|
|
9
|
-
DECLARE $created AS Timestamp;
|
|
10
|
-
UPSERT INTO qdr__collections (collection, table_name, vector_dimension, distance, vector_type, created_at)
|
|
11
|
-
VALUES ($collection, $table, $dim, $distance, $vtype, $created);
|
|
12
|
-
`;
|
|
13
|
-
await withSession(async (s) => {
|
|
14
|
-
await s.executeQuery(upsertMeta, {
|
|
15
|
-
$collection: TypedValues.utf8(metaKey),
|
|
16
|
-
$table: TypedValues.utf8(tableName),
|
|
17
|
-
$dim: TypedValues.uint32(dim),
|
|
18
|
-
$distance: TypedValues.utf8(distance),
|
|
19
|
-
$vtype: TypedValues.utf8(vectorType),
|
|
20
|
-
$created: TypedValues.timestamp(new Date()),
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
}
|