ydb-qdrant 7.0.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +2 -2
  2. package/dist/config/env.d.ts +0 -8
  3. package/dist/config/env.js +2 -29
  4. package/dist/package/api.d.ts +5 -2
  5. package/dist/package/api.js +2 -2
  6. package/dist/qdrant/QdrantRestTypes.d.ts +35 -0
  7. package/dist/repositories/collectionsRepo.d.ts +1 -2
  8. package/dist/repositories/collectionsRepo.js +62 -103
  9. package/dist/repositories/collectionsRepo.one-table.js +103 -47
  10. package/dist/repositories/collectionsRepo.shared.d.ts +2 -0
  11. package/dist/repositories/collectionsRepo.shared.js +32 -0
  12. package/dist/repositories/pointsRepo.d.ts +4 -8
  13. package/dist/repositories/pointsRepo.one-table/Delete.js +122 -67
  14. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +5 -2
  15. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +7 -6
  16. package/dist/repositories/pointsRepo.one-table/Search.d.ts +4 -0
  17. package/dist/repositories/pointsRepo.one-table/Search.js +208 -0
  18. package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
  19. package/dist/repositories/pointsRepo.one-table/Upsert.js +51 -66
  20. package/dist/repositories/pointsRepo.one-table.d.ts +1 -1
  21. package/dist/repositories/pointsRepo.one-table.js +1 -1
  22. package/dist/routes/collections.js +7 -61
  23. package/dist/routes/points.js +11 -66
  24. package/dist/services/PointsService.d.ts +3 -8
  25. package/dist/services/PointsService.js +19 -23
  26. package/dist/types.d.ts +23 -33
  27. package/dist/types.js +18 -20
  28. package/dist/utils/normalization.js +13 -14
  29. package/dist/utils/retry.js +19 -29
  30. package/dist/utils/vectorBinary.js +10 -5
  31. package/dist/ydb/bootstrapMetaTable.d.ts +7 -0
  32. package/dist/ydb/bootstrapMetaTable.js +75 -0
  33. package/dist/ydb/client.d.ts +23 -17
  34. package/dist/ydb/client.js +82 -423
  35. package/dist/ydb/schema.js +88 -148
  36. package/package.json +2 -10
  37. package/dist/qdrant/QdrantTypes.d.ts +0 -19
  38. package/dist/repositories/pointsRepo.one-table/Search/Approximate.d.ts +0 -18
  39. package/dist/repositories/pointsRepo.one-table/Search/Approximate.js +0 -119
  40. package/dist/repositories/pointsRepo.one-table/Search/Exact.d.ts +0 -17
  41. package/dist/repositories/pointsRepo.one-table/Search/Exact.js +0 -101
  42. package/dist/repositories/pointsRepo.one-table/Search/index.d.ts +0 -8
  43. package/dist/repositories/pointsRepo.one-table/Search/index.js +0 -30
  44. package/dist/utils/typeGuards.d.ts +0 -1
  45. package/dist/utils/typeGuards.js +0 -3
  46. package/dist/ydb/QueryDiagnostics.d.ts +0 -6
  47. package/dist/ydb/QueryDiagnostics.js +0 -52
  48. package/dist/ydb/SessionPool.d.ts +0 -36
  49. package/dist/ydb/SessionPool.js +0 -248
  50. package/dist/ydb/bulkUpsert.d.ts +0 -6
  51. package/dist/ydb/bulkUpsert.js +0 -52
  52. /package/dist/qdrant/{QdrantTypes.js → QdrantRestTypes.js} +0 -0
