ydb-qdrant 4.8.0 → 5.0.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.
Files changed (32) hide show
  1. package/README.md +5 -9
  2. package/dist/config/env.d.ts +1 -7
  3. package/dist/config/env.js +7 -27
  4. package/dist/index.js +10 -5
  5. package/dist/repositories/collectionsRepo.d.ts +2 -3
  6. package/dist/repositories/collectionsRepo.js +32 -63
  7. package/dist/repositories/collectionsRepo.one-table.js +83 -6
  8. package/dist/repositories/pointsRepo.d.ts +3 -3
  9. package/dist/repositories/pointsRepo.js +3 -13
  10. package/dist/repositories/pointsRepo.one-table.js +9 -7
  11. package/dist/services/CollectionService.d.ts +0 -7
  12. package/dist/services/CollectionService.js +7 -21
  13. package/dist/services/CollectionService.one-table.d.ts +1 -1
  14. package/dist/services/PointsService.js +8 -17
  15. package/dist/utils/distance.d.ts +0 -1
  16. package/dist/utils/distance.js +0 -14
  17. package/dist/utils/retry.js +13 -1
  18. package/dist/ydb/client.d.ts +14 -3
  19. package/dist/ydb/client.js +61 -3
  20. package/package.json +4 -4
  21. package/dist/indexing/IndexScheduler.d.ts +0 -5
  22. package/dist/indexing/IndexScheduler.js +0 -21
  23. package/dist/indexing/IndexScheduler.multi-table.d.ts +0 -12
  24. package/dist/indexing/IndexScheduler.multi-table.js +0 -54
  25. package/dist/indexing/IndexScheduler.one-table.d.ts +0 -1
  26. package/dist/indexing/IndexScheduler.one-table.js +0 -4
  27. package/dist/repositories/collectionsRepo.multi-table.d.ts +0 -3
  28. package/dist/repositories/collectionsRepo.multi-table.js +0 -23
  29. package/dist/repositories/pointsRepo.multi-table.d.ts +0 -12
  30. package/dist/repositories/pointsRepo.multi-table.js +0 -144
  31. package/dist/services/CollectionService.multi-table.d.ts +0 -5
  32. package/dist/services/CollectionService.multi-table.js +0 -7
