ydb-qdrant 7.0.1 → 8.1.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.
- package/README.md +2 -2
- package/dist/config/env.d.ts +0 -8
- package/dist/config/env.js +2 -29
- package/dist/package/api.d.ts +5 -2
- package/dist/package/api.js +2 -2
- package/dist/qdrant/QdrantRestTypes.d.ts +35 -0
- package/dist/repositories/collectionsRepo.d.ts +1 -2
- package/dist/repositories/collectionsRepo.js +62 -103
- package/dist/repositories/collectionsRepo.one-table.js +103 -47
- package/dist/repositories/collectionsRepo.shared.d.ts +2 -0
- package/dist/repositories/collectionsRepo.shared.js +32 -0
- package/dist/repositories/pointsRepo.d.ts +4 -8
- package/dist/repositories/pointsRepo.one-table/Delete.js +122 -67
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +5 -2
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +7 -6
- package/dist/repositories/pointsRepo.one-table/Search.d.ts +4 -0
- package/dist/repositories/pointsRepo.one-table/Search.js +208 -0
- package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
- package/dist/repositories/pointsRepo.one-table/Upsert.js +51 -66
- package/dist/repositories/pointsRepo.one-table.d.ts +1 -1
- package/dist/repositories/pointsRepo.one-table.js +1 -1
- package/dist/routes/collections.js +7 -61
- package/dist/routes/points.js +15 -66
- package/dist/services/PointsService.d.ts +3 -8
- package/dist/services/PointsService.js +19 -23
- package/dist/types.d.ts +23 -33
- package/dist/types.js +18 -20
- package/dist/utils/normalization.js +13 -14
- package/dist/utils/retry.js +19 -29
- package/dist/utils/vectorBinary.js +10 -5
- package/dist/ydb/bootstrapMetaTable.d.ts +7 -0
- package/dist/ydb/bootstrapMetaTable.js +75 -0
- package/dist/ydb/client.d.ts +23 -17
- package/dist/ydb/client.js +82 -423
- package/dist/ydb/schema.js +88 -148
- package/package.json +2 -10
- package/dist/qdrant/QdrantTypes.d.ts +0 -19
- package/dist/repositories/pointsRepo.one-table/Search/Approximate.d.ts +0 -18
- package/dist/repositories/pointsRepo.one-table/Search/Approximate.js +0 -119
- package/dist/repositories/pointsRepo.one-table/Search/Exact.d.ts +0 -17
- package/dist/repositories/pointsRepo.one-table/Search/Exact.js +0 -101
- package/dist/repositories/pointsRepo.one-table/Search/index.d.ts +0 -8
- package/dist/repositories/pointsRepo.one-table/Search/index.js +0 -30
- package/dist/utils/typeGuards.d.ts +0 -1
- package/dist/utils/typeGuards.js +0 -3
- package/dist/ydb/QueryDiagnostics.d.ts +0 -6
- package/dist/ydb/QueryDiagnostics.js +0 -52
- package/dist/ydb/SessionPool.d.ts +0 -36
- package/dist/ydb/SessionPool.js +0 -248
- package/dist/ydb/bulkUpsert.d.ts +0 -6
- package/dist/ydb/bulkUpsert.js +0 -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
|
|
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
|
|
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
|
package/dist/config/env.d.ts
CHANGED
|
@@ -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"
|
package/dist/config/env.js
CHANGED
|
@@ -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,
|
|
83
|
-
export const SEARCH_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_SEARCH_TIMEOUT_MS,
|
|
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 });
|
package/dist/package/api.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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
|
}
|
package/dist/package/api.js
CHANGED
|
@@ -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.
|
|
36
|
+
options.authService !== undefined) {
|
|
37
37
|
configureDriver({
|
|
38
38
|
endpoint: options.endpoint,
|
|
39
39
|
database: options.database,
|
|
40
40
|
connectionString: options.connectionString,
|
|
41
|
-
|
|
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
|
|
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,
|
|
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 {
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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.
|
|
95
|
-
const dimension = Number(row.
|
|
96
|
-
const distance =
|
|
97
|
-
const vectorType =
|
|
98
|
-
const lastAccessRaw = row.
|
|
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 (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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 (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
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,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 {
|
|
3
|
-
export declare function upsertPoints(tableName: string, points:
|
|
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<
|
|
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>;
|