package/README.md CHANGED
@@ -37,7 +37,7 @@ Architecture diagrams: [docs page](http://ydb-qdrant.tech/docs/)
37
37
  - **Evaluation, CI, and release process**: [docs/evaluation-and-ci.md](docs/evaluation-and-ci.md)
38
38
 
39
39
  ## Requirements
40
- - Node.js >=20.19.0
40
+ - Node.js 18+
41
41
  - A YDB endpoint and database path
42
42
  - One of the supported auth methods (via environment)
43
43
 
@@ -54,7 +54,7 @@ npm install
54
54
  ```
55
55
 
56
56
  ## Configure credentials
57
- The server resolves credentials via `@ydbjs/auth` (plus a service-account key-file provider) and supports these env vars (first match wins):
57
+ The server uses `getCredentialsFromEnv()` and supports these env vars (first match wins):
58
58
 
59
59
  - Service account key file (recommended)
60
60
  ```bash
@@ -3,14 +3,6 @@ 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 QueryStatsMode {
7
- None = "none",
8
- Basic = "basic",
9
- Full = "full",
10
- Profile = "profile"
11
- }
12
- export declare const QUERY_STATS_MODE: QueryStatsMode;
13
- export declare const QUERY_RETRY_LOG_ENABLED: boolean;
14
6
  export declare enum SearchMode {
15
7
  Exact = "exact",
16
8
  Approximate = "approximate"
@@ -1,5 +1,4 @@
1
1
  import "dotenv/config";
2
- import { z } from "zod";
3
2
  export const YDB_ENDPOINT = process.env.YDB_ENDPOINT ?? "";
4
3
  export const YDB_DATABASE = process.env.YDB_DATABASE ?? "";
5
4
  export const PORT = process.env.PORT ? Number(process.env.PORT) : 8080;
@@ -21,32 +20,6 @@ function parseIntegerEnv(value, defaultValue, opts) {
21
20
  }
22
21
  return result;
23
22
  }
24
- function parseBooleanEnv(value, defaultValue) {
25
- if (value === undefined) {
26
- return defaultValue;
27
- }
28
- const normalized = value.trim().toLowerCase();
29
- if (normalized === "" ||
30
- normalized === "0" ||
31
- normalized === "false" ||
32
- normalized === "no" ||
33
- normalized === "off") {
34
- return false;
35
- }
36
- return true;
37
- }
38
- export var QueryStatsMode;
39
- (function (QueryStatsMode) {
40
- QueryStatsMode["None"] = "none";
41
- QueryStatsMode["Basic"] = "basic";
42
- QueryStatsMode["Full"] = "full";
43
- QueryStatsMode["Profile"] = "profile";
44
- })(QueryStatsMode || (QueryStatsMode = {}));
45
- const QueryStatsModeSchema = z
46
- .nativeEnum(QueryStatsMode)
47
- .catch(QueryStatsMode.None);
48
- export const QUERY_STATS_MODE = QueryStatsModeSchema.parse(process.env.YDB_QDRANT_QUERY_STATS_MODE?.trim().toLowerCase());
49
- export const QUERY_RETRY_LOG_ENABLED = parseBooleanEnv(process.env.YDB_QDRANT_QUERY_RETRY_LOG, false);
50
23
  export var SearchMode;
51
24
  (function (SearchMode) {
52
25
  SearchMode["Exact"] = "exact";
@@ -79,6 +52,6 @@ export const SESSION_POOL_MIN_SIZE = NORMALIZED_SESSION_POOL_MIN_SIZE;
79
52
  export const SESSION_POOL_MAX_SIZE = RAW_SESSION_POOL_MAX_SIZE;
80
53
  export const SESSION_KEEPALIVE_PERIOD_MS = parseIntegerEnv(process.env.YDB_SESSION_KEEPALIVE_PERIOD_MS, 5000, { min: 1000, max: 60000 });
81
54
  export const STARTUP_PROBE_SESSION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_STARTUP_PROBE_SESSION_TIMEOUT_MS, 5000, { min: 1000 });
82
- export const UPSERT_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_TIMEOUT_MS, 20000, { min: 1000 });
83
- export const SEARCH_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_SEARCH_TIMEOUT_MS, 20000, { min: 1000 });
55
+ export const UPSERT_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_TIMEOUT_MS, 5000, { min: 1000 });
56
+ export const SEARCH_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_SEARCH_TIMEOUT_MS, 10000, { min: 1000 });
84
57
  export const LAST_ACCESS_MIN_WRITE_INTERVAL_MS = parseIntegerEnv(process.env.YDB_QDRANT_LAST_ACCESS_MIN_WRITE_INTERVAL_MS, 60000, { min: 1000 });
