ydb-qdrant 8.1.0 → 9.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -18
- package/dist/SmokeTest.js +2 -2
- package/dist/compute/ComputePool.d.ts +5 -0
- package/dist/compute/ComputePool.js +64 -0
- package/dist/compute/ComputeWorker.d.ts +36 -0
- package/dist/compute/ComputeWorker.js +84 -0
- package/dist/config/env.d.ts +24 -7
- package/dist/config/env.js +65 -35
- package/dist/index.d.ts +2 -0
- package/dist/index.js +92 -2
- package/dist/logging/DeployLogFormatter.d.ts +2 -0
- package/dist/logging/DeployLogFormatter.js +131 -0
- package/dist/logging/logger.js +13 -1
- package/dist/logging/requestContext.d.ts +17 -0
- package/dist/logging/requestContext.js +43 -0
- package/dist/middleware/requestLogger.js +134 -6
- package/dist/middleware/upsertBodyPhase.d.ts +6 -0
- package/dist/middleware/upsertBodyPhase.js +184 -0
- package/dist/middleware/upsertRequestTimeout.d.ts +16 -0
- package/dist/middleware/upsertRequestTimeout.js +158 -0
- package/dist/package/api.d.ts +20 -12
- package/dist/package/api.js +57 -28
- package/dist/qdrant/QdrantRestTypes.d.ts +4 -0
- package/dist/qdrant/Requests.d.ts +97 -0
- package/dist/qdrant/Requests.js +72 -0
- package/dist/repositories/collectionsRepo.d.ts +18 -2
- package/dist/repositories/collectionsRepo.js +103 -7
- package/dist/repositories/collectionsRepo.one-table.d.ts +4 -3
- package/dist/repositories/collectionsRepo.one-table.js +99 -36
- package/dist/repositories/collectionsRepo.shared.d.ts +2 -2
- package/dist/repositories/collectionsRepo.shared.js +9 -4
- package/dist/repositories/pointsRepo.d.ts +6 -4
- package/dist/repositories/pointsRepo.js +8 -7
- package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -2
- package/dist/repositories/pointsRepo.one-table/Delete.js +157 -60
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +7 -5
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +44 -13
- package/dist/repositories/pointsRepo.one-table/Retrieve.d.ts +6 -0
- package/dist/repositories/pointsRepo.one-table/Retrieve.js +69 -0
- package/dist/repositories/pointsRepo.one-table/Search.d.ts +2 -3
- package/dist/repositories/pointsRepo.one-table/Search.js +102 -124
- package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
- package/dist/repositories/pointsRepo.one-table/Upsert.js +244 -48
- package/dist/repositories/pointsRepo.one-table.d.ts +1 -0
- package/dist/repositories/pointsRepo.one-table.js +1 -0
- package/dist/routes/collections.js +45 -36
- package/dist/routes/points.js +145 -56
- package/dist/server.js +42 -6
- package/dist/services/CollectionService.d.ts +7 -5
- package/dist/services/CollectionService.js +12 -9
- package/dist/services/CollectionService.one-table.js +1 -2
- package/dist/services/CollectionService.shared.d.ts +6 -5
- package/dist/services/CollectionService.shared.js +28 -12
- package/dist/services/PointsService.d.ts +8 -0
- package/dist/services/PointsService.js +132 -15
- package/dist/types.d.ts +4 -94
- package/dist/types.js +1 -54
- package/dist/utils/EnvParsers.d.ts +5 -0
- package/dist/utils/EnvParsers.js +30 -0
- package/dist/utils/PayloadSign.d.ts +4 -0
- package/dist/utils/PayloadSign.js +18 -0
- package/dist/utils/distance.d.ts +1 -12
- package/dist/utils/distance.js +0 -21
- package/dist/utils/pathPrefix.d.ts +3 -0
- package/dist/utils/pathPrefix.js +47 -0
- package/dist/utils/prefixExpansion.d.ts +1 -0
- package/dist/utils/prefixExpansion.js +11 -0
- package/dist/utils/qdrantResponse.d.ts +13 -0
- package/dist/utils/qdrantResponse.js +12 -0
- package/dist/utils/requestIdentity.d.ts +8 -0
- package/dist/utils/requestIdentity.js +52 -0
- package/dist/utils/retry.d.ts +2 -0
- package/dist/utils/retry.js +55 -11
- package/dist/utils/tenant.d.ts +12 -6
- package/dist/utils/tenant.js +41 -32
- package/dist/utils/vectorBinary.d.ts +0 -1
- package/dist/utils/vectorBinary.js +0 -98
- package/dist/utils/ydbErrors.d.ts +1 -0
- package/dist/utils/ydbErrors.js +14 -0
- package/dist/ydb/bootstrapMetaTable.js +14 -2
- package/dist/ydb/client.d.ts +10 -2
- package/dist/ydb/client.js +83 -24
- package/dist/ydb/helpers.d.ts +0 -1
- package/dist/ydb/helpers.js +1 -2
- package/dist/ydb/schema.d.ts +2 -0
- package/dist/ydb/schema.js +84 -7
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
# YDB Qdrant-compatible Service
|
|
18
18
|
|
|
19
|
-
Qdrant-compatible Node.js/TypeScript **service and npm library** that stores and searches vectors in YDB using a global one-table layout (`qdrant_all_points`) with exact KNN search
|
|
19
|
+
Qdrant-compatible Node.js/TypeScript **service and npm library** that stores and searches vectors in YDB using a global one-table layout (`qdrant_all_points`) with exact KNN search over `embedding`. Topics: ydb, vector-search, qdrant-compatible, nodejs, typescript, express, yql, ann, semantic-search, rag.
|
|
20
20
|
|
|
21
21
|
Modes:
|
|
22
22
|
- **HTTP server**: Qdrant-compatible REST API (`/collections`, `/points/*`) on top of YDB.
|
|
@@ -79,8 +79,8 @@ The server uses `getCredentialsFromEnv()` and supports these env vars (first mat
|
|
|
79
79
|
|
|
80
80
|
Also set endpoint and database:
|
|
81
81
|
```bash
|
|
82
|
-
export
|
|
83
|
-
export
|
|
82
|
+
export YDB_QDRANT_ENDPOINT=grpcs://ydb.serverless.yandexcloud.net:2135
|
|
83
|
+
export YDB_QDRANT_DATABASE=/ru-central1/<cloud>/<db>
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
Optional env:
|
|
@@ -88,23 +88,25 @@ Optional env:
|
|
|
88
88
|
# Server
|
|
89
89
|
export PORT=8080
|
|
90
90
|
export LOG_LEVEL=info
|
|
91
|
-
# One-table
|
|
92
|
-
export
|
|
93
|
-
export
|
|
91
|
+
# One-table tuning
|
|
92
|
+
export YDB_QDRANT_UPSERT_BATCH_SIZE=100
|
|
93
|
+
export YDB_QDRANT_SEARCH_TIMEOUT_MS=10000
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
## Use as a Node.js library (npm package)
|
|
97
97
|
|
|
98
98
|
The package entrypoint exports a programmatic API that mirrors the Qdrant HTTP semantics.
|
|
99
99
|
|
|
100
|
+
- `createYdbQdrantClient()` requires exactly one namespace/signing identifier:
|
|
101
|
+
pass either `apiKey` or `userUid`, but not both.
|
|
102
|
+
|
|
100
103
|
- Import and initialize a client (reuses the same YDB env vars as the server):
|
|
101
104
|
```ts
|
|
102
105
|
import { createYdbQdrantClient } from "ydb-qdrant";
|
|
103
106
|
|
|
104
107
|
async function main() {
|
|
105
|
-
// defaultTenant is optional; defaults to "default"
|
|
106
108
|
const client = await createYdbQdrantClient({
|
|
107
|
-
|
|
109
|
+
apiKey: "my-stable-namespace-key",
|
|
108
110
|
endpoint: "grpcs://lb.etn01g9tcilcon2mrt3h.ydb.mdb.yandexcloud.net:2135",
|
|
109
111
|
database: "/ru-central1/b1ge4v9r1l3h1q4njclp/etn01g9tcilcon2mrt3h",
|
|
110
112
|
});
|
|
@@ -133,20 +135,20 @@ The package entrypoint exports a programmatic API that mirrors the Qdrant HTTP s
|
|
|
133
135
|
}
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
-
|
|
138
|
+
- Explicit namespace usage with `userUid`:
|
|
137
139
|
```ts
|
|
138
140
|
const client = await createYdbQdrantClient({
|
|
141
|
+
userUid: "team_a",
|
|
139
142
|
endpoint: "grpcs://lb.etn01g9tcilcon2mrt3h.ydb.mdb.yandexcloud.net:2135",
|
|
140
143
|
database: "/ru-central1/b1ge4v9r1l3h1q4njclp/etn01g9tcilcon2mrt3h",
|
|
141
144
|
});
|
|
142
|
-
const tenantClient = client.forTenant("tenant-a");
|
|
143
145
|
|
|
144
|
-
await
|
|
146
|
+
await client.upsertPoints("sessions", {
|
|
145
147
|
points: [{ id: "s1", vector: [/* ... */] }],
|
|
146
148
|
});
|
|
147
149
|
```
|
|
148
150
|
|
|
149
|
-
The request/response shapes follow the same schemas as the HTTP API (`CreateCollectionReq`, `UpsertPointsReq`, `SearchReq`, `DeletePointsReq`), so code written against the REST API can usually be translated directly to the library calls.
|
|
151
|
+
The request/response shapes follow the same schemas as the HTTP API (`CreateCollectionReq`, `UpsertPointsReq`, `SearchReq`, `DeletePointsReq`, `RetrievePointsReq`), so code written against the REST API can usually be translated directly to the library calls. `createYdbQdrantClient` requires exactly one of `apiKey` or `userUid`.
|
|
150
152
|
|
|
151
153
|
### Example: in-process points search with a shared client
|
|
152
154
|
|
|
@@ -160,7 +162,7 @@ let clientPromise: ReturnType<typeof createYdbQdrantClient> | null = null;
|
|
|
160
162
|
async function getClient() {
|
|
161
163
|
if (!clientPromise) {
|
|
162
164
|
clientPromise = createYdbQdrantClient({
|
|
163
|
-
|
|
165
|
+
apiKey: 'myapp-shared-key',
|
|
164
166
|
endpoint: 'grpcs://lb.etn01g9tcilcon2mrt3h.ydb.mdb.yandexcloud.net:2135',
|
|
165
167
|
database: '/ru-central1/b1ge4v9r1l3h1q4njclp/etn01g9tcilcon2mrt3h',
|
|
166
168
|
});
|
|
@@ -193,9 +195,9 @@ For full tables of popular embedding models and their dimensions, see [docs/vect
|
|
|
193
195
|
|
|
194
196
|
**Option 1: Public Demo (No setup required)**
|
|
195
197
|
- Set Qdrant URL to `http://ydb-qdrant.tech:8080`
|
|
196
|
-
-
|
|
198
|
+
- `api-key` recommended for stable isolation
|
|
197
199
|
- Free to use for testing and development
|
|
198
|
-
-
|
|
200
|
+
- If `api-key` is omitted, anonymous isolation requires request client metadata such as IP or User-Agent
|
|
199
201
|
|
|
200
202
|
**Option 2: Self-hosted (Local)**
|
|
201
203
|
- Set Qdrant URL to `http://localhost:8080`
|
|
@@ -274,8 +276,8 @@ Basic example:
|
|
|
274
276
|
```bash
|
|
275
277
|
docker run -d --name ydb-qdrant \
|
|
276
278
|
-p 8080:8080 \
|
|
277
|
-
-e
|
|
278
|
-
-e
|
|
279
|
+
-e YDB_QDRANT_ENDPOINT=grpcs://ydb.serverless.yandexcloud.net:2135 \
|
|
280
|
+
-e YDB_QDRANT_DATABASE=/ru-central1/<cloud>/<db> \
|
|
279
281
|
-e YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS=/sa-key.json \
|
|
280
282
|
-v /abs/path/sa-key.json:/sa-key.json:ro \
|
|
281
283
|
ghcr.io/astandrik/ydb-qdrant:latest
|
|
@@ -330,7 +332,7 @@ curl -X POST http://localhost:8080/collections/mycol/points/delete \
|
|
|
330
332
|
|
|
331
333
|
## Architecture and Storage
|
|
332
334
|
|
|
333
|
-
For details on the YDB one-table storage layout, vector serialization
|
|
335
|
+
For details on the YDB one-table storage layout, vector serialization, request normalization, and Qdrant compatibility semantics, see [docs/architecture-and-storage.md](docs/architecture-and-storage.md).
|
|
334
336
|
|
|
335
337
|
## Evaluation, CI, and Release
|
|
336
338
|
|
package/dist/SmokeTest.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
2
|
import { createYdbQdrantClient } from "./package/api.js";
|
|
3
3
|
async function main() {
|
|
4
|
-
const tenant = process.env.SMOKE_TENANT ?? "smoke";
|
|
5
4
|
const collection = process.env.SMOKE_COLLECTION ?? "demo";
|
|
6
|
-
const
|
|
5
|
+
const apiKey = process.env.SMOKE_API_KEY ?? "smoke-demo-api-key";
|
|
6
|
+
const client = await createYdbQdrantClient({ apiKey });
|
|
7
7
|
await client.createCollection(collection, {
|
|
8
8
|
vectors: {
|
|
9
9
|
size: 4,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PrepareUpsertBatchTask, PreparedUpsertRow, VerifySearchRowsResult, VerifySearchRowsTask } from "./ComputeWorker.js";
|
|
2
|
+
export declare function isComputePoolEnabled(): boolean;
|
|
3
|
+
export declare function isComputePoolQueueAtLimitError(err: unknown): boolean;
|
|
4
|
+
export declare function runPrepareUpsertBatch(task: PrepareUpsertBatchTask): Promise<PreparedUpsertRow[]>;
|
|
5
|
+
export declare function runVerifySearchRows(task: VerifySearchRowsTask): Promise<VerifySearchRowsResult>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Piscina } from "piscina";
|
|
2
|
+
import { WORKERS_ENABLED, WORKERS_IDLE_TIMEOUT_MS, WORKERS_MAX_QUEUE, WORKERS_MAX_THREADS, WORKERS_MIN_THREADS, } from "../config/env.js";
|
|
3
|
+
import { logger } from "../logging/logger.js";
|
|
4
|
+
let pool;
|
|
5
|
+
function buildWorkerFilename() {
|
|
6
|
+
const isTsSource = import.meta.url.endsWith(".ts");
|
|
7
|
+
const workerUrl = new URL(isTsSource ? "./ComputeWorker.ts" : "./ComputeWorker.js", import.meta.url);
|
|
8
|
+
return workerUrl.href;
|
|
9
|
+
}
|
|
10
|
+
function buildExecArgv() {
|
|
11
|
+
const isTsSource = import.meta.url.endsWith(".ts");
|
|
12
|
+
if (!isTsSource) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
// Enable TypeScript execution in worker threads while running under tsx (dev).
|
|
16
|
+
// See: https://tsx.is/dev-api/
|
|
17
|
+
return ["--import", "tsx"];
|
|
18
|
+
}
|
|
19
|
+
function createPool() {
|
|
20
|
+
const filename = buildWorkerFilename();
|
|
21
|
+
const execArgv = buildExecArgv();
|
|
22
|
+
const p = new Piscina({
|
|
23
|
+
filename,
|
|
24
|
+
...(execArgv ? { execArgv } : {}),
|
|
25
|
+
minThreads: WORKERS_MIN_THREADS,
|
|
26
|
+
maxThreads: WORKERS_MAX_THREADS,
|
|
27
|
+
...(WORKERS_IDLE_TIMEOUT_MS >= 0
|
|
28
|
+
? { idleTimeout: WORKERS_IDLE_TIMEOUT_MS }
|
|
29
|
+
: {}),
|
|
30
|
+
...(WORKERS_MAX_QUEUE !== undefined ? { maxQueue: WORKERS_MAX_QUEUE } : {}),
|
|
31
|
+
});
|
|
32
|
+
p.on("error", (err) => {
|
|
33
|
+
logger.error({ err }, "Compute worker pool emitted an error");
|
|
34
|
+
});
|
|
35
|
+
return p;
|
|
36
|
+
}
|
|
37
|
+
function getPool() {
|
|
38
|
+
if (!pool) {
|
|
39
|
+
pool = createPool();
|
|
40
|
+
}
|
|
41
|
+
return pool;
|
|
42
|
+
}
|
|
43
|
+
export function isComputePoolEnabled() {
|
|
44
|
+
return WORKERS_ENABLED === true;
|
|
45
|
+
}
|
|
46
|
+
export function isComputePoolQueueAtLimitError(err) {
|
|
47
|
+
if (!(err instanceof Error)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Piscina uses these messages for backpressure / overload conditions.
|
|
51
|
+
// See piscina/src/errors.ts in the dependency for the canonical strings.
|
|
52
|
+
return (err.message === "Task queue is at limit" ||
|
|
53
|
+
err.message === "No task queue available and all Workers are busy");
|
|
54
|
+
}
|
|
55
|
+
export async function runPrepareUpsertBatch(task) {
|
|
56
|
+
return (await getPool().run(task, {
|
|
57
|
+
name: "prepareUpsertBatch",
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
export async function runVerifySearchRows(task) {
|
|
61
|
+
return (await getPool().run(task, {
|
|
62
|
+
name: "verifySearchRows",
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { UpsertPoint } from "../qdrant/Requests.js";
|
|
2
|
+
import type { YdbQdrantScoredPoint } from "../qdrant/QdrantRestTypes.js";
|
|
3
|
+
export type PrepareUpsertBatchTask = {
|
|
4
|
+
collection: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
batch: Array<Pick<UpsertPoint, "id" | "vector" | "payload">>;
|
|
7
|
+
};
|
|
8
|
+
export type PreparedUpsertRow = {
|
|
9
|
+
collection: string;
|
|
10
|
+
point_id: string;
|
|
11
|
+
embedding: Buffer;
|
|
12
|
+
payload: string;
|
|
13
|
+
payload_sign: string;
|
|
14
|
+
path_prefix: string | null;
|
|
15
|
+
};
|
|
16
|
+
export declare function prepareUpsertBatch(task: PrepareUpsertBatchTask): PreparedUpsertRow[];
|
|
17
|
+
export type VerifySearchRowsTask = {
|
|
18
|
+
collection: string;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
withPayload: boolean | undefined;
|
|
21
|
+
rows: Array<{
|
|
22
|
+
pointId: string;
|
|
23
|
+
payloadText: string | undefined;
|
|
24
|
+
payloadSign: string | undefined;
|
|
25
|
+
scoreText: string | undefined;
|
|
26
|
+
scoreFloat: number | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
export type VerifySearchRowsResult = {
|
|
30
|
+
points: YdbQdrantScoredPoint[];
|
|
31
|
+
dropped: Array<{
|
|
32
|
+
pointId: string;
|
|
33
|
+
reason: "missing_payload_or_signature" | "signature_mismatch";
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
export declare function verifySearchRows(task: VerifySearchRowsTask): VerifySearchRowsResult;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { buildVectorBinaryParams } from "../ydb/helpers.js";
|
|
2
|
+
import { Piscina, move, transferableSymbol, valueSymbol } from "piscina";
|
|
3
|
+
import { computePayloadSign } from "../utils/PayloadSign.js";
|
|
4
|
+
import { extractPathPrefix } from "../utils/pathPrefix.js";
|
|
5
|
+
export function prepareUpsertBatch(task) {
|
|
6
|
+
const apiKey = task.apiKey.trim();
|
|
7
|
+
if (apiKey.length === 0) {
|
|
8
|
+
throw new Error("prepareUpsertBatch: apiKey is empty");
|
|
9
|
+
}
|
|
10
|
+
const rows = task.batch.map((p) => {
|
|
11
|
+
const binaries = buildVectorBinaryParams(p.vector);
|
|
12
|
+
const payloadObj = p.payload ?? {};
|
|
13
|
+
return {
|
|
14
|
+
collection: task.collection,
|
|
15
|
+
point_id: String(p.id),
|
|
16
|
+
embedding: binaries.float,
|
|
17
|
+
payload: JSON.stringify(payloadObj),
|
|
18
|
+
payload_sign: computePayloadSign({ apiKey, payload: payloadObj }),
|
|
19
|
+
path_prefix: extractPathPrefix(payloadObj),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
if (!Piscina.isWorkerThread) {
|
|
23
|
+
return rows;
|
|
24
|
+
}
|
|
25
|
+
// Transfer underlying ArrayBuffers to the main thread to avoid cloning large binary payloads.
|
|
26
|
+
// See Node worker_threads transferList semantics and Piscina's Transferable interface:
|
|
27
|
+
// https://nodejs.org/api/worker_threads.html
|
|
28
|
+
// https://www.npmjs.com/package/piscina
|
|
29
|
+
const transferable = {
|
|
30
|
+
get [transferableSymbol]() {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const r of rows) {
|
|
33
|
+
out.push(r.embedding.buffer);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
},
|
|
37
|
+
get [valueSymbol]() {
|
|
38
|
+
return rows;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return move(transferable);
|
|
42
|
+
}
|
|
43
|
+
export function verifySearchRows(task) {
|
|
44
|
+
const apiKey = task.apiKey.trim();
|
|
45
|
+
if (apiKey.length === 0) {
|
|
46
|
+
throw new Error("verifySearchRows: apiKey is empty");
|
|
47
|
+
}
|
|
48
|
+
const shouldReturnPayload = task.withPayload === true;
|
|
49
|
+
const points = [];
|
|
50
|
+
const dropped = [];
|
|
51
|
+
for (const row of task.rows) {
|
|
52
|
+
let payload;
|
|
53
|
+
if (row.payloadText) {
|
|
54
|
+
try {
|
|
55
|
+
payload = JSON.parse(row.payloadText);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
payload = undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!payload || typeof row.payloadSign !== "string" || row.payloadSign === "") {
|
|
62
|
+
dropped.push({
|
|
63
|
+
pointId: row.pointId,
|
|
64
|
+
reason: "missing_payload_or_signature",
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const expected = computePayloadSign({ apiKey, payload });
|
|
69
|
+
if (expected !== row.payloadSign) {
|
|
70
|
+
dropped.push({
|
|
71
|
+
pointId: row.pointId,
|
|
72
|
+
reason: "signature_mismatch",
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const score = Number(row.scoreFloat ?? row.scoreText);
|
|
77
|
+
points.push({
|
|
78
|
+
id: row.pointId,
|
|
79
|
+
score,
|
|
80
|
+
...(shouldReturnPayload ? { payload } : {}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return { points, dropped };
|
|
84
|
+
}
|
package/dist/config/env.d.ts
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
|
+
export declare const YDB_QDRANT_ENDPOINT: string;
|
|
3
|
+
export declare const YDB_QDRANT_DATABASE: string;
|
|
4
|
+
export declare const LEGACY_YDB_ENDPOINT: string;
|
|
5
|
+
export declare const LEGACY_YDB_DATABASE: string;
|
|
2
6
|
export declare const YDB_ENDPOINT: string;
|
|
3
7
|
export declare const YDB_DATABASE: string;
|
|
8
|
+
export declare function resolveYdbConnectionConfig(options?: {
|
|
9
|
+
endpoint?: string;
|
|
10
|
+
database?: string;
|
|
11
|
+
connectionString?: string;
|
|
12
|
+
}): {
|
|
13
|
+
connectionString: string;
|
|
14
|
+
} | {
|
|
15
|
+
endpoint: string;
|
|
16
|
+
database: string;
|
|
17
|
+
};
|
|
4
18
|
export declare const PORT: number;
|
|
5
19
|
export declare const LOG_LEVEL: string;
|
|
6
|
-
export declare enum SearchMode {
|
|
7
|
-
Exact = "exact",
|
|
8
|
-
Approximate = "approximate"
|
|
9
|
-
}
|
|
10
|
-
export declare function resolveSearchMode(raw: string | undefined): SearchMode;
|
|
11
|
-
export declare const SEARCH_MODE: SearchMode;
|
|
12
|
-
export declare const OVERFETCH_MULTIPLIER: number;
|
|
13
20
|
export declare const UPSERT_BATCH_SIZE: number;
|
|
21
|
+
export declare const DELETE_FILTER_SELECT_BATCH_SIZE: number;
|
|
14
22
|
export declare const SESSION_POOL_MIN_SIZE: number;
|
|
15
23
|
export declare const SESSION_POOL_MAX_SIZE: number;
|
|
16
24
|
export declare const SESSION_KEEPALIVE_PERIOD_MS: number;
|
|
25
|
+
export declare const YDB_SESSION_RETRY_MAX_RETRIES: number;
|
|
17
26
|
export declare const STARTUP_PROBE_SESSION_TIMEOUT_MS: number;
|
|
18
27
|
export declare const UPSERT_OPERATION_TIMEOUT_MS: number;
|
|
28
|
+
export declare const UPSERT_BODY_TIMEOUT_MS: number;
|
|
29
|
+
export declare const UPSERT_HTTP_TIMEOUT_MS: number;
|
|
19
30
|
export declare const SEARCH_OPERATION_TIMEOUT_MS: number;
|
|
31
|
+
export declare const TABLE_SESSION_TIMEOUT_MS: number;
|
|
20
32
|
export declare const LAST_ACCESS_MIN_WRITE_INTERVAL_MS: number;
|
|
33
|
+
export declare const WORKERS_ENABLED: boolean;
|
|
34
|
+
export declare const WORKERS_MAX_THREADS: number;
|
|
35
|
+
export declare const WORKERS_MIN_THREADS: number;
|
|
36
|
+
export declare const WORKERS_IDLE_TIMEOUT_MS: number;
|
|
37
|
+
export declare const WORKERS_MAX_QUEUE: number | "auto" | undefined;
|
package/dist/config/env.js
CHANGED
|
@@ -1,47 +1,54 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
|
-
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
import { parseBooleanEnv, parseIntegerEnv } from "../utils/EnvParsers.js";
|
|
3
|
+
export const YDB_QDRANT_ENDPOINT = process.env.YDB_QDRANT_ENDPOINT?.trim() ?? "";
|
|
4
|
+
export const YDB_QDRANT_DATABASE = process.env.YDB_QDRANT_DATABASE?.trim() ?? "";
|
|
5
|
+
export const LEGACY_YDB_ENDPOINT = process.env.YDB_ENDPOINT?.trim() ?? "";
|
|
6
|
+
export const LEGACY_YDB_DATABASE = process.env.YDB_DATABASE?.trim() ?? "";
|
|
7
|
+
export const YDB_ENDPOINT = YDB_QDRANT_ENDPOINT;
|
|
8
|
+
export const YDB_DATABASE = YDB_QDRANT_DATABASE;
|
|
9
|
+
const LEGACY_YDB_ENDPOINT_ERROR = [
|
|
10
|
+
"Legacy env var YDB_ENDPOINT is not supported by ydb-qdrant.",
|
|
11
|
+
"Reason: ydb-sdk uses process.env.YDB_ENDPOINT as a dev-only override that forces all discovered endpoints to that host, breaking discovery and potentially causing session hangs/timeouts.",
|
|
12
|
+
"Fix: set YDB_QDRANT_ENDPOINT instead.",
|
|
13
|
+
].join(" ");
|
|
14
|
+
const MISSING_YDB_CONNECTION_SETTINGS_ERROR = [
|
|
15
|
+
"Missing YDB connection settings.",
|
|
16
|
+
"Set YDB_QDRANT_ENDPOINT (grpc(s)://host:port) and YDB_QDRANT_DATABASE (/path/to/db).",
|
|
17
|
+
"Legacy YDB_ENDPOINT/YDB_DATABASE are not supported as application config.",
|
|
18
|
+
].join(" ");
|
|
19
|
+
export function resolveYdbConnectionConfig(options) {
|
|
20
|
+
if (LEGACY_YDB_ENDPOINT) {
|
|
21
|
+
throw new Error(LEGACY_YDB_ENDPOINT_ERROR);
|
|
13
22
|
}
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
23
|
+
const connectionString = options?.connectionString?.trim();
|
|
24
|
+
if (connectionString) {
|
|
25
|
+
return { connectionString };
|
|
17
26
|
}
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
const endpoint = options?.endpoint?.trim() || YDB_QDRANT_ENDPOINT;
|
|
28
|
+
const database = options?.database?.trim() || YDB_QDRANT_DATABASE;
|
|
29
|
+
if (!endpoint || !database) {
|
|
30
|
+
throw new Error(MISSING_YDB_CONNECTION_SETTINGS_ERROR);
|
|
20
31
|
}
|
|
21
|
-
return
|
|
32
|
+
return { endpoint, database };
|
|
22
33
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
})(SearchMode || (SearchMode = {}));
|
|
28
|
-
export function resolveSearchMode(raw) {
|
|
29
|
-
const normalized = raw?.trim().toLowerCase();
|
|
30
|
-
if (normalized === SearchMode.Exact) {
|
|
31
|
-
return SearchMode.Exact;
|
|
34
|
+
function parseWorkersMaxQueue(value) {
|
|
35
|
+
const raw = value?.trim().toLowerCase();
|
|
36
|
+
if (!raw) {
|
|
37
|
+
return "auto";
|
|
32
38
|
}
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
39
|
+
if (raw === "auto") {
|
|
40
|
+
return "auto";
|
|
35
41
|
}
|
|
36
|
-
|
|
37
|
-
return
|
|
42
|
+
const n = parseIntegerEnv(raw, 0, { min: 1 });
|
|
43
|
+
return n > 0 ? n : undefined;
|
|
38
44
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export const
|
|
45
|
+
export const PORT = parseIntegerEnv(process.env.PORT, 8080, {
|
|
46
|
+
min: 1,
|
|
47
|
+
max: 65535,
|
|
48
|
+
});
|
|
49
|
+
export const LOG_LEVEL = process.env.LOG_LEVEL ?? "info";
|
|
44
50
|
export const UPSERT_BATCH_SIZE = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_BATCH_SIZE, 100, { min: 1 });
|
|
51
|
+
export const DELETE_FILTER_SELECT_BATCH_SIZE = parseIntegerEnv(process.env.YDB_QDRANT_DELETE_FILTER_SELECT_BATCH_SIZE, 10000, { min: 1 });
|
|
45
52
|
// Session pool configuration
|
|
46
53
|
const RAW_SESSION_POOL_MIN_SIZE = parseIntegerEnv(process.env.YDB_SESSION_POOL_MIN_SIZE, 5, { min: 1, max: 500 });
|
|
47
54
|
const RAW_SESSION_POOL_MAX_SIZE = parseIntegerEnv(process.env.YDB_SESSION_POOL_MAX_SIZE, 100, { min: 1, max: 500 });
|
|
@@ -51,7 +58,30 @@ const NORMALIZED_SESSION_POOL_MIN_SIZE = RAW_SESSION_POOL_MIN_SIZE > RAW_SESSION
|
|
|
51
58
|
export const SESSION_POOL_MIN_SIZE = NORMALIZED_SESSION_POOL_MIN_SIZE;
|
|
52
59
|
export const SESSION_POOL_MAX_SIZE = RAW_SESSION_POOL_MAX_SIZE;
|
|
53
60
|
export const SESSION_KEEPALIVE_PERIOD_MS = parseIntegerEnv(process.env.YDB_SESSION_KEEPALIVE_PERIOD_MS, 5000, { min: 1000, max: 60000 });
|
|
61
|
+
export const YDB_SESSION_RETRY_MAX_RETRIES = parseIntegerEnv(process.env.YDB_SESSION_RETRY_MAX_RETRIES, 3, { min: 0, max: 50 });
|
|
54
62
|
export const STARTUP_PROBE_SESSION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_STARTUP_PROBE_SESSION_TIMEOUT_MS, 5000, { min: 1000 });
|
|
55
63
|
export const UPSERT_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_TIMEOUT_MS, 5000, { min: 1000 });
|
|
64
|
+
const RAW_UPSERT_BODY_TIMEOUT_MS = process.env.YDB_QDRANT_UPSERT_BODY_TIMEOUT_MS?.trim();
|
|
65
|
+
export const UPSERT_BODY_TIMEOUT_MS = RAW_UPSERT_BODY_TIMEOUT_MS === "0"
|
|
66
|
+
? 0
|
|
67
|
+
: parseIntegerEnv(RAW_UPSERT_BODY_TIMEOUT_MS, 60000, {
|
|
68
|
+
min: 1000,
|
|
69
|
+
});
|
|
70
|
+
const RAW_UPSERT_HTTP_TIMEOUT_MS = process.env.YDB_QDRANT_UPSERT_HTTP_TIMEOUT_MS?.trim();
|
|
71
|
+
export const UPSERT_HTTP_TIMEOUT_MS = RAW_UPSERT_HTTP_TIMEOUT_MS === "0"
|
|
72
|
+
? 0
|
|
73
|
+
: parseIntegerEnv(RAW_UPSERT_HTTP_TIMEOUT_MS, 10000, {
|
|
74
|
+
min: 1000,
|
|
75
|
+
});
|
|
56
76
|
export const SEARCH_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_SEARCH_TIMEOUT_MS, 10000, { min: 1000 });
|
|
77
|
+
export const TABLE_SESSION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_SESSION_TIMEOUT_MS, 30000, { min: 5000, max: 120000 });
|
|
57
78
|
export const LAST_ACCESS_MIN_WRITE_INTERVAL_MS = parseIntegerEnv(process.env.YDB_QDRANT_LAST_ACCESS_MIN_WRITE_INTERVAL_MS, 60000, { min: 1000 });
|
|
79
|
+
export const WORKERS_ENABLED = parseBooleanEnv(process.env.YDB_QDRANT_WORKERS_ENABLED, false);
|
|
80
|
+
const RAW_WORKERS_MIN_THREADS = parseIntegerEnv(process.env.YDB_QDRANT_WORKERS_MIN_THREADS, 0, { min: 0, max: 512 });
|
|
81
|
+
const RAW_WORKERS_MAX_THREADS = parseIntegerEnv(process.env.YDB_QDRANT_WORKERS_MAX_THREADS, 1, { min: 1, max: 512 });
|
|
82
|
+
export const WORKERS_MAX_THREADS = RAW_WORKERS_MAX_THREADS;
|
|
83
|
+
export const WORKERS_MIN_THREADS = RAW_WORKERS_MIN_THREADS > RAW_WORKERS_MAX_THREADS
|
|
84
|
+
? RAW_WORKERS_MAX_THREADS
|
|
85
|
+
: RAW_WORKERS_MIN_THREADS;
|
|
86
|
+
export const WORKERS_IDLE_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_WORKERS_IDLE_TIMEOUT_MS, 10000, { min: 0, max: 600000 });
|
|
87
|
+
export const WORKERS_MAX_QUEUE = parseWorkersMaxQueue(process.env.YDB_QDRANT_WORKERS_MAX_QUEUE);
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,15 +1,95 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
2
|
import { buildServer } from "./server.js";
|
|
3
|
-
import { PORT } from "./config/env.js";
|
|
3
|
+
import { PORT, UPSERT_BODY_TIMEOUT_MS, UPSERT_HTTP_TIMEOUT_MS, } from "./config/env.js";
|
|
4
4
|
import { logger } from "./logging/logger.js";
|
|
5
5
|
import { readyOrThrow, isCompilationTimeoutError } from "./ydb/client.js";
|
|
6
|
-
import { ensureMetaTable, ensureGlobalPointsTable } from "./ydb/schema.js";
|
|
6
|
+
import { GLOBAL_POINTS_TABLE, ensureMetaTable, ensureGlobalPointsTable, POINTS_BY_FILE_LOOKUP_TABLE, ensurePointsByFileTable, } from "./ydb/schema.js";
|
|
7
7
|
import { verifyCollectionsQueryCompilationForStartup } from "./repositories/collectionsRepo.js";
|
|
8
|
+
import { scheduleExit } from "./utils/exit.js";
|
|
9
|
+
function describeUnknownError(err) {
|
|
10
|
+
if (err instanceof Error) {
|
|
11
|
+
return err.message;
|
|
12
|
+
}
|
|
13
|
+
if (typeof err === "string") {
|
|
14
|
+
return err;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(err);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return String(err);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isFatalStartupSchemaError(err) {
|
|
24
|
+
if (!(err instanceof Error)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return (err.message.includes(`Global points table ${GLOBAL_POINTS_TABLE}`) ||
|
|
28
|
+
err.message.includes(`Points-by-file lookup table ${POINTS_BY_FILE_LOOKUP_TABLE}`));
|
|
29
|
+
}
|
|
30
|
+
let originalStderrWrite;
|
|
31
|
+
let stderrAsErrorLoggerInstalled = false;
|
|
32
|
+
function writeToOriginalStderr(chunk, encoding, cb) {
|
|
33
|
+
if (!originalStderrWrite) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (typeof encoding === "function") {
|
|
37
|
+
return originalStderrWrite(chunk, encoding);
|
|
38
|
+
}
|
|
39
|
+
if (encoding !== undefined) {
|
|
40
|
+
return originalStderrWrite(chunk, encoding, cb);
|
|
41
|
+
}
|
|
42
|
+
return originalStderrWrite(chunk);
|
|
43
|
+
}
|
|
44
|
+
export function installStderrAsErrorLogger() {
|
|
45
|
+
if (stderrAsErrorLoggerInstalled) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Some infra log collectors treat raw stderr lines as DEBUG by default.
|
|
49
|
+
// Mirror stderr writes into structured error logs while preserving the
|
|
50
|
+
// original stream so native/runtime diagnostics still reach stderr.
|
|
51
|
+
originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
52
|
+
process.stderr.write = ((chunk, encoding, cb) => {
|
|
53
|
+
const enc = typeof encoding === "string" ? encoding : undefined;
|
|
54
|
+
const text = typeof chunk === "string"
|
|
55
|
+
? chunk
|
|
56
|
+
: Buffer.from(chunk).toString(enc ?? "utf8");
|
|
57
|
+
for (const line of text.split(/\r?\n/)) {
|
|
58
|
+
if (line.length > 0) {
|
|
59
|
+
logger.error({ stream: "stderr" }, line);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return writeToOriginalStderr(chunk, encoding, cb);
|
|
63
|
+
});
|
|
64
|
+
stderrAsErrorLoggerInstalled = true;
|
|
65
|
+
}
|
|
66
|
+
export function __restoreStderrWriteForTests() {
|
|
67
|
+
if (!stderrAsErrorLoggerInstalled || !originalStderrWrite) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
process.stderr.write = originalStderrWrite;
|
|
71
|
+
stderrAsErrorLoggerInstalled = false;
|
|
72
|
+
}
|
|
73
|
+
installStderrAsErrorLogger();
|
|
74
|
+
process.on("uncaughtException", (err) => {
|
|
75
|
+
logger.fatal({ err }, "Uncaught exception");
|
|
76
|
+
scheduleExit(1);
|
|
77
|
+
});
|
|
78
|
+
process.on("unhandledRejection", (reason) => {
|
|
79
|
+
if (reason instanceof Error) {
|
|
80
|
+
logger.fatal({ err: reason }, "Unhandled rejection");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
logger.fatal({ err: new Error(describeUnknownError(reason)), reason }, "Unhandled rejection");
|
|
84
|
+
}
|
|
85
|
+
scheduleExit(1);
|
|
86
|
+
});
|
|
8
87
|
async function start() {
|
|
9
88
|
try {
|
|
10
89
|
await readyOrThrow();
|
|
11
90
|
await ensureMetaTable();
|
|
12
91
|
await ensureGlobalPointsTable();
|
|
92
|
+
await ensurePointsByFileTable();
|
|
13
93
|
await verifyCollectionsQueryCompilationForStartup();
|
|
14
94
|
logger.info("YDB compilation startup probe for qdr__collections completed successfully");
|
|
15
95
|
}
|
|
@@ -17,9 +97,19 @@ async function start() {
|
|
|
17
97
|
if (isCompilationTimeoutError(err)) {
|
|
18
98
|
logger.error({ err }, "Fatal YDB compilation timeout during startup probe; exiting so supervisor can restart the process");
|
|
19
99
|
process.exit(1);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (isFatalStartupSchemaError(err)) {
|
|
103
|
+
logger.error({ err }, "Fatal YDB schema/startup check failure; exiting until required migrations are applied");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
return;
|
|
20
106
|
}
|
|
21
107
|
logger.error({ err }, "YDB not ready; startup continues, requests may fail until configured.");
|
|
22
108
|
}
|
|
109
|
+
logger.info({
|
|
110
|
+
upsertBodyTimeoutMs: UPSERT_BODY_TIMEOUT_MS,
|
|
111
|
+
upsertProcessingTimeoutMs: UPSERT_HTTP_TIMEOUT_MS,
|
|
112
|
+
}, "Resolved upsert timeout budgets");
|
|
23
113
|
const app = buildServer();
|
|
24
114
|
app.listen(PORT, () => {
|
|
25
115
|
logger.info({ port: PORT }, "ydb-qdrant proxy listening");
|