package/README.md CHANGED
@@ -7,9 +7,7 @@
7
7
  [![k6 Stress Load Test](https://img.shields.io/github/actions/workflow/status/astandrik/ydb-qdrant/ci-load-stress.yml?branch=main&label=k6%20stress%20load%20test)](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-load-stress.yml)
8
8
  [![Coverage](https://coveralls.io/repos/github/astandrik/ydb-qdrant/badge.svg?branch=main)](https://coveralls.io/github/astandrik/ydb-qdrant?branch=main)
9
9
 
10
- [![Recall (multi_table)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astandrik/ydb-qdrant/recall-badges/recall-multi-table.json)](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-recall.yml)
11
10
  [![Recall (one_table)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astandrik/ydb-qdrant/recall-badges/recall-one-table.json)](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-recall.yml)
12
- [![F1 (multi_table)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astandrik/ydb-qdrant/recall-badges/f1-multi-table.json)](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-recall.yml)
13
11
  [![F1 (one_table)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astandrik/ydb-qdrant/recall-badges/f1-one-table.json)](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-recall.yml)
14
12
 
15
13
  [![npm version](https://img.shields.io/npm/v/ydb-qdrant.svg)](https://www.npmjs.com/package/ydb-qdrant)
@@ -18,7 +16,7 @@
18
16
 
19
17
  # YDB Qdrant-compatible Service
20
18
 
21
- Qdrant-compatible Node.js/TypeScript **service and npm library** that stores and searches vectors in YDB using single‑phase top‑k with an automatic YDB vector index (`vector_kmeans_tree`) and tablescan fallback. 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 (single-phase over `embedding`) by default and an optional approximate mode (twophase bit-quantized over `embedding_quantized` + `embedding`). Topics: ydb, vector-search, qdrant-compatible, nodejs, typescript, express, yql, ann, semantic-search, rag.
22
20
 
23
21
  Modes:
24
22
  - **HTTP server**: Qdrant-compatible REST API (`/collections`, `/points/*`) on top of YDB.
@@ -35,7 +33,7 @@ Architecture diagrams: [docs page](http://ydb-qdrant.tech/docs/)
35
33
 
36
34
  - **Vector dimensions and embedding models**: [docs/vector-dimensions.md](docs/vector-dimensions.md)
37
35
  - **Deployment and Docker options**: [docs/deployment-and-docker.md](docs/deployment-and-docker.md)
38
- - **Architecture, storage layout, and vector indexing**: [docs/architecture-and-storage.md](docs/architecture-and-storage.md)
36
+ - **Architecture, storage layout, and search modes**: [docs/architecture-and-storage.md](docs/architecture-and-storage.md)
39
37
  - **Evaluation, CI, and release process**: [docs/evaluation-and-ci.md](docs/evaluation-and-ci.md)
40
38
 
41
39
  ## Requirements
@@ -90,10 +88,8 @@ Optional env:
90
88
  # Server
91
89
  export PORT=8080
92
90
  export LOG_LEVEL=info
93
- # Collection storage mode (optional; default is multi_table)
94
- export YDB_QDRANT_COLLECTION_STORAGE_MODE=multi_table # or one_table
95
- # One-table search tuning (one_table mode only)
96
- export YDB_QDRANT_SEARCH_MODE=approximate # or exact
91
+ # One-table search tuning (default is 'exact' when unset)
92
+ export YDB_QDRANT_SEARCH_MODE=approximate # approximate or exact (default: exact)
97
93
  export YDB_QDRANT_OVERFETCH_MULTIPLIER=10 # candidate multiplier in approximate mode
98
94
  ```
99
95
 
@@ -334,7 +330,7 @@ curl -X POST http://localhost:8080/collections/mycol/points/delete \
334
330
 
335
331
  ## Architecture and Storage
336
332
 
337
- For details on the YDB storage layout (multi_table vs one_table), vector serialization, vector index auto-build behavior, request normalization, and Qdrant compatibility semantics, see [docs/architecture-and-storage.md](docs/architecture-and-storage.md).
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).
338
334
 
339
335
  ## Evaluation, CI, and Release
340
336
 
@@ -3,18 +3,12 @@ export declare const YDB_ENDPOINT: string;
3
3
  export declare const YDB_DATABASE: string;
4
4
  export declare const PORT: number;
5
5
  export declare const LOG_LEVEL: string;
6
- export declare enum CollectionStorageMode {
7
- MultiTable = "multi_table",
8
- OneTable = "one_table"
9
- }
10
- export declare const COLLECTION_STORAGE_MODE: CollectionStorageMode;
11
6
  export declare const GLOBAL_POINTS_AUTOMIGRATE_ENABLED: boolean;
12
- export declare const VECTOR_INDEX_BUILD_ENABLED: boolean;
13
- export declare function isOneTableMode(mode: CollectionStorageMode): mode is CollectionStorageMode.OneTable;
14
7
  export declare enum SearchMode {
15
8
  Exact = "exact",
16
9
  Approximate = "approximate"
17
10
  }
11
+ export declare function resolveSearchMode(raw: string | undefined): SearchMode;
18
12
  export declare const SEARCH_MODE: SearchMode;
19
13
  export declare const OVERFETCH_MULTIPLIER: number;
20
14
  export declare const CLIENT_SIDE_SERIALIZATION_ENABLED: boolean;
@@ -34,32 +34,13 @@ function parseBooleanEnv(value, defaultValue) {
34
34
  }
35
35
  return true;
36
36
  }
37
- export var CollectionStorageMode;
38
- (function (CollectionStorageMode) {
39
- CollectionStorageMode["MultiTable"] = "multi_table";
40
- CollectionStorageMode["OneTable"] = "one_table";
41
- })(CollectionStorageMode || (CollectionStorageMode = {}));
42
- function resolveCollectionStorageModeEnv() {
43
- const explicit = process.env.YDB_QDRANT_COLLECTION_STORAGE_MODE ??
44
- process.env.YDB_QDRANT_TABLE_LAYOUT;
45
- if (explicit?.trim().toLowerCase() === CollectionStorageMode.OneTable) {
46
- return CollectionStorageMode.OneTable;
47
- }
48
- return CollectionStorageMode.MultiTable;
49
- }
50
- export const COLLECTION_STORAGE_MODE = resolveCollectionStorageModeEnv();
51
37
  export const GLOBAL_POINTS_AUTOMIGRATE_ENABLED = parseBooleanEnv(process.env.YDB_QDRANT_GLOBAL_POINTS_AUTOMIGRATE, false);
52
- export const VECTOR_INDEX_BUILD_ENABLED = parseBooleanEnv(process.env.VECTOR_INDEX_BUILD_ENABLED, COLLECTION_STORAGE_MODE === CollectionStorageMode.MultiTable);
53
- export function isOneTableMode(mode) {
54
- return mode === CollectionStorageMode.OneTable;
55
- }
56
38
  export var SearchMode;
57
39
  (function (SearchMode) {
58
40
  SearchMode["Exact"] = "exact";
59
41
  SearchMode["Approximate"] = "approximate";
60
42
  })(SearchMode || (SearchMode = {}));
61
- function resolveSearchModeEnv(mode) {
62
- const raw = process.env.YDB_QDRANT_SEARCH_MODE;
43
+ export function resolveSearchMode(raw) {
63
44
  const normalized = raw?.trim().toLowerCase();
64
45
  if (normalized === SearchMode.Exact) {
65
46
  return SearchMode.Exact;
@@ -67,14 +48,13 @@ function resolveSearchModeEnv(mode) {
67
48
  if (normalized === SearchMode.Approximate) {
68
49
  return SearchMode.Approximate;
69
50
  }
70
- // Default: keep current behavior for one-table (approximate two-phase search).
71
- if (isOneTableMode(mode)) {
72
- return SearchMode.Approximate;
73
- }
74
- // For multi-table, this value is currently unused but defaults to approximate.
75
- return SearchMode.Approximate;
51
+ // Default: exact search (single-phase over full-precision embedding) for the one-table layout.
52
+ return SearchMode.Exact;
53
+ }
54
+ function resolveSearchModeEnv() {
55
+ return resolveSearchMode(process.env.YDB_QDRANT_SEARCH_MODE);
76
56
  }
77
- export const SEARCH_MODE = resolveSearchModeEnv(COLLECTION_STORAGE_MODE);
57
+ export const SEARCH_MODE = resolveSearchModeEnv();
78
58
  export const OVERFETCH_MULTIPLIER = parseIntegerEnv(process.env.YDB_QDRANT_OVERFETCH_MULTIPLIER, 10, { min: 1 });
79
59
  export const CLIENT_SIDE_SERIALIZATION_ENABLED = parseBooleanEnv(process.env.YDB_QDRANT_CLIENT_SIDE_SERIALIZATION_ENABLED, false);
80
60
  export const UPSERT_BATCH_SIZE = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_BATCH_SIZE, 100, { min: 1 });
package/dist/index.js CHANGED
@@ -1,18 +1,23 @@
1
1
  import "dotenv/config";
2
2
  import { buildServer } from "./server.js";
3
- import { PORT, COLLECTION_STORAGE_MODE, isOneTableMode, } from "./config/env.js";
3
+ import { PORT } from "./config/env.js";
4
4
  import { logger } from "./logging/logger.js";
5
- import { readyOrThrow } from "./ydb/client.js";
5
+ import { readyOrThrow, isCompilationTimeoutError } from "./ydb/client.js";
6
6
  import { ensureMetaTable, ensureGlobalPointsTable } from "./ydb/schema.js";
7
+ import { verifyCollectionsQueryCompilationForStartup } from "./repositories/collectionsRepo.js";
7
8
  async function start() {
8
9
  try {
9
10
  await readyOrThrow();
10
11
  await ensureMetaTable();
11
- if (isOneTableMode(COLLECTION_STORAGE_MODE)) {
12
- await ensureGlobalPointsTable();
13
- }
12
+ await ensureGlobalPointsTable();
13
+ await verifyCollectionsQueryCompilationForStartup();
14
+ logger.info("YDB compilation startup probe for qdr__collections completed successfully");
14
15
  }
15
16
  catch (err) {
17
+ if (isCompilationTimeoutError(err)) {
18
+ logger.error({ err }, "Fatal YDB compilation timeout during startup probe; exiting so supervisor can restart the process");
19
+ process.exit(1);
20
+ }
16
21
  logger.error({ err }, "YDB not ready; startup continues, requests may fail until configured.");
17
22
  }
18
23
  const app = buildServer();
@@ -1,11 +1,10 @@
1
1
  import type { DistanceKind, VectorType } from "../types";
2
- import { type CollectionStorageMode } from "../config/env.js";
3
- export declare function createCollection(metaKey: string, dim: number, distance: DistanceKind, vectorType: VectorType, tableName: string, layout?: CollectionStorageMode): Promise<void>;
2
+ export declare function createCollection(metaKey: string, dim: number, distance: DistanceKind, vectorType: VectorType): Promise<void>;
4
3
  export declare function getCollectionMeta(metaKey: string): Promise<{
5
4
  table: string;
6
5
  dimension: number;
7
6
  distance: DistanceKind;
8
7
  vectorType: VectorType;
9
8
  } | null>;
9
+ export declare function verifyCollectionsQueryCompilationForStartup(): Promise<void>;
10
10
  export declare function deleteCollection(metaKey: string, uid?: string): Promise<void>;
11
- export declare function buildVectorIndex(tableName: string, dimension: number, distance: DistanceKind, vectorType: VectorType): Promise<void>;
@@ -1,16 +1,8 @@
1
- import { TypedValues, withSession } from "../ydb/client.js";
2
- import { mapDistanceToIndexParam } from "../utils/distance.js";
3
- import { COLLECTION_STORAGE_MODE, isOneTableMode, } from "../config/env.js";
4
- import { GLOBAL_POINTS_TABLE } from "../ydb/schema.js";
1
+ import { TypedValues, withSession, createExecuteQuerySettings, withStartupProbeSession, createExecuteQuerySettingsWithTimeout, } from "../ydb/client.js";
5
2
  import { uidFor } from "../utils/tenant.js";
6
- import { createCollectionMultiTable, deleteCollectionMultiTable, } from "./collectionsRepo.multi-table.js";
7
3
  import { createCollectionOneTable, deleteCollectionOneTable, } from "./collectionsRepo.one-table.js";
8
- export async function createCollection(metaKey, dim, distance, vectorType, tableName, layout = COLLECTION_STORAGE_MODE) {
9
- if (isOneTableMode(layout)) {
10
- await createCollectionOneTable(metaKey, dim, distance, vectorType);
11
- return;
12
- }
13
- await createCollectionMultiTable(metaKey, dim, distance, vectorType, tableName);
4
+ export async function createCollection(metaKey, dim, distance, vectorType) {
5
+ await createCollectionOneTable(metaKey, dim, distance, vectorType);
14
6
  }
15
7
  export async function getCollectionMeta(metaKey) {
16
8
  const qry = `
@@ -20,9 +12,10 @@ export async function getCollectionMeta(metaKey) {
20
12
  WHERE collection = $collection;
21
13
  `;
22
14
  const res = await withSession(async (s) => {
15
+ const settings = createExecuteQuerySettings();
23
16
  return await s.executeQuery(qry, {
24
17
  $collection: TypedValues.utf8(metaKey),
25
- });
18
+ }, undefined, settings);
26
19
  });
27
20
  const rowset = res.resultSets?.[0];
28
21
  if (!rowset || rowset.rows?.length !== 1)
@@ -34,60 +27,36 @@ export async function getCollectionMeta(metaKey) {
34
27
  const vectorType = row.items?.[3]?.textValue ?? "float";
35
28
  return { table, dimension, distance, vectorType };
36
29
  }
30
+ export async function verifyCollectionsQueryCompilationForStartup() {
31
+ const probeKey = "__startup_probe__/__startup_probe__";
32
+ const qry = `
33
+ DECLARE $collection AS Utf8;
34
+ SELECT table_name, vector_dimension, distance, vector_type
35
+ FROM qdr__collections
36
+ WHERE collection = $collection;
37
+ `;
38
+ await withStartupProbeSession(async (s) => {
39
+ const settings = createExecuteQuerySettingsWithTimeout({
40
+ keepInCache: true,
41
+ idempotent: true,
42
+ timeoutMs: 3000,
43
+ });
44
+ await s.executeQuery(qry, {
45
+ $collection: TypedValues.utf8(probeKey),
46
+ }, undefined, settings);
47
+ });
48
+ }
37
49
  export async function deleteCollection(metaKey, uid) {
38
50
  const meta = await getCollectionMeta(metaKey);
39
51
  if (!meta)
40
52
  return;
41
- if (meta.table === GLOBAL_POINTS_TABLE) {
42
- const effectiveUid = uid ??
43
- (() => {
44
- const [tenant, collection] = metaKey.split("/", 2);
45
- if (!tenant || !collection) {
46
- throw new Error(`deleteCollection: cannot derive uid from malformed metaKey=${metaKey}`);
47
- }
48
- return uidFor(tenant, collection);
49
- })();
50
- await deleteCollectionOneTable(metaKey, effectiveUid);
51
- return;
52
- }
53
- await deleteCollectionMultiTable(metaKey, meta.table);
54
- }
55
- export async function buildVectorIndex(tableName, dimension, distance, vectorType) {
56
- const distParam = mapDistanceToIndexParam(distance);
57
- // defaults for <100k vectors
58
- const levels = 1;
59
- const clusters = 128;
60
- await withSession(async (s) => {
61
- // Drop existing index if present
62
- const dropDdl = `ALTER TABLE ${tableName} DROP INDEX emb_idx;`;
63
- const rawSession = s;
64
- try {
65
- const dropReq = { sessionId: rawSession.sessionId, yqlText: dropDdl };
66
- await rawSession.api.executeSchemeQuery(dropReq);
67
- }
68
- catch (e) {
69
- const msg = e instanceof Error ? e.message : String(e);
70
- // ignore if index doesn't exist
71
- if (!/not found|does not exist|no such index/i.test(msg)) {
72
- throw e;
73
- }
53
+ let effectiveUid = uid;
54
+ if (!effectiveUid) {
55
+ const [tenant, collection] = metaKey.split("/", 2);
56
+ if (!tenant || !collection) {
57
+ throw new Error(`deleteCollection: cannot derive uid from malformed metaKey=${metaKey}`);
74
58
  }
75
- // Create new index
76
- const createDdl = `
77
- ALTER TABLE ${tableName}
78
- ADD INDEX emb_idx GLOBAL SYNC USING vector_kmeans_tree
79
- ON (embedding)
80
- WITH (
81
- ${distParam === "inner_product"
82
- ? `similarity="inner_product"`
83
- : `distance="${distParam}"`},
84
- vector_type="${vectorType}",
85
- vector_dimension=${dimension},
86
- clusters=${clusters},
87
- levels=${levels}
88
- );
89
- `;
90
- const createReq = { sessionId: rawSession.sessionId, yqlText: createDdl };
91
- await rawSession.api.executeSchemeQuery(createReq);
92
- });
59
+ effectiveUid = uidFor(tenant, collection);
60
+ }
61
+ await deleteCollectionOneTable(metaKey, effectiveUid);
93
62
  }
@@ -1,6 +1,63 @@
1
- import { TypedValues, withSession } from "../ydb/client.js";
1
+ import { TypedValues, Types, withSession, createExecuteQuerySettings, } from "../ydb/client.js";
2
2
  import { GLOBAL_POINTS_TABLE, ensureGlobalPointsTable } from "../ydb/schema.js";
3
3
  import { upsertCollectionMeta } from "./collectionsRepo.shared.js";
4
+ import { withRetry, isTransientYdbError } from "../utils/retry.js";
5
+ const DELETE_COLLECTION_BATCH_SIZE = 10000;
6
+ function isOutOfBufferMemoryYdbError(error) {
7
+ const msg = error instanceof Error ? error.message : String(error);
8
+ if (/Out of buffer memory/i.test(msg)) {
9
+ return true;
10
+ }
11
+ if (typeof error === "object" && error !== null) {
12
+ const issues = error.issues;
13
+ if (issues !== undefined) {
14
+ const issuesText = typeof issues === "string" ? issues : JSON.stringify(issues);
15
+ return /Out of buffer memory/i.test(issuesText);
16
+ }
17
+ }
18
+ return false;
19
+ }
20
+ async function deletePointsForUidInChunks(s, uid) {
21
+ const selectYql = `
22
+ DECLARE $uid AS Utf8;
23
+ DECLARE $limit AS Uint32;
24
+ SELECT point_id
25
+ FROM ${GLOBAL_POINTS_TABLE}
26
+ WHERE uid = $uid
27
+ LIMIT $limit;
28
+ `;
29
+ const deleteBatchYql = `
30
+ DECLARE $uid AS Utf8;
31
+ DECLARE $ids AS List<Utf8>;
32
+ DELETE FROM ${GLOBAL_POINTS_TABLE}
33
+ WHERE uid = $uid AND point_id IN $ids;
34
+ `;
35
+ // Best‑effort loop: stop when there are no more rows for this uid.
36
+ // Each iteration only touches a limited number of rows to avoid
37
+ // hitting YDB's per‑operation buffer limits.
38
+ let iterations = 0;
39
+ const MAX_ITERATIONS = 1000;
40
+ const settings = createExecuteQuerySettings();
41
+ while (iterations++ < MAX_ITERATIONS) {
42
+ const rs = (await s.executeQuery(selectYql, {
43
+ $uid: TypedValues.utf8(uid),
44
+ $limit: TypedValues.uint32(DELETE_COLLECTION_BATCH_SIZE),
45
+ }, undefined, settings));
46
+ const rowset = rs.resultSets?.[0];
47
+ const rows = rowset?.rows ?? [];
48
+ const ids = rows
49
+ .map((row) => row.items?.[0]?.textValue)
50
+ .filter((id) => typeof id === "string");
51
+ if (ids.length === 0) {
52
+ break;
53
+ }
54
+ const idsValue = TypedValues.list(Types.UTF8, ids);
55
+ await s.executeQuery(deleteBatchYql, {
56
+ $uid: TypedValues.utf8(uid),
57
+ $ids: idsValue,
58
+ }, undefined, settings);
59
+ }
60
+ }
4
61
  export async function createCollectionOneTable(metaKey, dim, distance, vectorType) {
5
62
  await upsertCollectionMeta(metaKey, dim, distance, vectorType, GLOBAL_POINTS_TABLE);
6
63
  }
@@ -10,16 +67,36 @@ export async function deleteCollectionOneTable(metaKey, uid) {
10
67
  DECLARE $uid AS Utf8;
11
68
  DELETE FROM ${GLOBAL_POINTS_TABLE} WHERE uid = $uid;
12
69
  `;
13
- await withSession(async (s) => {
14
- await s.executeQuery(deletePointsYql, {
15
- $uid: TypedValues.utf8(uid),
16
- });
70
+ await withRetry(() => withSession(async (s) => {
71
+ const settings = createExecuteQuerySettings();
72
+ try {
73
+ await s.executeQuery(deletePointsYql, {
74
+ $uid: TypedValues.utf8(uid),
75
+ }, undefined, settings);
76
+ }
77
+ catch (err) {
78
+ if (!isOutOfBufferMemoryYdbError(err)) {
79
+ throw err;
80
+ }
81
+ await deletePointsForUidInChunks(s, uid);
82
+ }
83
+ }), {
84
+ isTransient: isTransientYdbError,
85
+ context: {
86
+ operation: "deleteCollectionOneTable",
87
+ tableName: GLOBAL_POINTS_TABLE,
88
+ metaKey,
89
+ uid,
90
+ },
17
91
  });
18
92
  const delMeta = `
19
93
  DECLARE $collection AS Utf8;
20
94
  DELETE FROM qdr__collections WHERE collection = $collection;
21
95
  `;
22
96
  await withSession(async (s) => {
23
- await s.executeQuery(delMeta, { $collection: TypedValues.utf8(metaKey) });
97
+ const settings = createExecuteQuerySettings();
98
+ await s.executeQuery(delMeta, {
99
+ $collection: TypedValues.utf8(metaKey),
100
+ }, undefined, settings);
24
101
  });
25
102
  }
@@ -3,10 +3,10 @@ export declare function upsertPoints(tableName: string, points: Array<{
3
3
  id: string | number;
4
4
  vector: number[];
5
5
  payload?: Record<string, unknown>;
6
- }>, dimension: number, uid?: string): Promise<number>;
7
- export declare function searchPoints(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number, uid?: string): Promise<Array<{
6
+ }>, dimension: number, uid: string): Promise<number>;
7
+ export declare function searchPoints(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number, uid: string): Promise<Array<{
8
8
  id: string;
9
9
  score: number;
10
10
  payload?: Record<string, unknown>;
11
11
  }>>;
12
- export declare function deletePoints(tableName: string, ids: Array<string | number>, uid?: string): Promise<number>;
12
+ export declare function deletePoints(tableName: string, ids: Array<string | number>, uid: string): Promise<number>;
@@ -1,22 +1,12 @@
1
- import { upsertPointsMultiTable, searchPointsMultiTable, deletePointsMultiTable, } from "./pointsRepo.multi-table.js";
2
1
  import { SEARCH_MODE, OVERFETCH_MULTIPLIER, } from "../config/env.js";
3
2
  import { upsertPointsOneTable, searchPointsOneTable, deletePointsOneTable, } from "./pointsRepo.one-table.js";
4
3
  export async function upsertPoints(tableName, points, dimension, uid) {
5
- if (uid) {
6
- return await upsertPointsOneTable(tableName, points, dimension, uid);
7
- }
8
- return await upsertPointsMultiTable(tableName, points, dimension);
4
+ return await upsertPointsOneTable(tableName, points, dimension, uid);
9
5
  }
10
6
  export async function searchPoints(tableName, queryVector, top, withPayload, distance, dimension, uid) {
11
7
  const mode = SEARCH_MODE;
12
- if (uid) {
13
- return await searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid, mode, OVERFETCH_MULTIPLIER);
14
- }
15
- return await searchPointsMultiTable(tableName, queryVector, top, withPayload, distance, dimension);
8
+ return await searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid, mode, OVERFETCH_MULTIPLIER);
16
9
  }
17
10
  export async function deletePoints(tableName, ids, uid) {
18
- if (uid) {
19
- return await deletePointsOneTable(tableName, ids, uid);
20
- }
21
- return await deletePointsMultiTable(tableName, ids);
11
+ return await deletePointsOneTable(tableName, ids, uid);
22
12
  }
@@ -1,6 +1,5 @@
1
- import { TypedValues, Types, withSession } from "../ydb/client.js";
1
+ import { TypedValues, Types, withSession, createExecuteQuerySettings, } from "../ydb/client.js";
2
2
  import { buildVectorParam, buildVectorBinaryParams } from "../ydb/helpers.js";
3
- import { notifyUpsert } from "../indexing/IndexScheduler.js";
4
3
  import { mapDistanceToKnnFn, mapDistanceToBitKnnFn, } from "../utils/distance.js";
5
4
  import { withRetry, isTransientYdbError } from "../utils/retry.js";
6
5
  import { UPSERT_BATCH_SIZE } from "../ydb/schema.js";
@@ -25,6 +24,7 @@ export async function upsertPointsOneTable(tableName, points, dimension, uid) {
25
24
  }
26
25
  let upserted = 0;
27
26
  await withSession(async (s) => {
27
+ const settings = createExecuteQuerySettings();
28
28
  for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
29
29
  const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
30
30
  let ddl;
@@ -120,14 +120,13 @@ export async function upsertPointsOneTable(tableName, points, dimension, uid) {
120
120
  })),
121
121
  },
122
122
  }, "one_table upsert: executing YQL");
123
- await withRetry(() => s.executeQuery(ddl, params), {
123
+ await withRetry(() => s.executeQuery(ddl, params, undefined, settings), {
124
124
  isTransient: isTransientYdbError,
125
125
  context: { tableName, batchSize: batch.length },
126
126
  });
127
127
  upserted += batch.length;
128
128
  }
129
129
  });
130
- notifyUpsert(tableName, upserted);
131
130
  return upserted;
132
131
  }
133
132
  async function searchPointsOneTableExact(tableName, queryVector, top, withPayload, distance, dimension, uid) {
@@ -193,7 +192,8 @@ async function searchPointsOneTableExact(tableName, queryVector, top, withPayloa
193
192
  vectorPreview: queryVector.slice(0, 3),
194
193
  },
195
194
  }, "one_table search (exact): executing YQL");