@@ -1,8 +1,9 @@
1
- import type { CredentialsProvider } from "@ydbjs/auth";
1
+ import type { IAuthService } from "ydb-sdk";
2
2
  import { createCollection as serviceCreateCollection, deleteCollection as serviceDeleteCollection, getCollection as serviceGetCollection, putCollectionIndex as servicePutCollectionIndex } from "../services/CollectionService.js";
3
3
  import { upsertPoints as serviceUpsertPoints, searchPoints as serviceSearchPoints, deletePoints as serviceDeletePoints } from "../services/PointsService.js";
4
4
  export { QdrantServiceError } from "../services/errors.js";
5
5
  export { CreateCollectionReq, UpsertPointsReq, SearchReq, DeletePointsReq, } from "../types.js";
6
+ export type { UpsertPointsBody, SearchPointsBody } from "../types.js";
6
7
  type CreateCollectionResult = Awaited<ReturnType<typeof serviceCreateCollection>>;
7
8
  type GetCollectionResult = Awaited<ReturnType<typeof serviceGetCollection>>;
8
9
  type DeleteCollectionResult = Awaited<ReturnType<typeof serviceDeleteCollection>>;
@@ -15,14 +16,16 @@ export interface YdbQdrantClientOptions {
15
16
  endpoint?: string;
16
17
  database?: string;
17
18
  connectionString?: string;
18
- credentialsProvider?: CredentialsProvider;
19
+ authService?: IAuthService;
19
20
  }
20
21
  export interface YdbQdrantTenantClient {
21
22
  createCollection(collection: string, body: unknown): Promise<CreateCollectionResult>;
22
23
  getCollection(collection: string): Promise<GetCollectionResult>;
23
24
  deleteCollection(collection: string): Promise<DeleteCollectionResult>;
24
25
  putCollectionIndex(collection: string): Promise<PutIndexResult>;
26
+ upsertPoints(collection: string, body: import("../types.js").UpsertPointsBody): Promise<UpsertPointsResult>;
25
27
  upsertPoints(collection: string, body: unknown): Promise<UpsertPointsResult>;
28
+ searchPoints(collection: string, body: import("../types.js").SearchPointsBody): Promise<SearchPointsResult>;
26
29
  searchPoints(collection: string, body: unknown): Promise<SearchPointsResult>;
27
30
  deletePoints(collection: string, body: unknown): Promise<DeletePointsResult>;
28
31
  }
