ydb-qdrant 6.0.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +2 -2
  2. package/dist/config/env.d.ts +8 -3
  3. package/dist/config/env.js +15 -5
  4. package/dist/package/api.d.ts +2 -2
  5. package/dist/package/api.js +2 -2
  6. package/dist/qdrant/QdrantTypes.d.ts +19 -0
  7. package/dist/qdrant/QdrantTypes.js +1 -0
  8. package/dist/repositories/collectionsRepo.d.ts +2 -1
  9. package/dist/repositories/collectionsRepo.js +103 -62
  10. package/dist/repositories/collectionsRepo.one-table.js +47 -129
  11. package/dist/repositories/pointsRepo.d.ts +5 -7
  12. package/dist/repositories/pointsRepo.js +6 -3
  13. package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -0
  14. package/dist/repositories/pointsRepo.one-table/Delete.js +111 -0
  15. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +11 -0
  16. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +32 -0
  17. package/dist/repositories/pointsRepo.one-table/Search/Approximate.d.ts +18 -0
  18. package/dist/repositories/pointsRepo.one-table/Search/Approximate.js +119 -0
  19. package/dist/repositories/pointsRepo.one-table/Search/Exact.d.ts +17 -0
  20. package/dist/repositories/pointsRepo.one-table/Search/Exact.js +101 -0
  21. package/dist/repositories/pointsRepo.one-table/Search/index.d.ts +8 -0
  22. package/dist/repositories/pointsRepo.one-table/Search/index.js +30 -0
  23. package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -0
  24. package/dist/repositories/pointsRepo.one-table/Upsert.js +100 -0
  25. package/dist/repositories/pointsRepo.one-table.d.ts +3 -13
  26. package/dist/repositories/pointsRepo.one-table.js +3 -403
  27. package/dist/routes/collections.js +61 -7
  28. package/dist/routes/points.js +71 -3
  29. package/dist/server.d.ts +1 -0
  30. package/dist/server.js +70 -2
  31. package/dist/services/CollectionService.d.ts +9 -0
  32. package/dist/services/CollectionService.js +9 -0
  33. package/dist/services/PointsService.d.ts +8 -10
  34. package/dist/services/PointsService.js +78 -4
  35. package/dist/types.d.ts +72 -8
  36. package/dist/types.js +43 -17
  37. package/dist/utils/normalization.d.ts +1 -0
  38. package/dist/utils/normalization.js +15 -13
  39. package/dist/utils/retry.js +29 -19
  40. package/dist/utils/typeGuards.d.ts +1 -0
  41. package/dist/utils/typeGuards.js +3 -0
  42. package/dist/utils/vectorBinary.js +88 -9
  43. package/dist/ydb/QueryDiagnostics.d.ts +6 -0
  44. package/dist/ydb/QueryDiagnostics.js +52 -0
  45. package/dist/ydb/SessionPool.d.ts +36 -0
  46. package/dist/ydb/SessionPool.js +248 -0
  47. package/dist/ydb/bulkUpsert.d.ts +6 -0
  48. package/dist/ydb/bulkUpsert.js +52 -0
  49. package/dist/ydb/client.d.ts +17 -16
  50. package/dist/ydb/client.js +427 -62
  51. package/dist/ydb/helpers.d.ts +0 -2
  52. package/dist/ydb/helpers.js +0 -7
  53. package/dist/ydb/schema.js +171 -77
  54. package/package.json +12 -7
  55. package/dist/repositories/collectionsRepo.shared.d.ts +0 -2
  56. package/dist/repositories/collectionsRepo.shared.js +0 -26
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 18+
40
+ - Node.js >=20.19.0
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 uses `getCredentialsFromEnv()` and supports these env vars (first match wins):
57
+ The server resolves credentials via `@ydbjs/auth` (plus a service-account key-file provider) and supports these env vars (first match wins):
58
58
 
59
59
  - Service account key file (recommended)