196
- const rs = await s.executeQuery(yql, params);
195
+ const settings = createExecuteQuerySettings();
196
+ const rs = await s.executeQuery(yql, params, undefined, settings);
197
197
  const rowset = rs.resultSets?.[0];
198
198
  const rows = (rowset?.rows ?? []);
199
199
  return rows.map((row) => {
@@ -334,7 +334,8 @@ async function searchPointsOneTableApproximate(tableName, queryVector, top, with
334
334
  },
335
335
  }, "one_table search (approximate): executing YQL");
336
336
  }
337
- const rs = await s.executeQuery(yql, params);
337
+ const settings = createExecuteQuerySettings();
338
+ const rs = await s.executeQuery(yql, params, undefined, settings);
338
339
  const rowset = rs.resultSets?.[0];
339
340
  const rows = (rowset?.rows ?? []);
340
341
  return rows.map((row) => {
@@ -371,6 +372,7 @@ export async function searchPointsOneTable(tableName, queryVector, top, withPayl
371
372
  export async function deletePointsOneTable(tableName, ids, uid) {
372
373
  let deleted = 0;
373
374
  await withSession(async (s) => {
375
+ const settings = createExecuteQuerySettings();
374
376
  for (const id of ids) {
375
377
  const yql = `
376
378
  DECLARE $uid AS Utf8;
@@ -381,7 +383,7 @@ export async function deletePointsOneTable(tableName, ids, uid) {
381
383
  $uid: TypedValues.utf8(uid),
382
384
  $id: TypedValues.utf8(String(id)),
383
385
  };
384
- await s.executeQuery(yql, params);
386
+ await s.executeQuery(yql, params, undefined, settings);
385
387
  deleted += 1;
386
388
  }
387
389
  });
@@ -10,13 +10,6 @@ export interface NormalizedCollectionContext {
10
10
  collection: string;
11
11
  metaKey: string;
12
12
  }
13
- export declare function normalizeCollectionContext(input: CollectionContextInput): NormalizedCollectionContext;
14
- export declare function resolvePointsTableAndUid(ctx: NormalizedCollectionContext, meta: {
15
- table: string;
16
- }): Promise<{
17
- tableName: string;
18
- uid: string | undefined;
19
- }>;
20
13
  export declare function putCollectionIndex(ctx: CollectionContextInput): Promise<{
21
14
  acknowledged: boolean;
22
15
  }>;
@@ -1,24 +1,11 @@
1
1
  import { CreateCollectionReq } from "../types.js";
2
- import { ensureMetaTable, GLOBAL_POINTS_TABLE } from "../ydb/schema.js";
2
+ import { ensureMetaTable } from "../ydb/schema.js";
3
3
  import { createCollection as repoCreateCollection, deleteCollection as repoDeleteCollection, getCollectionMeta, } from "../repositories/collectionsRepo.js";
4
4
  import { QdrantServiceError } from "./errors.js";
5
- import { normalizeCollectionContextShared, tableNameFor, } from "./CollectionService.shared.js";
6
- import { resolvePointsTableAndUidOneTable } from "./CollectionService.one-table.js";
7
- export function normalizeCollectionContext(input) {
8
- return normalizeCollectionContextShared(input.tenant, input.collection, input.apiKey, input.userAgent);
9
- }
10
- export async function resolvePointsTableAndUid(ctx, meta) {
11
- if (meta?.table === GLOBAL_POINTS_TABLE) {
12
- return await resolvePointsTableAndUidOneTable(ctx);
13
- }
14
- return {
15
- tableName: meta.table,
16
- uid: undefined,
17
- };
18
- }
5
+ import { normalizeCollectionContextShared } from "./CollectionService.shared.js";
19
6
  export async function putCollectionIndex(ctx) {
20
7
  await ensureMetaTable();
21
- const normalized = normalizeCollectionContext(ctx);
8
+ const normalized = normalizeCollectionContextShared(ctx.tenant, ctx.collection, ctx.apiKey, ctx.userAgent);
22
9
  const meta = await getCollectionMeta(normalized.metaKey);
23
10
  if (!meta) {
24
11
  throw new QdrantServiceError(404, {
@@ -30,7 +17,7 @@ export async function putCollectionIndex(ctx) {
30
17
  }
31
18
  export async function createCollection(ctx, body) {
32
19
  await ensureMetaTable();
33
- const normalized = normalizeCollectionContext(ctx);
20
+ const normalized = normalizeCollectionContextShared(ctx.tenant, ctx.collection, ctx.apiKey, ctx.userAgent);
34
21
  const parsed = CreateCollectionReq.safeParse(body);
35
22
  if (!parsed.success) {
36
23
  throw new QdrantServiceError(400, {
@@ -54,13 +41,12 @@ export async function createCollection(ctx, body) {
54
41
  error: errorMessage,
55
42
  });
56
43
  }
57
- const tableName = tableNameFor(normalized.tenant, normalized.collection);
58
- await repoCreateCollection(normalized.metaKey, dim, distance, vectorType, tableName);
44
+ await repoCreateCollection(normalized.metaKey, dim, distance, vectorType);
59
45
  return { name: normalized.collection, tenant: normalized.tenant };
60
46
  }
61
47
  export async function getCollection(ctx) {
62
48
  await ensureMetaTable();
63
- const normalized = normalizeCollectionContext(ctx);
49
+ const normalized = normalizeCollectionContextShared(ctx.tenant, ctx.collection, ctx.apiKey, ctx.userAgent);
64
50
  const meta = await getCollectionMeta(normalized.metaKey);
65
51
  if (!meta) {
66
52
  throw new QdrantServiceError(404, {
@@ -79,7 +65,7 @@ export async function getCollection(ctx) {
79
65
  }
80
66
  export async function deleteCollection(ctx) {
81
67
  await ensureMetaTable();
82
- const normalized = normalizeCollectionContext(ctx);
68
+ const normalized = normalizeCollectionContextShared(ctx.tenant, ctx.collection, ctx.apiKey, ctx.userAgent);
83
69
  await repoDeleteCollection(normalized.metaKey);
84
70
  return { acknowledged: true };
85
71
  }
@@ -1,5 +1,5 @@
1
1
  import { type NormalizedCollectionContextLike } from "./CollectionService.shared.js";
2
2
  export declare function resolvePointsTableAndUidOneTable(ctx: NormalizedCollectionContextLike): Promise<{
3
3
  tableName: string;
4
- uid: string | undefined;
4
+ uid: string;
5
5
  }>;
@@ -2,16 +2,14 @@ import { UpsertPointsReq, SearchReq, DeletePointsReq } from "../types.js";
2
2
  import { ensureMetaTable } from "../ydb/schema.js";
3
3
  import { getCollectionMeta } from "../repositories/collectionsRepo.js";
4
4
  import { deletePoints as repoDeletePoints, searchPoints as repoSearchPoints, upsertPoints as repoUpsertPoints, } from "../repositories/pointsRepo.js";
5
- import { requestIndexBuild } from "../indexing/IndexScheduler.js";
6
5
  import { logger } from "../logging/logger.js";
7
- import { VECTOR_INDEX_BUILD_ENABLED } from "../config/env.js";
8
6
  import { QdrantServiceError, isVectorDimensionMismatchError, } from "./errors.js";
9
- import { normalizeCollectionContext, resolvePointsTableAndUid, } from "./CollectionService.js";
7
+ import { normalizeCollectionContextShared } from "./CollectionService.shared.js";
8
+ import { resolvePointsTableAndUidOneTable } from "./CollectionService.one-table.js";
10
9
  import { normalizeSearchBodyForSearch, normalizeSearchBodyForQuery, } from "../utils/normalization.js";
11
- let loggedIndexBuildDisabled = false;
12
10
  export async function upsertPoints(ctx, body) {
13
11
  await ensureMetaTable();
14
- const normalized = normalizeCollectionContext(ctx);
12
+ const normalized = normalizeCollectionContextShared(ctx.tenant, ctx.collection, ctx.apiKey, ctx.userAgent);
15
13
  const meta = await getCollectionMeta(normalized.metaKey);
16
14
  if (!meta) {
17
15
  throw new QdrantServiceError(404, {
@@ -26,7 +24,7 @@ export async function upsertPoints(ctx, body) {
26
24
  error: parsed.error.flatten(),
27
25
  });
28
26
  }
29
- const { tableName, uid } = await resolvePointsTableAndUid(normalized, meta);
27
+ const { tableName, uid } = await resolvePointsTableAndUidOneTable(normalized);
30
28
  let upserted;
31
29
  try {
32
30
  upserted = await repoUpsertPoints(tableName, parsed.data.points, meta.dimension, uid);
@@ -46,18 +44,11 @@ export async function upsertPoints(ctx, body) {
46
44
  }
47
45
  throw err;
48
46
  }
49
- if (VECTOR_INDEX_BUILD_ENABLED) {
50
- requestIndexBuild(tableName, meta.dimension, meta.distance, meta.vectorType);
51
- }
52
- else if (!loggedIndexBuildDisabled) {
53
- logger.info({ table: tableName }, "vector index building disabled by env; skipping automatic emb_idx rebuilds");
54
- loggedIndexBuildDisabled = true;
55
- }
56
47
  return { upserted };
57
48
  }
58
49
  async function executeSearch(ctx, normalizedSearch, source) {
59
50
  await ensureMetaTable();
60
- const normalized = normalizeCollectionContext(ctx);
51
+ const normalized = normalizeCollectionContextShared(ctx.tenant, ctx.collection, ctx.apiKey, ctx.userAgent);
61
52
  logger.info({ tenant: normalized.tenant, collection: normalized.collection }, `${source}: resolve collection meta`);
62
53
  const meta = await getCollectionMeta(normalized.metaKey);
63
54
  if (!meta) {
@@ -87,7 +78,7 @@ async function executeSearch(ctx, normalizedSearch, source) {
87
78
  error: parsed.error.flatten(),
88
79
  });
89
80
  }
90
- const { tableName, uid } = await resolvePointsTableAndUid(normalized, meta);
81
+ const { tableName, uid } = await resolvePointsTableAndUidOneTable(normalized);
91
82
  logger.info({
92
83
  tenant: normalized.tenant,
93
84
  collection: normalized.collection,
@@ -154,7 +145,7 @@ export async function queryPoints(ctx, body) {
154
145
  }
155
146
  export async function deletePoints(ctx, body) {
156
147
  await ensureMetaTable();
157
- const normalized = normalizeCollectionContext(ctx);
148
+ const normalized = normalizeCollectionContextShared(ctx.tenant, ctx.collection, ctx.apiKey, ctx.userAgent);
158
149
  const meta = await getCollectionMeta(normalized.metaKey);
159
150
  if (!meta) {
160
151
  throw new QdrantServiceError(404, {
@@ -169,7 +160,7 @@ export async function deletePoints(ctx, body) {
169
160
  error: parsed.error.flatten(),
170
161
  });
171
162
  }
172
- const { tableName, uid } = await resolvePointsTableAndUid(normalized, meta);
163
+ const { tableName, uid } = await resolvePointsTableAndUidOneTable(normalized);
173
164
  const deleted = await repoDeletePoints(tableName, parsed.data.points, uid);
174
165
  return { deleted };
175
166
  }
@@ -3,7 +3,6 @@ export declare function mapDistanceToKnnFn(distance: DistanceKind): {
3
3
  fn: string;
4
4
  order: "ASC" | "DESC";
5
5
  };
6
- export declare function mapDistanceToIndexParam(distance: DistanceKind): string;
7
6
  /**
8
7
  * Maps a user-specified distance metric to a YDB Knn function
9
8
  * suitable for bit-quantized vectors (Phase 1 approximate candidate selection).
@@ -12,20 +12,6 @@ export function mapDistanceToKnnFn(distance) {
12
12
  return { fn: "Knn::CosineDistance", order: "ASC" };
13
13
  }
14
14
  }
15
- export function mapDistanceToIndexParam(distance) {
16
- switch (distance) {
17
- case "Cosine":
18
- return "cosine";
19
- case "Dot":
20
- return "inner_product";
21
- case "Euclid":
22
- return "euclidean";
23
- case "Manhattan":
24
- return "manhattan";
25
- default:
26
- return "cosine";
27
- }
28
- }
29
15
  /**
30
16
  * Maps a user-specified distance metric to a YDB Knn function
31
17
  * suitable for bit-quantized vectors (Phase 1 approximate candidate selection).
@@ -3,7 +3,19 @@ const DEFAULT_MAX_RETRIES = 6;
3
3
  const DEFAULT_BASE_DELAY_MS = 250;
4
4
  export function isTransientYdbError(error) {
5
5
  const msg = error instanceof Error ? error.message : String(error);
6
- return /Aborted|schema version mismatch|Table metadata loading|Failed to load metadata/i.test(msg);
6
+ if (/Aborted|schema version mismatch|Table metadata loading|Failed to load metadata|overloaded|is in process of split|wrong shard state|Rejecting data TxId/i.test(msg)) {
7
+ return true;
8
+ }
9
+ if (typeof error === "object" && error !== null) {
10
+ const issues = error.issues;
11
+ if (issues !== undefined) {
12
+ const issuesText = typeof issues === "string" ? issues : JSON.stringify(issues);
13
+ if (/overloaded|is in process of split|wrong shard state|Rejecting data TxId/i.test(issuesText)) {
14
+ return true;
15
+ }
16
+ }
17
+ }
18
+ return false;
7
19
  }
8
20
  export async function withRetry(fn, options = {}) {
9
21
  const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
@@ -1,18 +1,29 @@
1
- import type { Session, IAuthService } from "ydb-sdk";
2
- declare const Types: typeof import("ydb-sdk").Types, TypedValues: typeof import("ydb-sdk").TypedValues, TableDescription: typeof import("ydb-sdk").TableDescription, Column: typeof import("ydb-sdk").Column;
3
- export { Types, TypedValues, TableDescription, Column };
1
+ import type { Session, IAuthService, ExecuteQuerySettings as YdbExecuteQuerySettings } from "ydb-sdk";
2
+ declare const Types: typeof import("ydb-sdk").Types, TypedValues: typeof import("ydb-sdk").TypedValues, TableDescription: typeof import("ydb-sdk").TableDescription, Column: typeof import("ydb-sdk").Column, ExecuteQuerySettings: typeof YdbExecuteQuerySettings;
3
+ export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings };
4
+ export declare function createExecuteQuerySettings(options?: {
5
+ keepInCache?: boolean;
6
+ idempotent?: boolean;
7
+ }): YdbExecuteQuerySettings;
8
+ export declare function createExecuteQuerySettingsWithTimeout(options: {
9
+ keepInCache?: boolean;
10
+ idempotent?: boolean;
11
+ timeoutMs: number;
12
+ }): YdbExecuteQuerySettings;
4
13
  type DriverConfig = {
5
14
  endpoint?: string;
6
15
  database?: string;
7
16
  connectionString?: string;
8
17
  authService?: IAuthService;
9
18
  };
19
+ export declare function isCompilationTimeoutError(error: unknown): boolean;
10
20
  export declare function __setDriverForTests(fake: unknown): void;
11
21
  export declare function __setDriverFactoryForTests(factory: ((config: unknown) => unknown) | undefined): void;
12
22
  export declare function __resetRefreshStateForTests(): void;
13
23
  export declare function configureDriver(config: DriverConfig): void;
14
24
  export declare function readyOrThrow(): Promise<void>;
15
25
  export declare function withSession<T>(fn: (s: Session) => Promise<T>): Promise<T>;
26
+ export declare function withStartupProbeSession<T>(fn: (s: Session) => Promise<T>): Promise<T>;
16
27
  export declare function isYdbAvailable(timeoutMs?: number): Promise<boolean>;
17
28
  /**
18
29
  * Destroys the current driver and its session pool.
@@ -2,18 +2,59 @@ import { createRequire } from "module";
2
2
  import { YDB_DATABASE, YDB_ENDPOINT, SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, } from "../config/env.js";
3
3
  import { logger } from "../logging/logger.js";
4
4
  const require = createRequire(import.meta.url);
5
- const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, } = require("ydb-sdk");
6
- export { Types, TypedValues, TableDescription, Column };
5
+ const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, OperationParams, } = require("ydb-sdk");
6
+ export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings };
7
+ export function createExecuteQuerySettings(options) {
8
+ const { keepInCache = true, idempotent = true } = options ?? {};
9
+ const settings = new ExecuteQuerySettings();
10
+ if (keepInCache) {
11
+ settings.withKeepInCache(true);
12
+ }
13
+ if (idempotent) {
14
+ settings.withIdempotent(true);
15
+ }
16
+ return settings;
17
+ }
18
+ export function createExecuteQuerySettingsWithTimeout(options) {
19
+ const settings = createExecuteQuerySettings(options);
20
+ const op = new OperationParams();
21
+ const seconds = Math.max(1, Math.ceil(options.timeoutMs / 1000));
22
+ // Limit both overall operation processing time and cancellation time on the
23
+ // server side so the probe fails fast instead of hanging for the default.
24
+ op.withOperationTimeoutSeconds(seconds);
25
+ op.withCancelAfterSeconds(seconds);
26
+ settings.withOperationParams(op);
27
+ return settings;
28
+ }
7
29
  const DRIVER_READY_TIMEOUT_MS = 15000;
8
30
  const TABLE_SESSION_TIMEOUT_MS = 20000;
9
31
  const YDB_HEALTHCHECK_READY_TIMEOUT_MS = 5000;
10
32
  const DRIVER_REFRESH_COOLDOWN_MS = 30000;
33
+ const STARTUP_PROBE_SESSION_TIMEOUT_MS = 3000;
11
34
  let overrideConfig;
12
35
  let driver;
13
36
  let lastDriverRefreshAt = 0;
14
37
  let driverRefreshInFlight = null;
15
38
  // Test-only: allows injecting a mock Driver factory
16
39
  let driverFactoryOverride;
40
+ export function isCompilationTimeoutError(error) {
41
+ if (!(error instanceof Error)) {
42
+ return false;
43
+ }
44
+ const msg = error.message ?? "";
45
+ if (/Timeout \(code 400090\)/i.test(msg) ||
46
+ /Query compilation timed out/i.test(msg)) {
47
+ return true;
48
+ }
49
+ // Startup probe uses explicit cancel-after; YDB returns Cancelled with
50
+ // issues like "Cancelling after 3000ms during compilation". Treat this as
51
+ // a compilation-time failure for fatal startup handling.
52
+ if (/Cancelled \(code 400160\)/i.test(msg) &&
53
+ /during compilation/i.test(msg)) {
54
+ return true;
55
+ }
56
+ return false;
57
+ }
17
58
  function shouldTriggerDriverRefresh(error) {
18
59
  if (!(error instanceof Error)) {
19
60
  return false;
@@ -28,6 +69,12 @@ function shouldTriggerDriverRefresh(error) {
28
69
  if (/SessionExpired|SESSION_EXPIRED|session.*expired/i.test(msg)) {
29
70
  return true;
30
71
  }
72
+ // YDB query compilation timeout (TIMEOUT code 400090) – treat as a signal
73
+ // to refresh the driver/session pool so that subsequent attempts use a
74
+ // fresh connection state.
75
+ if (isCompilationTimeoutError(error)) {
76
+ return true;
77
+ }
31
78
  return false;
32
79
  }
33
80
  async function maybeRefreshDriverOnSessionError(error) {
@@ -44,7 +91,8 @@ async function maybeRefreshDriverOnSessionError(error) {
44
91
  return;
45
92
  }
46
93
  lastDriverRefreshAt = now;
47
- logger.warn({ err: error }, "YDB session-related error detected; refreshing driver");
94
+ const errorMessage = error instanceof Error ? error.message ?? "" : String(error);
95
+ logger.warn({ err: error, errorMessage, lastDriverRefreshAt }, "YDB session-related error detected; refreshing driver");
48
96
  try {
49
97
  const refreshPromise = refreshDriver();
50
98
  driverRefreshInFlight = refreshPromise;
@@ -120,6 +168,16 @@ export async function withSession(fn) {
120
168
  throw err;
121
169
  }
122
170
  }
171
+ export async function withStartupProbeSession(fn) {
172
+ const d = getOrCreateDriver();
173
+ try {
174
+ return await d.tableClient.withSession(fn, STARTUP_PROBE_SESSION_TIMEOUT_MS);
175
+ }
176
+ catch (err) {
177
+ void maybeRefreshDriverOnSessionError(err);
178
+ throw err;
179
+ }
180
+ }
123
181
  export async function isYdbAvailable(timeoutMs = YDB_HEALTHCHECK_READY_TIMEOUT_MS) {
124
182
  const d = getOrCreateDriver();
125
183
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-qdrant",
3
- "version": "4.8.0",
3
+ "version": "5.0.0",
4
4
  "main": "dist/package/api.js",
5
5
  "types": "dist/package/api.d.ts",
6
6
  "exports": {
@@ -16,8 +16,8 @@
16
16
  "scripts": {
17
17
  "test": "vitest run --exclude \"test/integration/**\"",
18
18
  "test:coverage": "vitest run --coverage --exclude \"test/integration/**\"",
19
- "test:integration": "VECTOR_INDEX_BUILD_ENABLED=false vitest run test/integration/YdbRealIntegration.index-disabled.test.ts && VECTOR_INDEX_BUILD_ENABLED=true vitest run test/integration/YdbRealIntegration.test.ts test/integration/YdbRecallIntegration.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table YDB_QDRANT_SEARCH_MODE=approximate vitest run test/integration/YdbRealIntegration.one-table.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table YDB_QDRANT_SEARCH_MODE=exact vitest run test/integration/YdbRealIntegration.one-table.test.ts",
20
- "test:recall": "VECTOR_INDEX_BUILD_ENABLED=true vitest run test/integration/YdbRecallIntegration.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table YDB_QDRANT_SEARCH_MODE=approximate vitest run test/integration/YdbRealIntegration.one-table.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table YDB_QDRANT_SEARCH_MODE=exact vitest run test/integration/YdbRealIntegration.one-table.test.ts",
19
+ "test:integration": "vitest run test/integration/YdbRealIntegration.test.ts && YDB_QDRANT_SEARCH_MODE=approximate vitest run test/integration/YdbRealIntegration.one-table.test.ts && YDB_QDRANT_SEARCH_MODE=exact vitest run test/integration/YdbRealIntegration.one-table.test.ts",
20
+ "test:recall": "YDB_QDRANT_SEARCH_MODE=approximate vitest run test/integration/YdbRealIntegration.one-table.test.ts && YDB_QDRANT_SEARCH_MODE=exact vitest run test/integration/YdbRealIntegration.one-table.test.ts",
21
21
  "load:soak": "k6 run loadtest/soak-test.js",
22
22
  "load:stress": "k6 run loadtest/stress-test.js",
23
23
  "build": "tsc -p tsconfig.json",
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "author": "",
51
51
  "license": "Apache-2.0",
52
- "description": "Qdrant-compatible Node.js/TypeScript API that stores/searches embeddings in YDB using single-phase top-k vector search with an automatic vector index and table-scan fallback.",
52
+ "description": "Qdrant-compatible Node.js/TypeScript API that stores/searches embeddings in YDB using a global one-table layout with exact and approximate KNN search over serialized vectors.",
53
53
  "type": "module",
54
54
  "publishConfig": {
55
55
  "access": "public"
@@ -1,5 +0,0 @@
1
- import type { DistanceKind, VectorType } from "../types.js";
2
- export declare function notifyUpsert(tableName: string, count?: number): void;
3
- export declare function requestIndexBuild(tableName: string, dimension: number, distance: DistanceKind, vectorType: VectorType, opts?: {
4
- force?: boolean;
5
- }): void;
@@ -1,21 +0,0 @@
1
- import { GLOBAL_POINTS_TABLE } from "../ydb/schema.js";
2
- import { state, requestIndexBuildMultiTable, } from "./IndexScheduler.multi-table.js";
3
- import { requestIndexBuildOneTable } from "./IndexScheduler.one-table.js";
4
- export function notifyUpsert(tableName, count = 1) {
5
- const now = Date.now();
6
- const s = state[tableName] ?? {
7
- lastUpsertMs: 0,
8
- pending: false,
9
- pointsUpserted: 0,
10
- };
11
- s.lastUpsertMs = now;
12
- s.pointsUpserted += count;
13
- state[tableName] = s;
14
- }
15
- export function requestIndexBuild(tableName, dimension, distance, vectorType, opts) {
16
- if (tableName === GLOBAL_POINTS_TABLE) {
17
- requestIndexBuildOneTable(tableName);
18
- return;
19
- }
20
- requestIndexBuildMultiTable(tableName, dimension, distance, vectorType, opts);
21
- }
@@ -1,12 +0,0 @@
1
- import type { DistanceKind, VectorType } from "../types.js";
2
- type CollectionKey = string;
3
- export declare const state: Record<CollectionKey, {
4
- lastUpsertMs: number;
5
- timer?: NodeJS.Timeout;
6
- pending: boolean;
7
- pointsUpserted: number;
8
- }>;
9
- export declare function requestIndexBuildMultiTable(tableName: string, dimension: number, distance: DistanceKind, vectorType: VectorType, opts?: {
10
- force?: boolean;
11
- }): void;
12
- export {};
@@ -1,54 +0,0 @@
1
- import { buildVectorIndex } from "../repositories/collectionsRepo.js";
2
- import { logger } from "../logging/logger.js";
3
- const QUIET_MS = 10000;
4
- const MIN_POINTS_THRESHOLD = 100;
5
- export const state = {};
6
- export function requestIndexBuildMultiTable(tableName, dimension, distance, vectorType, opts) {
7
- const s = state[tableName] ?? {
8
- lastUpsertMs: 0,
9
- pending: false,
10
- pointsUpserted: 0,
11
- };
12
- state[tableName] = s;
13
- if (opts?.force) {
14
- logger.info({ tableName }, "index build (force) starting");
15
- void buildVectorIndex(tableName, dimension, distance, vectorType)
16
- .then(() => {
17
- logger.info({ tableName }, "index build (force) completed");
18
- s.pointsUpserted = 0;
19
- })
20
- .catch((err) => {
21
- logger.error({ err, tableName }, "index build (force) failed");
22
- });
23
- return;
24
- }
25
- if (s.pending && s.timer) {
26
- return;
27
- }
28
- s.pending = true;
29
- s.timer = setTimeout(function tryBuild() {
30
- const since = Date.now() - (state[tableName]?.lastUpsertMs ?? 0);
31
- if (since < QUIET_MS) {
32
- s.timer = setTimeout(tryBuild, QUIET_MS - since);
33
- return;
34
- }
35
- const pointsCount = state[tableName]?.pointsUpserted ?? 0;
36
- if (pointsCount < MIN_POINTS_THRESHOLD) {
37
- logger.info({ tableName, pointsCount, threshold: MIN_POINTS_THRESHOLD }, "index build skipped (below threshold)");
38
- s.pending = false;
39
- s.timer = undefined;
40
- return;
41
- }
42
- logger.info({ tableName, pointsCount }, "index build (scheduled) starting");
43
- void buildVectorIndex(tableName, dimension, distance, vectorType)
44
- .then(() => {
45
- logger.info({ tableName }, "index build (scheduled) completed");
46
- state[tableName].pointsUpserted = 0;
47
- })
48
- .catch((err) => logger.error({ err, tableName }, "index build (scheduled) failed"))
49
- .finally(() => {
50
- s.pending = false;
51
- s.timer = undefined;
52
- });
53
- }, QUIET_MS);
54
- }
@@ -1 +0,0 @@
1
- export declare function requestIndexBuildOneTable(tableName: string): void;
@@ -1,4 +0,0 @@
1
- import { logger } from "../logging/logger.js";
2
- export function requestIndexBuildOneTable(tableName) {
3
- logger.info({ tableName }, "index build skipped (one_table mode: global vector index not supported)");
4
- }
@@ -1,3 +0,0 @@
1
- import type { DistanceKind, VectorType } from "../types";
2
- export declare function createCollectionMultiTable(metaKey: string, dim: number, distance: DistanceKind, vectorType: VectorType, tableName: string): Promise<void>;
3
- export declare function deleteCollectionMultiTable(metaKey: string, tableName: string): Promise<void>;
@@ -1,23 +0,0 @@
1
- import { Types, TypedValues, withSession, TableDescription, Column, } from "../ydb/client.js";
2
- import { upsertCollectionMeta } from "./collectionsRepo.shared.js";
3
- export async function createCollectionMultiTable(metaKey, dim, distance, vectorType, tableName) {
4
- await withSession(async (s) => {
5
- const desc = new TableDescription()
6
- .withColumns(new Column("point_id", Types.UTF8), new Column("embedding", Types.BYTES), new Column("payload", Types.JSON_DOCUMENT))
7
- .withPrimaryKey("point_id");
8
- await s.createTable(tableName, desc);
9
- });
10
- await upsertCollectionMeta(metaKey, dim, distance, vectorType, tableName);
11
- }
12
- export async function deleteCollectionMultiTable(metaKey, tableName) {
13
- await withSession(async (s) => {
14
- await s.dropTable(tableName);
15
- });
16
- const delMeta = `
17
- DECLARE $collection AS Utf8;
18
- DELETE FROM qdr__collections WHERE collection = $collection;
19
- `;
20
- await withSession(async (s) => {
21
- await s.executeQuery(delMeta, { $collection: TypedValues.utf8(metaKey) });
22
- });
23
- }
@@ -1,12 +0,0 @@
1
- import type { DistanceKind } from "../types";
2
- export declare function upsertPointsMultiTable(tableName: string, points: Array<{
3
- id: string | number;
4
- vector: number[];
5
- payload?: Record<string, unknown>;
6
- }>, dimension: number): Promise<number>;
7
- export declare function searchPointsMultiTable(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number): Promise<Array<{
8
- id: string;
9
- score: number;
10
- payload?: Record<string, unknown>;
11
- }>>;
12
- export declare function deletePointsMultiTable(tableName: string, ids: Array<string | number>): Promise<number>;
@@ -1,144 +0,0 @@
1
- import { Types, TypedValues, withSession } from "../ydb/client.js";
2
- import { buildVectorParam } from "../ydb/helpers.js";
3
- import { logger } from "../logging/logger.js";
4
- import { notifyUpsert } from "../indexing/IndexScheduler.js";
5
- import { VECTOR_INDEX_BUILD_ENABLED } from "../config/env.js";
6
- import { mapDistanceToKnnFn } from "../utils/distance.js";
7
- import { withRetry, isTransientYdbError } from "../utils/retry.js";
8
- import { UPSERT_BATCH_SIZE } from "../ydb/schema.js";
9
- export async function upsertPointsMultiTable(tableName, points, dimension) {
10
- for (const p of points) {
11
- const id = String(p.id);
12
- if (p.vector.length !== dimension) {
13
- throw new Error(`Vector dimension mismatch for id=${id}: got ${p.vector.length}, expected ${dimension}`);
14
- }
15
- }
16
- let upserted = 0;
17
- await withSession(async (s) => {
18
- for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
19
- const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
20
- const ddl = `
21
- DECLARE $rows AS List<Struct<
22
- point_id: Utf8,
23
- vec: List<Float>,
24
- payload: JsonDocument
25
- >>;
26
-
27
- UPSERT INTO ${tableName} (point_id, embedding, payload)
28
- SELECT
29
- point_id,
30
- Untag(Knn::ToBinaryStringFloat(vec), "FloatVector") AS embedding,
31
- payload
32
- FROM AS_TABLE($rows);
33
- `;
34
- const rowType = Types.struct({
35
- point_id: Types.UTF8,
36
- vec: Types.list(Types.FLOAT),
37
- payload: Types.JSON_DOCUMENT,
38
- });
39
- const rowsValue = TypedValues.list(rowType, batch.map((p) => ({
40
- point_id: String(p.id),
41
- vec: p.vector,
42
- payload: JSON.stringify(p.payload ?? {}),
43
- })));
44
- const params = {
45
- $rows: rowsValue,
46
- };
47
- await withRetry(() => s.executeQuery(ddl, params), {
48
- isTransient: isTransientYdbError,
49
- context: { tableName, batchSize: batch.length },
50
- });
51
- upserted += batch.length;
52
- }
53
- });
54
- notifyUpsert(tableName, upserted);
55
- return upserted;
56
- }
57
- export async function searchPointsMultiTable(tableName, queryVector, top, withPayload, distance, dimension) {
58
- if (queryVector.length !== dimension) {
59
- throw new Error(`Vector dimension mismatch: got ${queryVector.length}, expected ${dimension}`);
60
- }
61
- const { fn, order } = mapDistanceToKnnFn(distance);
62
- const qf = buildVectorParam(queryVector);
63
- const params = {
64
- $qf: qf,
65
- $k2: TypedValues.uint32(top),
66
- };
67
- const buildQuery = (useIndex) => `
68
- DECLARE $qf AS List<Float>;
69
- DECLARE $k2 AS Uint32;
70
- $qbinf = Knn::ToBinaryStringFloat($qf);
71
- SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
72
- FROM ${tableName}${useIndex ? " VIEW emb_idx" : ""}
73
- ORDER BY score ${order}
74
- LIMIT $k2;
75
- `;
76
- let rs;
77
- if (VECTOR_INDEX_BUILD_ENABLED) {
78
- try {
79
- rs = await withSession(async (s) => {
80
- return await s.executeQuery(buildQuery(true), params);
81
- });
82
- logger.info({ tableName }, "vector index found; using index for search");
83
- }
84
- catch (e) {
85
- const msg = e instanceof Error ? e.message : String(e);
86
- const indexUnavailable = /not found|does not exist|no such index|no global index|is not ready to use/i.test(msg);
87
- if (indexUnavailable) {
88
- logger.info({ tableName }, "vector index not available (missing or building); falling back to table scan");
89
- rs = await withSession(async (s) => {
90
- return await s.executeQuery(buildQuery(false), params);
91
- });
92
- }
93
- else {
94
- throw e;
95
- }
96
- }
97
- }
98
- else {
99
- rs = await withSession(async (s) => {
100
- return await s.executeQuery(buildQuery(false), params);
101
- });
102
- }
103
- const rowset = rs.resultSets?.[0];
104
- const rows = (rowset?.rows ?? []);
105
- return rows.map((row) => {
106
- const id = row.items?.[0]?.textValue;
107
- if (typeof id !== "string") {
108
- throw new Error("point_id is missing in YDB search result");
109
- }
110
- let payload;
111
- let scoreIdx = 1;
112
- if (withPayload) {
113
- const payloadText = row.items?.[1]?.textValue;
114
- if (payloadText) {
115
- try {
116
- payload = JSON.parse(payloadText);
117
- }
118
- catch {
119
- payload = undefined;
120
- }
121
- }
122
- scoreIdx = 2;
123
- }
124
- const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
125
- return { id, score, ...(payload ? { payload } : {}) };
126
- });
127
- }
128
- export async function deletePointsMultiTable(tableName, ids) {
129
- let deleted = 0;
130
- await withSession(async (s) => {
131
- for (const id of ids) {
132
- const yql = `
133
- DECLARE $id AS Utf8;
134
- DELETE FROM ${tableName} WHERE point_id = $id;
135
- `;
136
- const params = {
137
- $id: TypedValues.utf8(String(id)),
138
- };
139
- await s.executeQuery(yql, params);
140
- deleted += 1;
141
- }
142
- });
143
- return deleted;
144
- }
@@ -1,5 +0,0 @@
1
- import { type NormalizedCollectionContextLike } from "./CollectionService.shared.js";
2
- export declare function resolvePointsTableAndUidMultiTable(ctx: NormalizedCollectionContextLike): {
3
- tableName: string;
4
- uid: string | undefined;
5
- };
@@ -1,7 +0,0 @@
1
- import { tableNameFor, } from "./CollectionService.shared.js";
2
- export function resolvePointsTableAndUidMultiTable(ctx) {
3
- return {
4
- tableName: tableNameFor(ctx.tenant, ctx.collection),
5
- uid: undefined,
6
- };
7
- }