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.
- package/README.md +2 -2
- package/dist/config/env.d.ts +8 -3
- package/dist/config/env.js +15 -5
- package/dist/package/api.d.ts +2 -2
- package/dist/package/api.js +2 -2
- package/dist/qdrant/QdrantTypes.d.ts +19 -0
- package/dist/qdrant/QdrantTypes.js +1 -0
- package/dist/repositories/collectionsRepo.d.ts +2 -1
- package/dist/repositories/collectionsRepo.js +103 -62
- package/dist/repositories/collectionsRepo.one-table.js +47 -129
- package/dist/repositories/pointsRepo.d.ts +5 -7
- package/dist/repositories/pointsRepo.js +6 -3
- package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -0
- package/dist/repositories/pointsRepo.one-table/Delete.js +111 -0
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +11 -0
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +32 -0
- package/dist/repositories/pointsRepo.one-table/Search/Approximate.d.ts +18 -0
- package/dist/repositories/pointsRepo.one-table/Search/Approximate.js +119 -0
- package/dist/repositories/pointsRepo.one-table/Search/Exact.d.ts +17 -0
- package/dist/repositories/pointsRepo.one-table/Search/Exact.js +101 -0
- package/dist/repositories/pointsRepo.one-table/Search/index.d.ts +8 -0
- package/dist/repositories/pointsRepo.one-table/Search/index.js +30 -0
- package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -0
- package/dist/repositories/pointsRepo.one-table/Upsert.js +100 -0
- package/dist/repositories/pointsRepo.one-table.d.ts +3 -13
- package/dist/repositories/pointsRepo.one-table.js +3 -403
- package/dist/routes/collections.js +61 -7
- package/dist/routes/points.js +71 -3
- package/dist/server.d.ts +1 -0
- package/dist/server.js +70 -2
- package/dist/services/CollectionService.d.ts +9 -0
- package/dist/services/CollectionService.js +9 -0
- package/dist/services/PointsService.d.ts +8 -10
- package/dist/services/PointsService.js +78 -4
- package/dist/types.d.ts +72 -8
- package/dist/types.js +43 -17
- package/dist/utils/normalization.d.ts +1 -0
- package/dist/utils/normalization.js +15 -13
- package/dist/utils/retry.js +29 -19
- package/dist/utils/typeGuards.d.ts +1 -0
- package/dist/utils/typeGuards.js +3 -0
- package/dist/utils/vectorBinary.js +88 -9
- package/dist/ydb/QueryDiagnostics.d.ts +6 -0
- package/dist/ydb/QueryDiagnostics.js +52 -0
- package/dist/ydb/SessionPool.d.ts +36 -0
- package/dist/ydb/SessionPool.js +248 -0
- package/dist/ydb/bulkUpsert.d.ts +6 -0
- package/dist/ydb/bulkUpsert.js +52 -0
- package/dist/ydb/client.d.ts +17 -16
- package/dist/ydb/client.js +427 -62
- package/dist/ydb/helpers.d.ts +0 -2
- package/dist/ydb/helpers.js +0 -7
- package/dist/ydb/schema.js +171 -77
- package/package.json +12 -7
- package/dist/repositories/collectionsRepo.shared.d.ts +0 -2
- 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
|
|
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
|
|
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
|
package/dist/config/env.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
|
|
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;
|
package/dist/config/env.js
CHANGED
|
@@ -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
|
|
38
|
-
|
|
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,
|
|
73
|
-
export const SEARCH_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_SEARCH_TIMEOUT_MS,
|
|
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 });
|
package/dist/package/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
18
|
+
credentialsProvider?: CredentialsProvider;
|
|
19
19
|
}
|
|
20
20
|
export interface YdbQdrantTenantClient {
|
|
21
21
|
createCollection(collection: string, body: unknown): Promise<CreateCollectionResult>;
|
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.credentialsProvider !== undefined) {
|
|
37
37
|
configureDriver({
|
|
38
38
|
endpoint: options.endpoint,
|
|
39
39
|
database: options.database,
|
|
40
40
|
connectionString: options.connectionString,
|
|
41
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
if (!rowset || rowset.rows?.length !== 1)
|
|
91
|
+
if (rows.length !== 1)
|
|
48
92
|
return null;
|
|
49
|
-
const row =
|
|
50
|
-
const table = row.
|
|
51
|
-
const dimension = Number(row.
|
|
52
|
-
const distance =
|
|
53
|
-
const vectorType =
|
|
54
|
-
const lastAccessRaw = row.
|
|
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 (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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 (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 {
|
|
1
|
+
import { withSession } from "../ydb/client.js";
|
|
2
2
|
import { GLOBAL_POINTS_TABLE, ensureGlobalPointsTable } from "../ydb/schema.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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?:
|
|
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
|
+
}
|