@@ -33,12 +33,12 @@ export async function createYdbQdrantClient(options = {}) {
33
33
  if (options.endpoint !== undefined ||
34
34
  options.database !== undefined ||
35
35
  options.connectionString !== undefined ||
36
- options.credentialsProvider !== undefined) {
36
+ options.authService !== undefined) {
37
37
  configureDriver({
38
38
  endpoint: options.endpoint,
39
39
  database: options.database,
40
40
  connectionString: options.connectionString,
41
- credentialsProvider: options.credentialsProvider,
41
+ authService: options.authService,
42
42
  });
43
43
  }
44
44
  await readyOrThrow();
@@ -0,0 +1,35 @@
1
+ import type { Schemas } from "@qdrant/js-client-rest";
2
+ /**
3
+ * Type-only surface derived from Qdrant's official REST OpenAPI schema.
4
+ *
5
+ * Important:
6
+ * - This module must remain type-only (no runtime imports/exports from @qdrant/*).
7
+ * - Prefer referencing these aliases across the codebase to keep our shapes aligned
8
+ * with upstream Qdrant where appropriate, while preserving our runtime behavior.
9
+ */
10
+ export type QdrantSchemas = Schemas;
11
+ export type QdrantDistance = Schemas["Distance"];
12
+ export type QdrantExtendedPointId = Schemas["ExtendedPointId"];
13
+ export type QdrantPayload = Schemas["Payload"];
14
+ export type QdrantFilter = Schemas["Filter"];
15
+ export type QdrantPointsSelector = Schemas["PointsSelector"];
16
+ export type QdrantPointStruct = Schemas["PointStruct"];
17
+ export type QdrantSearchRequest = Schemas["SearchRequest"];
18
+ export type QdrantScoredPoint = Schemas["ScoredPoint"];
19
+ export type QdrantWithPayloadInterface = Schemas["WithPayloadInterface"];
20
+ /**
21
+ * Project-specific narrowing for our Qdrant-compatible subset.
22
+ * These types are intentionally tighter than Qdrant's full schema.
23
+ */
24
+ export type YdbQdrantPointId = Extract<QdrantExtendedPointId, string | number>;
25
+ export type YdbQdrantVector = number[];
26
+ export type YdbQdrantUpsertPoint = Omit<QdrantPointStruct, "id" | "vector" | "payload"> & {
27
+ id: YdbQdrantPointId;
28
+ vector: YdbQdrantVector;
29
+ payload?: QdrantPayload;
30
+ };
31
+ export type YdbQdrantScoredPoint = {
32
+ id: string;
33
+ score: QdrantScoredPoint["score"];
34
+ payload?: QdrantPayload;
35
+ };
@@ -1,5 +1,4 @@
1
- import { type CollectionMeta, type DistanceKind, type VectorType } from "../types.js";
2
- export declare function __resetCachesForTests(): void;
1
+ import type { DistanceKind, VectorType, CollectionMeta } from "../types";
3
2
  export declare function createCollection(metaKey: string, dim: number, distance: DistanceKind, vectorType: VectorType): Promise<void>;
4
3
  export declare function getCollectionMeta(metaKey: string): Promise<CollectionMeta | null>;
5
4
  export declare function verifyCollectionsQueryCompilationForStartup(): Promise<void>;
@@ -1,50 +1,11 @@
1
- import { withSession, withStartupProbeSession } from "../ydb/client.js";
2
- import { STARTUP_PROBE_SESSION_TIMEOUT_MS, UPSERT_OPERATION_TIMEOUT_MS, LAST_ACCESS_MIN_WRITE_INTERVAL_MS, } from "../config/env.js";
1
+ import { TypedValues, withSession, createExecuteQuerySettings, withStartupProbeSession, createExecuteQuerySettingsWithTimeout, } from "../ydb/client.js";
2
+ import { STARTUP_PROBE_SESSION_TIMEOUT_MS, LAST_ACCESS_MIN_WRITE_INTERVAL_MS, } from "../config/env.js";
3
3
  import { logger } from "../logging/logger.js";
4
4
  import { uidFor } from "../utils/tenant.js";
5
- import { attachQueryDiagnostics } from "../ydb/QueryDiagnostics.js";
6
- import { withRetry, isTransientYdbError } from "../utils/retry.js";
7
5
  import { createCollectionOneTable, deleteCollectionOneTable, } from "./collectionsRepo.one-table.js";
8
- import { Timestamp, Utf8 } from "@ydbjs/value/primitive";
9
- import { DistanceKindSchema, VectorTypeSchema, } from "../types.js";
6
+ import { withRetry, isTransientYdbError } from "../utils/retry.js";
10
7
  const lastAccessWriteCache = new Map();
11
8
  const LAST_ACCESS_CACHE_MAX_SIZE = 10000;
12
- const collectionMetaCache = new Map();
13
- const COLLECTION_META_CACHE_MAX_SIZE = 10000;
14
- // Meta lookups are on the hot path for *every* request (upsert/search/delete).
15
- // Under sustained ingestion, YDB can get busy and even a single-row SELECT can stall.
16
- // Keep meta cached long enough to avoid turning that into a constant DB read load.
17
- const COLLECTION_META_CACHE_TTL_MS = 5 * 60_000;
18
- function evictOldestCollectionMetaEntry() {
19
- if (collectionMetaCache.size < COLLECTION_META_CACHE_MAX_SIZE) {
20
- return;
21
- }
22
- const oldestKey = collectionMetaCache.keys().next().value;
23
- if (oldestKey !== undefined) {
24
- collectionMetaCache.delete(oldestKey);
25
- }
26
- }
27
- function getCachedCollectionMeta(metaKey) {
28
- const entry = collectionMetaCache.get(metaKey);
29
- if (!entry)
30
- return null;
31
- if (Date.now() >= entry.expiresAtMs) {
32
- collectionMetaCache.delete(metaKey);
33
- return null;
34
- }
35
- return entry.meta;
36
- }
37
- function setCachedCollectionMeta(metaKey, meta) {
38
- evictOldestCollectionMetaEntry();
39
- collectionMetaCache.set(metaKey, {
40
- meta,
41
- expiresAtMs: Date.now() + COLLECTION_META_CACHE_TTL_MS,
42
- });
43
- }
44
- // Test-only: keep repository unit tests isolated since this module maintains in-memory caches.
45
- export function __resetCachesForTests() {
46
- collectionMetaCache.clear();
47
- }
48
9
  function evictOldestLastAccessEntry() {
49
10
  if (lastAccessWriteCache.size < LAST_ACCESS_CACHE_MAX_SIZE) {
50
11
  return;
@@ -63,39 +24,34 @@ function shouldWriteLastAccess(nowMs, key) {
63
24
  }
64
25
  export async function createCollection(metaKey, dim, distance, vectorType) {
65
26
  await createCollectionOneTable(metaKey, dim, distance, vectorType);
66
- collectionMetaCache.delete(metaKey);
67
27
  }
68
28
  export async function getCollectionMeta(metaKey) {
69
- const cached = getCachedCollectionMeta(metaKey);
70
- if (cached) {
71
- return cached;
72
- }
73
- const [rows] = await withSession(async (sql, signal) => {
74
- const q = attachQueryDiagnostics(sql `
75
- SELECT
76
- table_name,
77
- vector_dimension,
78
- distance,
79
- vector_type,
80
- CAST(last_accessed_at AS Utf8) AS last_accessed_at
81
- FROM qdr__collections
82
- WHERE collection = $collection;
83
- `, { operation: "getCollectionMeta", metaKey })
84
- .idempotent(true)
85
- // Collection metadata is required for upserts as well; use the more forgiving timeout.
86
- .timeout(UPSERT_OPERATION_TIMEOUT_MS)
87
- .signal(signal)
88
- .parameter("collection", new Utf8(metaKey));
89
- return await q;
29
+ const qry = `
30
+ DECLARE $collection AS Utf8;
31
+ SELECT
32
+ table_name,
33
+ vector_dimension,
34
+ distance,
35
+ vector_type,
36
+ CAST(last_accessed_at AS Utf8) AS last_accessed_at
37
+ FROM qdr__collections
38
+ WHERE collection = $collection;
39
+ `;
40
+ const res = await withSession(async (s) => {
41
+ const settings = createExecuteQuerySettings();
42
+ return await s.executeQuery(qry, {
43
+ $collection: TypedValues.utf8(metaKey),
44
+ }, undefined, settings);
90
45
  });
91
- if (rows.length !== 1)
46
+ const rowset = res.resultSets?.[0];
47
+ if (!rowset || rowset.rows?.length !== 1)
92
48
  return null;
93
- const row = rows[0];
94
- const table = row.table_name;
95
- const dimension = Number(row.vector_dimension);
96
- const distance = DistanceKindSchema.catch("Cosine").parse(row.distance);
97
- const vectorType = VectorTypeSchema.catch("float").parse(row.vector_type);
98
- const lastAccessRaw = row.last_accessed_at;
49
+ const row = rowset.rows[0];
50
+ const table = row.items?.[0]?.textValue;
51
+ const dimension = Number(row.items?.[1]?.uint32Value ?? row.items?.[1]?.textValue);
52
+ const distance = row.items?.[2]?.textValue ?? "Cosine";
53
+ const vectorType = row.items?.[3]?.textValue ?? "float";
54
+ const lastAccessRaw = row.items?.[4]?.textValue;
99
55
  const lastAccessedAt = typeof lastAccessRaw === "string" && lastAccessRaw.length > 0
100
56
  ? new Date(lastAccessRaw)
101
57
  : undefined;
@@ -108,35 +64,37 @@ export async function getCollectionMeta(metaKey) {
108
64
  if (lastAccessedAt) {
109
65
  result.lastAccessedAt = lastAccessedAt;
110
66
  }
111
- setCachedCollectionMeta(metaKey, result);
112
67
  return result;
113
68
  }
114
69
  export async function verifyCollectionsQueryCompilationForStartup() {
115
70
  const probeKey = "__startup_probe__/__startup_probe__";
71
+ const qry = `
72
+ DECLARE $collection AS Utf8;
73
+ SELECT
74
+ table_name,
75
+ vector_dimension,
76
+ distance,
77
+ vector_type,
78
+ CAST(last_accessed_at AS Utf8) AS last_accessed_at
79
+ FROM qdr__collections
80
+ WHERE collection = $collection;
81
+ `;
116
82
  await withRetry(async () => {
117
- await withStartupProbeSession(async (sql, signal) => {
118
- await sql `
119
- SELECT
120
- table_name,
121
- vector_dimension,
122
- distance,
123
- vector_type,
124
- CAST(last_accessed_at AS Utf8) AS last_accessed_at
125
- FROM qdr__collections
126
- WHERE collection = $collection;
127
- `
128
- .idempotent(true)
129
- .timeout(STARTUP_PROBE_SESSION_TIMEOUT_MS)
130
- .signal(signal)
131
- .parameter("collection", new Utf8(probeKey));
83
+ await withStartupProbeSession(async (s) => {
84
+ const settings = createExecuteQuerySettingsWithTimeout({
85
+ keepInCache: true,
86
+ idempotent: true,
87
+ timeoutMs: STARTUP_PROBE_SESSION_TIMEOUT_MS,
88
+ });
89
+ await s.executeQuery(qry, {
90
+ $collection: TypedValues.utf8(probeKey),
91
+ }, undefined, settings);
132
92
  });
133
93
  }, {
94
+ isTransient: isTransientYdbError,
134
95
  maxRetries: 2,
135
96
  baseDelayMs: 200,
136
- isTransient: isTransientYdbError,
137
- context: {
138
- operation: "verifyCollectionsQueryCompilationForStartup",
139
- },
97
+ context: { probe: "collections_startup_compilation" },
140
98
  });
141
99
  }
142
100
  export async function deleteCollection(metaKey, uid) {
@@ -152,7 +110,6 @@ export async function deleteCollection(metaKey, uid) {
152
110
  effectiveUid = uidFor(tenant, collection);
153
111
  }
154
112
  await deleteCollectionOneTable(metaKey, effectiveUid);
155
- collectionMetaCache.delete(metaKey);
156
113
  }
157
114
  /**
158
115
  * Best-effort metadata update for a collection's last_accessed_at timestamp.
@@ -167,18 +124,20 @@ export async function touchCollectionLastAccess(metaKey, now = new Date()) {
167
124
  if (!shouldWriteLastAccess(nowMs, metaKey)) {
168
125
  return;
169
126
  }
127
+ const qry = `
128
+ DECLARE $collection AS Utf8;
129
+ DECLARE $last_accessed AS Timestamp;
130
+ UPDATE qdr__collections
131
+ SET last_accessed_at = $last_accessed
132
+ WHERE collection = $collection;
133
+ `;
170
134
  try {
171
- await withSession(async (sql, signal) => {
172
- await sql `
173
- UPDATE qdr__collections
174
- SET last_accessed_at = $last_accessed
175
- WHERE collection = $collection;
176
- `
177
- .idempotent(true)
178
- .timeout(UPSERT_OPERATION_TIMEOUT_MS)
179
- .signal(signal)
180
- .parameter("collection", new Utf8(metaKey))
181
- .parameter("last_accessed", new Timestamp(now));
135
+ await withSession(async (s) => {
136
+ const settings = createExecuteQuerySettings();
137
+ await s.executeQuery(qry, {
138
+ $collection: TypedValues.utf8(metaKey),
139
+ $last_accessed: TypedValues.timestamp(now),
140
+ }, undefined, settings);
182
141
  });
183
142
  evictOldestLastAccessEntry();
184
143
  lastAccessWriteCache.set(metaKey, nowMs);
@@ -1,57 +1,113 @@
1
- import { withSession } from "../ydb/client.js";
1
+ import { TypedValues, Types, withSession, withQuerySession, createExecuteQuerySettings, } from "../ydb/client.js";
2
2
  import { GLOBAL_POINTS_TABLE, ensureGlobalPointsTable } from "../ydb/schema.js";
3
- import { UPSERT_OPERATION_TIMEOUT_MS } from "../config/env.js";
4
- import { Timestamp, Uint32, Utf8 } from "@ydbjs/value/primitive";
5
- async function upsertCollectionMeta(metaKey, dim, distance, vectorType, tableName) {
6
- const now = new Date();
7
- await withSession(async (sql, signal) => {
8
- await sql `
9
- UPSERT INTO qdr__collections (
10
- collection,
11
- table_name,
12
- vector_dimension,
13
- distance,
14
- vector_type,
15
- created_at,
16
- last_accessed_at
17
- )
18
- VALUES ($collection, $table, $dim, $distance, $vtype, $created, $last_accessed);
19
- `
20
- .idempotent(true)
21
- .timeout(UPSERT_OPERATION_TIMEOUT_MS)
22
- .signal(signal)
23
- .parameter("collection", new Utf8(metaKey))
24
- .parameter("table", new Utf8(tableName))
25
- .parameter("dim", new Uint32(dim))
26
- .parameter("distance", new Utf8(distance))
27
- .parameter("vtype", new Utf8(vectorType))
28
- .parameter("created", new Timestamp(now))
29
- .parameter("last_accessed", new Timestamp(now));
30
- });
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 = 10000;
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
+ }
31
60
  }
32
61
  export async function createCollectionOneTable(metaKey, dim, distance, vectorType) {
33
62
  await upsertCollectionMeta(metaKey, dim, distance, vectorType, GLOBAL_POINTS_TABLE);
34
63
  }
35
64
  export async function deleteCollectionOneTable(metaKey, uid) {
36
65
  await ensureGlobalPointsTable();
37
- await withSession(async (sql, signal) => {
38
- await sql `
39
- BATCH DELETE FROM ${sql.identifier(GLOBAL_POINTS_TABLE)}
40
- WHERE uid = $uid;
41
- `
42
- .idempotent(true)
43
- .timeout(UPSERT_OPERATION_TIMEOUT_MS)
44
- .signal(signal)
45
- .parameter("uid", new Utf8(uid));
66
+ const batchDeletePointsYql = `
67
+ DECLARE $uid AS Utf8;
68
+ BATCH DELETE FROM ${GLOBAL_POINTS_TABLE}
69
+ WHERE uid = $uid;
70
+ `;
71
+ await withRetry(async () => {
72
+ try {
73
+ await withQuerySession(async (qs) => {
74
+ await qs.execute({
75
+ text: batchDeletePointsYql,
76
+ parameters: {
77
+ $uid: TypedValues.utf8(uid),
78
+ },
79
+ });
80
+ });
81
+ }
82
+ catch (err) {
83
+ if (!isOutOfBufferMemoryYdbError(err)) {
84
+ throw err;
85
+ }
86
+ // BATCH DELETE already deletes in chunks per partition, but if YDB
87
+ // still reports an out-of-buffer-memory condition, fall back to
88
+ // per-uid chunked deletion strategy to complete the deletion.
89
+ await withSession(async (s) => {
90
+ await deletePointsForUidInChunks(s, uid);
91
+ });
92
+ }
93
+ }, {
94
+ isTransient: isTransientYdbError,
95
+ context: {
96
+ operation: "deleteCollectionOneTable",
97
+ tableName: GLOBAL_POINTS_TABLE,
98
+ metaKey,
99
+ uid,
100
+ mode: "batch_delete",
101
+ },
46
102
  });
47
- await withSession(async (sql, signal) => {
48
- await sql `
49
- DELETE FROM qdr__collections
50
- WHERE collection = $collection;
51
- `
52
- .idempotent(true)
53
- .timeout(UPSERT_OPERATION_TIMEOUT_MS)
54
- .signal(signal)
55
- .parameter("collection", new Utf8(metaKey));
103
+ const delMeta = `
104
+ DECLARE $collection AS Utf8;
105
+ DELETE FROM qdr__collections WHERE collection = $collection;
106
+ `;
107
+ await withSession(async (s) => {
108
+ const settings = createExecuteQuerySettings();
109
+ await s.executeQuery(delMeta, {
110
+ $collection: TypedValues.utf8(metaKey),
111
+ }, undefined, settings);
56
112
  });
57
113
  }
@@ -0,0 +1,2 @@
1
+ import type { DistanceKind, VectorType } from "../types";
2
+ export declare function upsertCollectionMeta(metaKey: string, dim: number, distance: DistanceKind, vectorType: VectorType, tableName: string): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { UPSERT_OPERATION_TIMEOUT_MS } from "../config/env.js";
2
+ import { TypedValues, withSession, createExecuteQuerySettingsWithTimeout, } from "../ydb/client.js";
3
+ export async function upsertCollectionMeta(metaKey, dim, distance, vectorType, tableName) {
4
+ const now = new Date();
5
+ const upsertMeta = `
6
+ DECLARE $collection AS Utf8;
7
+ DECLARE $table AS Utf8;
8
+ DECLARE $dim AS Uint32;
9
+ DECLARE $distance AS Utf8;
10
+ DECLARE $vtype AS Utf8;
11
+ DECLARE $created AS Timestamp;
12
+ DECLARE $last_accessed AS Timestamp;
13
+ UPSERT INTO qdr__collections (collection, table_name, vector_dimension, distance, vector_type, created_at, last_accessed_at)
14
+ VALUES ($collection, $table, $dim, $distance, $vtype, $created, $last_accessed);
15
+ `;
16
+ await withSession(async (s) => {
17
+ const settings = createExecuteQuerySettingsWithTimeout({
18
+ keepInCache: true,
19
+ idempotent: true,
20
+ timeoutMs: UPSERT_OPERATION_TIMEOUT_MS,
21
+ });
22
+ await s.executeQuery(upsertMeta, {
23
+ $collection: TypedValues.utf8(metaKey),
24
+ $table: TypedValues.utf8(tableName),
25
+ $dim: TypedValues.uint32(dim),
26
+ $distance: TypedValues.utf8(distance),
27
+ $vtype: TypedValues.utf8(vectorType),
28
+ $created: TypedValues.timestamp(now),
29
+ $last_accessed: TypedValues.timestamp(now),
30
+ }, undefined, settings);
31
+ });
32
+ }
@@ -1,10 +1,6 @@
1
- import type { DistanceKind } from "../types";
2
- import type { QdrantPayload, QdrantPointStructDense } from "../qdrant/QdrantTypes.js";
3
- export declare function upsertPoints(tableName: string, points: QdrantPointStructDense[], dimension: number, uid: string): Promise<number>;
4
- export declare function searchPoints(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number, uid: string, filterPaths?: Array<Array<string>>): Promise<Array<{
5
- id: string;
6
- score: number;
7
- payload?: QdrantPayload;
8
- }>>;
1
+ import type { DistanceKind, UpsertPoint } from "../types.js";
2
+ import type { YdbQdrantScoredPoint } from "../qdrant/QdrantRestTypes.js";
3
+ export declare function upsertPoints(tableName: string, points: UpsertPoint[], dimension: number, uid: string): Promise<number>;
4
+ export declare function searchPoints(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number, uid: string, filterPaths?: Array<Array<string>>): Promise<YdbQdrantScoredPoint[]>;
9
5
  export declare function deletePoints(tableName: string, ids: Array<string | number>, uid: string): Promise<number>;
10
6
  export declare function deletePointsByPathSegments(tableName: string, uid: string, paths: Array<Array<string>>): Promise<number>;