ydb-qdrant 8.1.1 → 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.
Files changed (87) hide show
  1. package/README.md +20 -18
  2. package/dist/SmokeTest.js +2 -2
  3. package/dist/compute/ComputePool.d.ts +5 -0
  4. package/dist/compute/ComputePool.js +64 -0
  5. package/dist/compute/ComputeWorker.d.ts +36 -0
  6. package/dist/compute/ComputeWorker.js +84 -0
  7. package/dist/config/env.d.ts +24 -7
  8. package/dist/config/env.js +65 -35
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +92 -2
  11. package/dist/logging/DeployLogFormatter.d.ts +2 -0
  12. package/dist/logging/DeployLogFormatter.js +131 -0
  13. package/dist/logging/logger.js +13 -1
  14. package/dist/logging/requestContext.d.ts +17 -0
  15. package/dist/logging/requestContext.js +43 -0
  16. package/dist/middleware/requestLogger.js +134 -6
  17. package/dist/middleware/upsertBodyPhase.d.ts +6 -0
  18. package/dist/middleware/upsertBodyPhase.js +184 -0
  19. package/dist/middleware/upsertRequestTimeout.d.ts +16 -0
  20. package/dist/middleware/upsertRequestTimeout.js +158 -0
  21. package/dist/package/api.d.ts +20 -12
  22. package/dist/package/api.js +57 -28
  23. package/dist/qdrant/QdrantRestTypes.d.ts +4 -0
  24. package/dist/qdrant/Requests.d.ts +97 -0
  25. package/dist/qdrant/Requests.js +72 -0
  26. package/dist/repositories/collectionsRepo.d.ts +18 -2
  27. package/dist/repositories/collectionsRepo.js +103 -7
  28. package/dist/repositories/collectionsRepo.one-table.d.ts +4 -3
  29. package/dist/repositories/collectionsRepo.one-table.js +99 -36
  30. package/dist/repositories/collectionsRepo.shared.d.ts +2 -2
  31. package/dist/repositories/collectionsRepo.shared.js +9 -4
  32. package/dist/repositories/pointsRepo.d.ts +6 -4
  33. package/dist/repositories/pointsRepo.js +8 -7
  34. package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -2
  35. package/dist/repositories/pointsRepo.one-table/Delete.js +157 -60
  36. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +7 -5
  37. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +44 -13
  38. package/dist/repositories/pointsRepo.one-table/Retrieve.d.ts +6 -0
  39. package/dist/repositories/pointsRepo.one-table/Retrieve.js +69 -0
  40. package/dist/repositories/pointsRepo.one-table/Search.d.ts +2 -3
  41. package/dist/repositories/pointsRepo.one-table/Search.js +102 -124
  42. package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
  43. package/dist/repositories/pointsRepo.one-table/Upsert.js +244 -48
  44. package/dist/repositories/pointsRepo.one-table.d.ts +1 -0
  45. package/dist/repositories/pointsRepo.one-table.js +1 -0
  46. package/dist/routes/collections.js +45 -36
  47. package/dist/routes/points.js +144 -59
  48. package/dist/server.js +42 -6
  49. package/dist/services/CollectionService.d.ts +7 -5
  50. package/dist/services/CollectionService.js +12 -9
  51. package/dist/services/CollectionService.one-table.js +1 -2
  52. package/dist/services/CollectionService.shared.d.ts +6 -5
  53. package/dist/services/CollectionService.shared.js +28 -12
  54. package/dist/services/PointsService.d.ts +8 -0
  55. package/dist/services/PointsService.js +132 -15
  56. package/dist/types.d.ts +4 -94
  57. package/dist/types.js +1 -54
  58. package/dist/utils/EnvParsers.d.ts +5 -0
  59. package/dist/utils/EnvParsers.js +30 -0
  60. package/dist/utils/PayloadSign.d.ts +4 -0
  61. package/dist/utils/PayloadSign.js +18 -0
  62. package/dist/utils/distance.d.ts +1 -12
  63. package/dist/utils/distance.js +0 -21
  64. package/dist/utils/pathPrefix.d.ts +3 -0
  65. package/dist/utils/pathPrefix.js +47 -0
  66. package/dist/utils/prefixExpansion.d.ts +1 -0
  67. package/dist/utils/prefixExpansion.js +11 -0
  68. package/dist/utils/qdrantResponse.d.ts +13 -0
  69. package/dist/utils/qdrantResponse.js +12 -0
  70. package/dist/utils/requestIdentity.d.ts +8 -0
  71. package/dist/utils/requestIdentity.js +52 -0
  72. package/dist/utils/retry.d.ts +2 -0
  73. package/dist/utils/retry.js +55 -11
  74. package/dist/utils/tenant.d.ts +12 -6
  75. package/dist/utils/tenant.js +41 -32
  76. package/dist/utils/vectorBinary.d.ts +0 -1
  77. package/dist/utils/vectorBinary.js +0 -98
  78. package/dist/utils/ydbErrors.d.ts +1 -0
  79. package/dist/utils/ydbErrors.js +14 -0
  80. package/dist/ydb/bootstrapMetaTable.js +14 -2
  81. package/dist/ydb/client.d.ts +10 -2
  82. package/dist/ydb/client.js +83 -24
  83. package/dist/ydb/helpers.d.ts +0 -1
  84. package/dist/ydb/helpers.js +1 -2
  85. package/dist/ydb/schema.d.ts +2 -0
  86. package/dist/ydb/schema.js +84 -7
  87. 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 (single-phase over `embedding`) by default and an optional approximate mode (two‑phase bit-quantized over `embedding_quantized` + `embedding`). Topics: ydb, vector-search, qdrant-compatible, nodejs, typescript, express, yql, ann, semantic-search, rag.
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 YDB_ENDPOINT=grpcs://ydb.serverless.yandexcloud.net:2135
83
- export YDB_DATABASE=/ru-central1/<cloud>/<db>
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 search tuning (default is 'exact' when unset)
92
- export YDB_QDRANT_SEARCH_MODE=approximate # approximate or exact (default: exact)
93
- export YDB_QDRANT_OVERFETCH_MULTIPLIER=10 # candidate multiplier in approximate mode
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
- defaultTenant: "myapp",
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
- - Multi-tenant usage with `forTenant`:
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 tenantClient.upsertPoints("sessions", {
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
- defaultTenant: 'myapp',
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
- - No API key needed
198
+ - `api-key` recommended for stable isolation
197
199
  - Free to use for testing and development
198
- - Shared instance - use `X-Tenant-Id` header for isolation
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 YDB_ENDPOINT=grpcs://ydb.serverless.yandexcloud.net:2135 \
278
- -e YDB_DATABASE=/ru-central1/<cloud>/<db> \
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 (full-precision and bit‑quantized), approximate vs exact search modes, request normalization, and Qdrant compatibility semantics, see [docs/architecture-and-storage.md](docs/architecture-and-storage.md).
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 client = await createYdbQdrantClient({ defaultTenant: tenant });
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
+ }
@@ -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;
@@ -1,47 +1,54 @@
1
1
  import "dotenv/config";
2
- export const YDB_ENDPOINT = process.env.YDB_ENDPOINT ?? "";
3
- export const YDB_DATABASE = process.env.YDB_DATABASE ?? "";
4
- export const PORT = process.env.PORT ? Number(process.env.PORT) : 8080;
5
- export const LOG_LEVEL = process.env.LOG_LEVEL ?? "info";
6
- function parseIntegerEnv(value, defaultValue, opts) {
7
- if (value === undefined) {
8
- return defaultValue;
9
- }
10
- const parsed = Number.parseInt(value.trim(), 10);
11
- if (!Number.isFinite(parsed)) {
12
- return defaultValue;
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
- let result = parsed;
15
- if (opts?.min !== undefined && result < opts.min) {
16
- result = opts.min;
23
+ const connectionString = options?.connectionString?.trim();
24
+ if (connectionString) {
25
+ return { connectionString };
17
26
  }
18
- if (opts?.max !== undefined && result > opts.max) {
19
- result = opts.max;
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 result;
32
+ return { endpoint, database };
22
33
  }
23
- export var SearchMode;
24
- (function (SearchMode) {
25
- SearchMode["Exact"] = "exact";
26
- SearchMode["Approximate"] = "approximate";
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 (normalized === SearchMode.Approximate) {
34
- return SearchMode.Approximate;
39
+ if (raw === "auto") {
40
+ return "auto";
35
41
  }
36
- // Default: exact search (single-phase over full-precision embedding) for the one-table layout.
37
- return SearchMode.Exact;
42
+ const n = parseIntegerEnv(raw, 0, { min: 1 });
43
+ return n > 0 ? n : undefined;
38
44
  }
39
- function resolveSearchModeEnv() {
40
- return resolveSearchMode(process.env.YDB_QDRANT_SEARCH_MODE);
41
- }
42
- export const SEARCH_MODE = resolveSearchModeEnv();
43
- export const OVERFETCH_MULTIPLIER = parseIntegerEnv(process.env.YDB_QDRANT_OVERFETCH_MULTIPLIER, 10, { min: 1 });
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
@@ -1 +1,3 @@
1
1
  import "dotenv/config";
2
+ export declare function installStderrAsErrorLogger(): void;
3
+ export declare function __restoreStderrWriteForTests(): void;
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");
@@ -0,0 +1,2 @@
1
+ import { Transform } from "node:stream";
2
+ export declare function createDeployLogFormatter(): Transform;