60
60
  ```bash
@@ -3,8 +3,14 @@ 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 const GLOBAL_POINTS_AUTOMIGRATE_ENABLED: boolean;
7
- export declare const USE_BATCH_DELETE_FOR_COLLECTIONS: boolean;
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;
8
14
  export declare enum SearchMode {
9
15
  Exact = "exact",
10
16
  Approximate = "approximate"
@@ -12,7 +18,6 @@ export declare enum SearchMode {
12
18
  export declare function resolveSearchMode(raw: string | undefined): SearchMode;
13
19
  export declare const SEARCH_MODE: SearchMode;
14
20
  export declare const OVERFETCH_MULTIPLIER: number;
15
- export declare const CLIENT_SIDE_SERIALIZATION_ENABLED: boolean;
16
21
  export declare const UPSERT_BATCH_SIZE: number;
17
22
  export declare const SESSION_POOL_MIN_SIZE: number;
18
23
  export declare const SESSION_POOL_MAX_SIZE: number;
@@ -1,4 +1,5 @@
1
1
  import "dotenv/config";
2
+ import { z } from "zod";
2
3
  export const YDB_ENDPOINT = process.env.YDB_ENDPOINT ?? "";
3
4
  export const YDB_DATABASE = process.env.YDB_DATABASE ?? "";
4
5
  export const PORT = process.env.PORT ? Number(process.env.PORT) : 8080;
@@ -34,8 +35,18 @@ function parseBooleanEnv(value, defaultValue) {
34
35
  }
35
36
  return true;
36
37
  }
37
- export const GLOBAL_POINTS_AUTOMIGRATE_ENABLED = parseBooleanEnv(process.env.YDB_QDRANT_GLOBAL_POINTS_AUTOMIGRATE, false);
38
- export const USE_BATCH_DELETE_FOR_COLLECTIONS = parseBooleanEnv(process.env.YDB_QDRANT_USE_BATCH_DELETE, false);
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);
39
50
  export var SearchMode;
40
51
  (function (SearchMode) {
41
52
  SearchMode["Exact"] = "exact";
@@ -57,7 +68,6 @@ function resolveSearchModeEnv() {
57
68
  }
58
69
  export const SEARCH_MODE = resolveSearchModeEnv();
59
70
  export const OVERFETCH_MULTIPLIER = parseIntegerEnv(process.env.YDB_QDRANT_OVERFETCH_MULTIPLIER, 10, { min: 1 });
60
- export const CLIENT_SIDE_SERIALIZATION_ENABLED = parseBooleanEnv(process.env.YDB_QDRANT_CLIENT_SIDE_SERIALIZATION_ENABLED, false);
61
71
  export const UPSERT_BATCH_SIZE = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_BATCH_SIZE, 100, { min: 1 });
62
72
  // Session pool configuration
63
73
  const RAW_SESSION_POOL_MIN_SIZE = parseIntegerEnv(process.env.YDB_SESSION_POOL_MIN_SIZE, 5, { min: 1, max: 500 });
@@ -69,6 +79,6 @@ export const SESSION_POOL_MIN_SIZE = NORMALIZED_SESSION_POOL_MIN_SIZE;
69
79
  export const SESSION_POOL_MAX_SIZE = RAW_SESSION_POOL_MAX_SIZE;
70
80
  export const SESSION_KEEPALIVE_PERIOD_MS = parseIntegerEnv(process.env.YDB_SESSION_KEEPALIVE_PERIOD_MS, 5000, { min: 1000, max: 60000 });
71
81
  export const STARTUP_PROBE_SESSION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_STARTUP_PROBE_SESSION_TIMEOUT_MS, 5000, { min: 1000 });
72
- export const UPSERT_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_TIMEOUT_MS, 5000, { min: 1000 });
73
- export const SEARCH_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_SEARCH_TIMEOUT_MS, 10000, { 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 });
74
84
  export const LAST_ACCESS_MIN_WRITE_INTERVAL_MS = parseIntegerEnv(process.env.YDB_QDRANT_LAST_ACCESS_MIN_WRITE_INTERVAL_MS, 60000, { min: 1000 });
@@ -1,4 +1,4 @@
1
- import type { IAuthService } from "ydb-sdk";
1
+ import type { CredentialsProvider } from "@ydbjs/auth";
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";
@@ -15,7 +15,7 @@ export interface YdbQdrantClientOptions {
15
15
  endpoint?: string;
16
16
  database?: string;
17
17
  connectionString?: string;
18
- authService?: IAuthService;
18
+ credentialsProvider?: CredentialsProvider;
19
19
  }
20
20
  export interface YdbQdrantTenantClient {
21
21
  createCollection(collection: string, body: unknown): Promise<CreateCollectionResult>;
@@ -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.authService !== undefined) {
36
+ options.credentialsProvider !== undefined) {
37
37
  configureDriver({
38
38
  endpoint: options.endpoint,
39
39
  database: options.database,
40
40
  connectionString: options.connectionString,
41
- authService: options.authService,
41
+ credentialsProvider: options.credentialsProvider,
42
42
  });
43
43
  }
44
44
  await readyOrThrow();
@@ -0,0 +1,19 @@
1
+ import type { Schemas } from "@qdrant/js-client-rest";
2
+ /**
3
+ * Centralized Qdrant OpenAPI-derived types (via @qdrant/js-client-rest), narrowed to the
4
+ * subset of shapes that ydb-qdrant currently supports.
5
+ *
6
+ * Important:
7
+ * - Qdrant's schema types are intentionally broad (named vectors, multi-vectors, sparse vectors, inference objects).
8
+ * - Internally we support dense vectors only (`number[]`), so we narrow types accordingly.
9
+ */
10
+ export type QdrantDistance = Schemas["Distance"];
11
+ export type QdrantPointId = Schemas["ExtendedPointId"];
12
+ export type QdrantDenseVector = Extract<Schemas["VectorStruct"], number[]>;
13
+ export type QdrantPayload = Record<string, unknown>;
14
+ export type QdrantPointStructDense = Omit<Schemas["PointStruct"], "vector" | "payload"> & {
15
+ vector: QdrantDenseVector;
16
+ payload?: QdrantPayload;
17
+ };
18
+ export type QdrantScoredPoint = Schemas["ScoredPoint"];
19
+ export type QdrantQueryResponse = Schemas["QueryResponse"];
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
- import type { DistanceKind, VectorType, CollectionMeta } from "../types";
1
+ import { type CollectionMeta, type DistanceKind, type VectorType } from "../types.js";
2
+ export declare function __resetCachesForTests(): void;
2
3
  export declare function createCollection(metaKey: string, dim: number, distance: DistanceKind, vectorType: VectorType): Promise<void>;
3
4
  export declare function getCollectionMeta(metaKey: string): Promise<CollectionMeta | null>;
4
5
  export declare function verifyCollectionsQueryCompilationForStartup(): Promise<void>;
@@ -1,11 +1,50 @@
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";
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";
3
3
  import { logger } from "../logging/logger.js";
4
4
  import { uidFor } from "../utils/tenant.js";
5
- import { createCollectionOneTable, deleteCollectionOneTable, } from "./collectionsRepo.one-table.js";
5
+ import { attachQueryDiagnostics } from "../ydb/QueryDiagnostics.js";
6
6
  import { withRetry, isTransientYdbError } from "../utils/retry.js";
7
+ import { createCollectionOneTable, deleteCollectionOneTable, } from "./collectionsRepo.one-table.js";
8
+ import { Timestamp, Utf8 } from "@ydbjs/value/primitive";
9
+ import { DistanceKindSchema, VectorTypeSchema, } from "../types.js";
7
10
  const lastAccessWriteCache = new Map();
8
11
  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
+ }
9
48
  function evictOldestLastAccessEntry() {
10
49
  if (lastAccessWriteCache.size < LAST_ACCESS_CACHE_MAX_SIZE) {
11
50
  return;
@@ -24,34 +63,39 @@ function shouldWriteLastAccess(nowMs, key) {
24
63
  }
25
64
  export async function createCollection(metaKey, dim, distance, vectorType) {
26
65
  await createCollectionOneTable(metaKey, dim, distance, vectorType);
66
+ collectionMetaCache.delete(metaKey);
27
67
  }
28
68
  export async function getCollectionMeta(metaKey) {
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);
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;
45
90
  });
46
- const rowset = res.resultSets?.[0];
47
- if (!rowset || rowset.rows?.length !== 1)
91
+ if (rows.length !== 1)
48
92
  return null;
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;
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;
55
99
  const lastAccessedAt = typeof lastAccessRaw === "string" && lastAccessRaw.length > 0
56
100
  ? new Date(lastAccessRaw)
57
101
  : undefined;
@@ -64,37 +108,35 @@ export async function getCollectionMeta(metaKey) {
64
108
  if (lastAccessedAt) {
65
109
  result.lastAccessedAt = lastAccessedAt;
66
110
  }
111
+ setCachedCollectionMeta(metaKey, result);
67
112
  return result;
68
113
  }
69
114
  export async function verifyCollectionsQueryCompilationForStartup() {
70
115
  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
- `;
82
116
  await withRetry(async () => {
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);
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));
92
132
  });
93
133
  }, {
94
- isTransient: isTransientYdbError,
95
134
  maxRetries: 2,
96
135
  baseDelayMs: 200,
97
- context: { probe: "collections_startup_compilation" },
136
+ isTransient: isTransientYdbError,
137
+ context: {
138
+ operation: "verifyCollectionsQueryCompilationForStartup",
139
+ },
98
140
  });
99
141
  }
100
142
  export async function deleteCollection(metaKey, uid) {
@@ -110,6 +152,7 @@ export async function deleteCollection(metaKey, uid) {
110
152
  effectiveUid = uidFor(tenant, collection);
111
153
  }
112
154
  await deleteCollectionOneTable(metaKey, effectiveUid);
155
+ collectionMetaCache.delete(metaKey);
113
156
  }
114
157
  /**
115
158
  * Best-effort metadata update for a collection's last_accessed_at timestamp.
@@ -124,20 +167,18 @@ export async function touchCollectionLastAccess(metaKey, now = new Date()) {
124
167
  if (!shouldWriteLastAccess(nowMs, metaKey)) {
125
168
  return;
126
169
  }
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
- `;
134
170
  try {
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);
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));
141
182
  });
142
183
  evictOldestLastAccessEntry();
143
184
  lastAccessWriteCache.set(metaKey, nowMs);
@@ -1,139 +1,57 @@
1
- import { TypedValues, Types, withSession, createExecuteQuerySettings, } from "../ydb/client.js";
1
+ import { withSession } from "../ydb/client.js";
2
2
  import { GLOBAL_POINTS_TABLE, ensureGlobalPointsTable } from "../ydb/schema.js";
3
- import { upsertCollectionMeta } from "./collectionsRepo.shared.js";
4
- import { withRetry, isTransientYdbError } from "../utils/retry.js";
5
- import { USE_BATCH_DELETE_FOR_COLLECTIONS } from "../config/env.js";
6
- const DELETE_COLLECTION_BATCH_SIZE = 10000;
7
- function isOutOfBufferMemoryYdbError(error) {
8
- const msg = error instanceof Error ? error.message : String(error);
9
- if (/Out of buffer memory/i.test(msg)) {
10
- return true;
11
- }
12
- if (typeof error === "object" && error !== null) {
13
- const issues = error.issues;
14
- if (issues !== undefined) {
15
- const issuesText = typeof issues === "string" ? issues : JSON.stringify(issues);
16
- return /Out of buffer memory/i.test(issuesText);
17
- }
18
- }
19
- return false;
20
- }
21
- async function deletePointsForUidInChunks(s, uid) {
22
- const selectYql = `
23
- DECLARE $uid AS Utf8;
24
- DECLARE $limit AS Uint32;
25
- SELECT point_id
26
- FROM ${GLOBAL_POINTS_TABLE}
27
- WHERE uid = $uid
28
- LIMIT $limit;
29
- `;
30
- const deleteBatchYql = `
31
- DECLARE $uid AS Utf8;
32
- DECLARE $ids AS List<Utf8>;
33
- DELETE FROM ${GLOBAL_POINTS_TABLE}
34
- WHERE uid = $uid AND point_id IN $ids;
35
- `;
36
- // Best‑effort loop: stop when there are no more rows for this uid.
37
- // Each iteration only touches a limited number of rows to avoid
38
- // hitting YDB's per‑operation buffer limits.
39
- let iterations = 0;
40
- const MAX_ITERATIONS = 1000;
41
- const settings = createExecuteQuerySettings();
42
- while (iterations++ < MAX_ITERATIONS) {
43
- const rs = (await s.executeQuery(selectYql, {
44
- $uid: TypedValues.utf8(uid),
45
- $limit: TypedValues.uint32(DELETE_COLLECTION_BATCH_SIZE),
46
- }, undefined, settings));
47
- const rowset = rs.resultSets?.[0];
48
- const rows = rowset?.rows ?? [];
49
- const ids = rows
50
- .map((row) => row.items?.[0]?.textValue)
51
- .filter((id) => typeof id === "string");
52
- if (ids.length === 0) {
53
- break;
54
- }
55
- const idsValue = TypedValues.list(Types.UTF8, ids);
56
- await s.executeQuery(deleteBatchYql, {
57
- $uid: TypedValues.utf8(uid),
58
- $ids: idsValue,
59
- }, undefined, settings);
60
- }
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
+ });
61
31
  }
62
32
  export async function createCollectionOneTable(metaKey, dim, distance, vectorType) {
63
33
  await upsertCollectionMeta(metaKey, dim, distance, vectorType, GLOBAL_POINTS_TABLE);
64
34
  }
65
35
  export async function deleteCollectionOneTable(metaKey, uid) {
66
36
  await ensureGlobalPointsTable();
67
- if (USE_BATCH_DELETE_FOR_COLLECTIONS) {
68
- const batchDeletePointsYql = `
69
- DECLARE $uid AS Utf8;
70
- BATCH DELETE FROM ${GLOBAL_POINTS_TABLE}
37
+ await withSession(async (sql, signal) => {
38
+ await sql `
39
+ BATCH DELETE FROM ${sql.identifier(GLOBAL_POINTS_TABLE)}
71
40
  WHERE uid = $uid;
72
- `;
73
- await withRetry(() => withSession(async (s) => {
74
- const settings = createExecuteQuerySettings();
75
- try {
76
- await s.executeQuery(batchDeletePointsYql, {
77
- $uid: TypedValues.utf8(uid),
78
- }, undefined, settings);
79
- }
80
- catch (err) {
81
- if (!isOutOfBufferMemoryYdbError(err)) {
82
- throw err;
83
- }
84
- // BATCH DELETE already deletes in chunks per partition, but if YDB
85
- // still reports an out-of-buffer-memory condition, fall back to
86
- // the same per-uid chunked deletion strategy as the legacy path.
87
- await deletePointsForUidInChunks(s, uid);
88
- }
89
- }), {
90
- isTransient: isTransientYdbError,
91
- context: {
92
- operation: "deleteCollectionOneTable",
93
- tableName: GLOBAL_POINTS_TABLE,
94
- metaKey,
95
- uid,
96
- mode: "batch_delete",
97
- },
98
- });
99
- }
100
- else {
101
- const deletePointsYql = `
102
- DECLARE $uid AS Utf8;
103
- DELETE FROM ${GLOBAL_POINTS_TABLE} WHERE uid = $uid;
104
- `;
105
- await withRetry(() => withSession(async (s) => {
106
- const settings = createExecuteQuerySettings();
107
- try {
108
- await s.executeQuery(deletePointsYql, {
109
- $uid: TypedValues.utf8(uid),
110
- }, undefined, settings);
111
- }
112
- catch (err) {
113
- if (!isOutOfBufferMemoryYdbError(err)) {
114
- throw err;
115
- }
116
- await deletePointsForUidInChunks(s, uid);
117
- }
118
- }), {
119
- isTransient: isTransientYdbError,
120
- context: {
121
- operation: "deleteCollectionOneTable",
122
- tableName: GLOBAL_POINTS_TABLE,
123
- metaKey,
124
- uid,
125
- mode: "legacy_chunked",
126
- },
127
- });
128
- }
129
- const delMeta = `
130
- DECLARE $collection AS Utf8;
131
- DELETE FROM qdr__collections WHERE collection = $collection;
132
- `;
133
- await withSession(async (s) => {
134
- const settings = createExecuteQuerySettings();
135
- await s.executeQuery(delMeta, {
136
- $collection: TypedValues.utf8(metaKey),
137
- }, undefined, settings);
41
+ `
42
+ .idempotent(true)
43
+ .timeout(UPSERT_OPERATION_TIMEOUT_MS)
44
+ .signal(signal)
45
+ .parameter("uid", new Utf8(uid));
46
+ });
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));
138
56
  });
139
57
  }
@@ -1,12 +1,10 @@
1
1
  import type { DistanceKind } from "../types";
2
- export declare function upsertPoints(tableName: string, points: Array<{
3
- id: string | number;
4
- vector: number[];
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<{
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<{
8
5
  id: string;
9
6
  score: number;
10
- payload?: Record<string, unknown>;
7
+ payload?: QdrantPayload;
11
8
  }>>;
12
9
  export declare function deletePoints(tableName: string, ids: Array<string | number>, uid: string): Promise<number>;
10
+ export declare function deletePointsByPathSegments(tableName: string, uid: string, paths: Array<Array<string>>): Promise<number>;
@@ -1,12 +1,15 @@
1
1
  import { SEARCH_MODE, OVERFETCH_MULTIPLIER, } from "../config/env.js";
2
- import { upsertPointsOneTable, searchPointsOneTable, deletePointsOneTable, } from "./pointsRepo.one-table.js";
2
+ import { upsertPointsOneTable, searchPointsOneTable, deletePointsOneTable, deletePointsByPathSegmentsOneTable, } from "./pointsRepo.one-table.js";
3
3
  export async function upsertPoints(tableName, points, dimension, uid) {
4
4
  return await upsertPointsOneTable(tableName, points, dimension, uid);
5
5
  }
6
- export async function searchPoints(tableName, queryVector, top, withPayload, distance, dimension, uid) {
6
+ export async function searchPoints(tableName, queryVector, top, withPayload, distance, dimension, uid, filterPaths) {
7
7
  const mode = SEARCH_MODE;
8
- return await searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid, mode, OVERFETCH_MULTIPLIER);
8
+ return await searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid, mode, OVERFETCH_MULTIPLIER, filterPaths);
9
9
  }
10
10
  export async function deletePoints(tableName, ids, uid) {
11
11
  return await deletePointsOneTable(tableName, ids, uid);
12
12
  }
13
+ export async function deletePointsByPathSegments(tableName, uid, paths) {
14
+ return await deletePointsByPathSegmentsOneTable(tableName, uid, paths);
15
+ }
@@ -0,0 +1,2 @@
1
+ export declare function deletePointsOneTable(tableName: string, ids: Array<string | number>, uid: string): Promise<number>;
2
+ export declare function deletePointsByPathSegmentsOneTable(tableName: string, uid: string, paths: Array<Array<string>>): Promise<number>;