ydb-qdrant 8.1.0 → 9.0.3
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 +20 -18
- package/dist/SmokeTest.js +2 -2
- package/dist/compute/ComputePool.d.ts +5 -0
- package/dist/compute/ComputePool.js +64 -0
- package/dist/compute/ComputeWorker.d.ts +36 -0
- package/dist/compute/ComputeWorker.js +84 -0
- package/dist/config/env.d.ts +24 -7
- package/dist/config/env.js +65 -35
- package/dist/index.d.ts +2 -0
- package/dist/index.js +92 -2
- package/dist/logging/DeployLogFormatter.d.ts +2 -0
- package/dist/logging/DeployLogFormatter.js +131 -0
- package/dist/logging/logger.js +13 -1
- package/dist/logging/requestContext.d.ts +17 -0
- package/dist/logging/requestContext.js +43 -0
- package/dist/middleware/requestLogger.js +134 -6
- package/dist/middleware/upsertBodyPhase.d.ts +6 -0
- package/dist/middleware/upsertBodyPhase.js +184 -0
- package/dist/middleware/upsertRequestTimeout.d.ts +16 -0
- package/dist/middleware/upsertRequestTimeout.js +158 -0
- package/dist/package/api.d.ts +20 -12
- package/dist/package/api.js +57 -28
- package/dist/qdrant/QdrantRestTypes.d.ts +4 -0
- package/dist/qdrant/Requests.d.ts +97 -0
- package/dist/qdrant/Requests.js +72 -0
- package/dist/repositories/collectionsRepo.d.ts +18 -2
- package/dist/repositories/collectionsRepo.js +103 -7
- package/dist/repositories/collectionsRepo.one-table.d.ts +4 -3
- package/dist/repositories/collectionsRepo.one-table.js +99 -36
- package/dist/repositories/collectionsRepo.shared.d.ts +2 -2
- package/dist/repositories/collectionsRepo.shared.js +9 -4
- package/dist/repositories/pointsRepo.d.ts +6 -4
- package/dist/repositories/pointsRepo.js +8 -7
- package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -2
- package/dist/repositories/pointsRepo.one-table/Delete.js +157 -60
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +7 -5
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +44 -13
- package/dist/repositories/pointsRepo.one-table/Retrieve.d.ts +6 -0
- package/dist/repositories/pointsRepo.one-table/Retrieve.js +69 -0
- package/dist/repositories/pointsRepo.one-table/Search.d.ts +2 -3
- package/dist/repositories/pointsRepo.one-table/Search.js +102 -124
- package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
- package/dist/repositories/pointsRepo.one-table/Upsert.js +244 -48
- package/dist/repositories/pointsRepo.one-table.d.ts +1 -0
- package/dist/repositories/pointsRepo.one-table.js +1 -0
- package/dist/routes/collections.js +45 -36
- package/dist/routes/points.js +145 -56
- package/dist/server.js +42 -6
- package/dist/services/CollectionService.d.ts +7 -5
- package/dist/services/CollectionService.js +12 -9
- package/dist/services/CollectionService.one-table.js +1 -2
- package/dist/services/CollectionService.shared.d.ts +6 -5
- package/dist/services/CollectionService.shared.js +28 -12
- package/dist/services/PointsService.d.ts +8 -0
- package/dist/services/PointsService.js +132 -15
- package/dist/types.d.ts +4 -94
- package/dist/types.js +1 -54
- package/dist/utils/EnvParsers.d.ts +5 -0
- package/dist/utils/EnvParsers.js +30 -0
- package/dist/utils/PayloadSign.d.ts +4 -0
- package/dist/utils/PayloadSign.js +18 -0
- package/dist/utils/distance.d.ts +1 -12
- package/dist/utils/distance.js +0 -21
- package/dist/utils/pathPrefix.d.ts +3 -0
- package/dist/utils/pathPrefix.js +47 -0
- package/dist/utils/prefixExpansion.d.ts +1 -0
- package/dist/utils/prefixExpansion.js +11 -0
- package/dist/utils/qdrantResponse.d.ts +13 -0
- package/dist/utils/qdrantResponse.js +12 -0
- package/dist/utils/requestIdentity.d.ts +8 -0
- package/dist/utils/requestIdentity.js +52 -0
- package/dist/utils/retry.d.ts +2 -0
- package/dist/utils/retry.js +55 -11
- package/dist/utils/tenant.d.ts +12 -6
- package/dist/utils/tenant.js +41 -32
- package/dist/utils/vectorBinary.d.ts +0 -1
- package/dist/utils/vectorBinary.js +0 -98
- package/dist/utils/ydbErrors.d.ts +1 -0
- package/dist/utils/ydbErrors.js +14 -0
- package/dist/ydb/bootstrapMetaTable.js +14 -2
- package/dist/ydb/client.d.ts +10 -2
- package/dist/ydb/client.js +83 -24
- package/dist/ydb/helpers.d.ts +0 -1
- package/dist/ydb/helpers.js +1 -2
- package/dist/ydb/schema.d.ts +2 -0
- package/dist/ydb/schema.js +84 -7
- package/package.json +10 -5
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AnonymousIdentityError, deriveAnonymousUserUid, deriveUserUidFromApiKey, sanitizeUserUid, } from "./tenant.js";
|
|
2
|
+
export function isAnonymousIdentityError(err) {
|
|
3
|
+
return err instanceof AnonymousIdentityError;
|
|
4
|
+
}
|
|
5
|
+
function sanitizeTenantId(tenantId) {
|
|
6
|
+
const raw = (tenantId ?? "default").toString();
|
|
7
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_");
|
|
8
|
+
const lowered = cleaned.toLowerCase().replace(/^_+/, "");
|
|
9
|
+
return lowered.length > 0 ? lowered : "default";
|
|
10
|
+
}
|
|
11
|
+
export function getRequestApiKey(req) {
|
|
12
|
+
const apiKey = req.header("api-key")?.trim();
|
|
13
|
+
return apiKey && apiKey.length > 0 ? apiKey : undefined;
|
|
14
|
+
}
|
|
15
|
+
export function getRequestClientIp(req) {
|
|
16
|
+
const reqIp = typeof req.ip === "string" && req.ip.trim().length > 0
|
|
17
|
+
? req.ip.trim()
|
|
18
|
+
: undefined;
|
|
19
|
+
if (reqIp) {
|
|
20
|
+
return reqIp;
|
|
21
|
+
}
|
|
22
|
+
const remoteAddress = req.socket.remoteAddress;
|
|
23
|
+
return typeof remoteAddress === "string" && remoteAddress.trim().length > 0
|
|
24
|
+
? remoteAddress.trim()
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
export function resolveRequestUserUid(req) {
|
|
28
|
+
const apiKey = getRequestApiKey(req);
|
|
29
|
+
if (apiKey) {
|
|
30
|
+
return deriveUserUidFromApiKey(apiKey);
|
|
31
|
+
}
|
|
32
|
+
return deriveAnonymousUserUid({
|
|
33
|
+
clientIp: getRequestClientIp(req),
|
|
34
|
+
userAgent: req.header("User-Agent") ?? undefined,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function resolveRequestNamespaceUserUid(req) {
|
|
38
|
+
const baseUserUid = resolveRequestUserUid(req);
|
|
39
|
+
const tenantId = sanitizeTenantId(req.header("X-Tenant-Id") ?? undefined);
|
|
40
|
+
return sanitizeUserUid(`${baseUserUid}_${tenantId}`);
|
|
41
|
+
}
|
|
42
|
+
export function resolveRequestSigningKey(req, userUid) {
|
|
43
|
+
const apiKey = getRequestApiKey(req);
|
|
44
|
+
if (apiKey) {
|
|
45
|
+
return apiKey;
|
|
46
|
+
}
|
|
47
|
+
const fallbackUserUid = userUid?.trim();
|
|
48
|
+
if (fallbackUserUid) {
|
|
49
|
+
return fallbackUserUid;
|
|
50
|
+
}
|
|
51
|
+
return resolveRequestUserUid(req);
|
|
52
|
+
}
|
package/dist/utils/retry.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export interface RetryOptions {
|
|
2
2
|
maxRetries?: number;
|
|
3
3
|
baseDelayMs?: number;
|
|
4
|
+
maxBackoffMs?: number;
|
|
4
5
|
isTransient?: (error: unknown) => boolean;
|
|
5
6
|
context?: Record<string, unknown>;
|
|
6
7
|
}
|
|
7
8
|
export declare function isTransientYdbError(error: unknown): boolean;
|
|
9
|
+
export declare function isTransientYdbErrorInAcquiredSession(error: unknown): boolean;
|
|
8
10
|
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
package/dist/utils/retry.js
CHANGED
|
@@ -1,25 +1,68 @@
|
|
|
1
1
|
import { logger } from "../logging/logger.js";
|
|
2
2
|
const DEFAULT_MAX_RETRIES = 6;
|
|
3
3
|
const DEFAULT_BASE_DELAY_MS = 250;
|
|
4
|
-
|
|
4
|
+
const UNDETERMINED_OPERATION_UNKNOWN_RE = /Undetermined \(code 400170\)|State of operation is unknown|Failed to d(?:eliver|eviler) message|issueCode["']?\s*:\s*2026/i;
|
|
5
|
+
const SESSION_RECREATION_ERROR_RE = /BadSession \(code 400100\)|Session is under shutdown|SessionBusy|SESSION_BUSY/i;
|
|
6
|
+
const SESSION_EXPIRED_ERROR_RE = /SessionExpired|SESSION_EXPIRED/i;
|
|
7
|
+
const SESSION_POOL_CONTENTION_RE = /SESSION_POOL_EMPTY|No session became available within timeout/i;
|
|
8
|
+
const SAME_SESSION_TRANSIENT_YDB_ERROR_RE = /Aborted|Unavailable \(code 400050\)|schema version mismatch|Table metadata loading|Failed to load metadata|overloaded|is in process of split|wrong shard state|Rejecting data TxId/i;
|
|
9
|
+
const TIMEOUT_ERROR_RE = /Timeout \(code 400090\)/i;
|
|
10
|
+
function normalizeMaxBackoffMs(value) {
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
return Number.POSITIVE_INFINITY;
|
|
13
|
+
}
|
|
14
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
15
|
+
return Number.POSITIVE_INFINITY;
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function getErrorMessageAndIssuesText(error) {
|
|
5
20
|
const msg = error instanceof Error ? error.message : String(error);
|
|
6
|
-
if (
|
|
7
|
-
return
|
|
21
|
+
if (typeof error !== "object" || error === null) {
|
|
22
|
+
return { msg };
|
|
8
23
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
const issues = error.issues;
|
|
25
|
+
if (issues === undefined) {
|
|
26
|
+
return { msg };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
msg,
|
|
30
|
+
issuesText: typeof issues === "string" ? issues : JSON.stringify(issues),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function matchesPatternSet(error, patterns) {
|
|
34
|
+
const { msg, issuesText } = getErrorMessageAndIssuesText(error);
|
|
35
|
+
for (const pattern of patterns) {
|
|
36
|
+
if (pattern.test(msg)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (issuesText !== undefined && pattern.test(issuesText)) {
|
|
40
|
+
return true;
|
|
16
41
|
}
|
|
17
42
|
}
|
|
18
43
|
return false;
|
|
19
44
|
}
|
|
45
|
+
export function isTransientYdbError(error) {
|
|
46
|
+
return matchesPatternSet(error, [
|
|
47
|
+
UNDETERMINED_OPERATION_UNKNOWN_RE,
|
|
48
|
+
SESSION_RECREATION_ERROR_RE,
|
|
49
|
+
SESSION_EXPIRED_ERROR_RE,
|
|
50
|
+
SESSION_POOL_CONTENTION_RE,
|
|
51
|
+
SAME_SESSION_TRANSIENT_YDB_ERROR_RE,
|
|
52
|
+
TIMEOUT_ERROR_RE,
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
export function isTransientYdbErrorInAcquiredSession(error) {
|
|
56
|
+
return matchesPatternSet(error, [
|
|
57
|
+
UNDETERMINED_OPERATION_UNKNOWN_RE,
|
|
58
|
+
SAME_SESSION_TRANSIENT_YDB_ERROR_RE,
|
|
59
|
+
TIMEOUT_ERROR_RE,
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
20
62
|
export async function withRetry(fn, options = {}) {
|
|
21
63
|
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
22
64
|
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
65
|
+
const maxBackoffMs = normalizeMaxBackoffMs(options.maxBackoffMs);
|
|
23
66
|
const isTransient = options.isTransient ?? isTransientYdbError;
|
|
24
67
|
const context = options.context ?? {};
|
|
25
68
|
let attempt = 0;
|
|
@@ -31,7 +74,8 @@ export async function withRetry(fn, options = {}) {
|
|
|
31
74
|
if (!isTransient(e) || attempt >= maxRetries) {
|
|
32
75
|
throw e;
|
|
33
76
|
}
|
|
34
|
-
const
|
|
77
|
+
const computedBackoffMs = Math.floor(baseDelayMs * Math.pow(2, attempt) + Math.random() * 100);
|
|
78
|
+
const backoffMs = Math.min(computedBackoffMs, maxBackoffMs);
|
|
35
79
|
logger.warn({
|
|
36
80
|
...context,
|
|
37
81
|
attempt,
|
package/dist/utils/tenant.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare class AnonymousIdentityError extends Error {
|
|
2
|
+
constructor();
|
|
3
|
+
}
|
|
2
4
|
export declare function normalizeUserAgent(userAgent: string | undefined): string | undefined;
|
|
3
|
-
export declare function sanitizeCollectionName(name: string,
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
7
|
-
|
|
5
|
+
export declare function sanitizeCollectionName(name: string, _userAgentNormalized?: string): string;
|
|
6
|
+
export declare function sanitizeUserUid(userUid: string): string;
|
|
7
|
+
export declare function deriveUserUidFromApiKey(apiKey: string): string;
|
|
8
|
+
export declare function deriveAnonymousUserUid(args: {
|
|
9
|
+
clientIp?: string;
|
|
10
|
+
userAgent?: string;
|
|
11
|
+
}): string;
|
|
12
|
+
export declare function metaKeyFor(sanitizedUserUid: string, sanitizedCollection: string): string;
|
|
13
|
+
export declare function uidFor(sanitizedUserUid: string, sanitizedCollection: string): string;
|
package/dist/utils/tenant.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { createHash } from "crypto";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const API_KEY_UID_PREFIX = "ak";
|
|
3
|
+
const ANONYMOUS_UID_PREFIX = "anon";
|
|
4
|
+
const UID_HASH_LENGTH = 16;
|
|
5
|
+
export class AnonymousIdentityError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Anonymous requests require api-key or identifiable client metadata.");
|
|
8
|
+
this.name = "AnonymousIdentityError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function shortHash(value) {
|
|
12
|
+
return createHash("sha256").update(value).digest("hex").slice(0, UID_HASH_LENGTH);
|
|
7
13
|
}
|
|
8
14
|
export function normalizeUserAgent(userAgent) {
|
|
9
15
|
if (!userAgent || userAgent.trim() === "")
|
|
@@ -22,39 +28,42 @@ export function normalizeUserAgent(userAgent) {
|
|
|
22
28
|
}
|
|
23
29
|
if (lowered.length === 0)
|
|
24
30
|
return undefined;
|
|
25
|
-
|
|
26
|
-
const shortHash = hash.slice(0, 8);
|
|
27
|
-
return `${lowered}_${shortHash}`;
|
|
31
|
+
return lowered;
|
|
28
32
|
}
|
|
29
|
-
export function sanitizeCollectionName(name,
|
|
33
|
+
export function sanitizeCollectionName(name, _userAgentNormalized) {
|
|
34
|
+
void _userAgentNormalized;
|
|
30
35
|
const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_");
|
|
31
36
|
const lowered = cleaned.toLowerCase().replace(/^_+/, "");
|
|
32
|
-
|
|
33
|
-
const hasApiKey = apiKeyHash !== undefined && apiKeyHash.trim().length > 0;
|
|
34
|
-
const hasUserAgent = userAgentNormalized !== undefined && userAgentNormalized.trim().length > 0;
|
|
35
|
-
if (hasApiKey && hasUserAgent) {
|
|
36
|
-
return `${base}_${apiKeyHash}_${userAgentNormalized}`;
|
|
37
|
-
}
|
|
38
|
-
else if (hasApiKey) {
|
|
39
|
-
return `${base}_${apiKeyHash}`;
|
|
40
|
-
}
|
|
41
|
-
else if (hasUserAgent) {
|
|
42
|
-
return `${base}_${userAgentNormalized}`;
|
|
43
|
-
}
|
|
44
|
-
return base;
|
|
37
|
+
return lowered.length > 0 ? lowered : "collection";
|
|
45
38
|
}
|
|
46
|
-
export function
|
|
47
|
-
const raw =
|
|
39
|
+
export function sanitizeUserUid(userUid) {
|
|
40
|
+
const raw = userUid.toString();
|
|
48
41
|
const cleaned = raw.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_");
|
|
49
42
|
const lowered = cleaned.toLowerCase().replace(/^_+/, "");
|
|
50
|
-
return lowered.length > 0 ? lowered : "
|
|
43
|
+
return lowered.length > 0 ? lowered : "anonymous";
|
|
44
|
+
}
|
|
45
|
+
export function deriveUserUidFromApiKey(apiKey) {
|
|
46
|
+
const normalizedApiKey = apiKey.trim();
|
|
47
|
+
if (normalizedApiKey.length === 0) {
|
|
48
|
+
throw new Error("deriveUserUidFromApiKey: apiKey is empty");
|
|
49
|
+
}
|
|
50
|
+
return sanitizeUserUid(`${API_KEY_UID_PREFIX}_${shortHash(normalizedApiKey)}`);
|
|
51
51
|
}
|
|
52
|
-
export function
|
|
53
|
-
|
|
52
|
+
export function deriveAnonymousUserUid(args) {
|
|
53
|
+
const normalizedUserAgent = normalizeUserAgent(args.userAgent);
|
|
54
|
+
const normalizedClientIp = args.clientIp?.trim();
|
|
55
|
+
if (!normalizedClientIp && !normalizedUserAgent) {
|
|
56
|
+
throw new AnonymousIdentityError();
|
|
57
|
+
}
|
|
58
|
+
const identitySeed = [
|
|
59
|
+
normalizedClientIp || "unknown_ip",
|
|
60
|
+
normalizedUserAgent || "unknown_ua",
|
|
61
|
+
].join("|");
|
|
62
|
+
return sanitizeUserUid(`${ANONYMOUS_UID_PREFIX}_${shortHash(identitySeed)}`);
|
|
54
63
|
}
|
|
55
|
-
export function metaKeyFor(
|
|
56
|
-
return `${
|
|
64
|
+
export function metaKeyFor(sanitizedUserUid, sanitizedCollection) {
|
|
65
|
+
return `${sanitizedUserUid}/${sanitizedCollection}`;
|
|
57
66
|
}
|
|
58
|
-
export function uidFor(
|
|
59
|
-
return
|
|
67
|
+
export function uidFor(sanitizedUserUid, sanitizedCollection) {
|
|
68
|
+
return metaKeyFor(sanitizedUserUid, sanitizedCollection);
|
|
60
69
|
}
|
|
@@ -19,101 +19,3 @@ export function vectorToFloatBinary(vector) {
|
|
|
19
19
|
buffer.writeUInt8(1, vector.length * 4);
|
|
20
20
|
return buffer;
|
|
21
21
|
}
|
|
22
|
-
export function vectorToBitBinary(vector) {
|
|
23
|
-
// Mirrors YDB's TKnnBitVectorSerializer (Knn::ToBinaryStringBit) layout:
|
|
24
|
-
// - Packed bits as integers written in native endianness
|
|
25
|
-
// - Then 1 byte: count of unused bits in the last data byte
|
|
26
|
-
// - Then 1 byte: format marker (10 = BitVector)
|
|
27
|
-
//
|
|
28
|
-
// Source: https://raw.githubusercontent.com/ydb-platform/ydb/0b506f56e399e0b4e6a6a4267799da68a3164bf7/ydb/library/yql/udfs/common/knn/knn-serializer.h
|
|
29
|
-
const bitLen = vector.length;
|
|
30
|
-
const dataByteLen = Math.ceil(bitLen / 8);
|
|
31
|
-
const totalLen = dataByteLen + 2; // +1 unused-bit-count +1 format marker
|
|
32
|
-
const buffer = Buffer.alloc(totalLen);
|
|
33
|
-
let offset = 0;
|
|
34
|
-
const writeU64 = (v) => {
|
|
35
|
-
if (IS_LITTLE_ENDIAN) {
|
|
36
|
-
buffer.writeBigUInt64LE(v, offset);
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
buffer.writeBigUInt64BE(v, offset);
|
|
40
|
-
}
|
|
41
|
-
offset += 8;
|
|
42
|
-
};
|
|
43
|
-
const writeU32 = (v) => {
|
|
44
|
-
if (IS_LITTLE_ENDIAN) {
|
|
45
|
-
buffer.writeUInt32LE(v, offset);
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
buffer.writeUInt32BE(v, offset);
|
|
49
|
-
}
|
|
50
|
-
offset += 4;
|
|
51
|
-
};
|
|
52
|
-
const writeU16 = (v) => {
|
|
53
|
-
if (IS_LITTLE_ENDIAN) {
|
|
54
|
-
buffer.writeUInt16LE(v, offset);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
buffer.writeUInt16BE(v, offset);
|
|
58
|
-
}
|
|
59
|
-
offset += 2;
|
|
60
|
-
};
|
|
61
|
-
const writeU8 = (v) => {
|
|
62
|
-
buffer.writeUInt8(v, offset);
|
|
63
|
-
offset += 1;
|
|
64
|
-
};
|
|
65
|
-
let accumulator = 0n;
|
|
66
|
-
let filledBits = 0;
|
|
67
|
-
for (let i = 0; i < vector.length; i += 1) {
|
|
68
|
-
const value = vector[i];
|
|
69
|
-
if (!Number.isFinite(value)) {
|
|
70
|
-
throw new Error(`Non-finite value in vector at index ${i}: ${value}`);
|
|
71
|
-
}
|
|
72
|
-
if (value > 0) {
|
|
73
|
-
accumulator |= 1n;
|
|
74
|
-
}
|
|
75
|
-
filledBits += 1;
|
|
76
|
-
if (filledBits === 64) {
|
|
77
|
-
writeU64(accumulator);
|
|
78
|
-
accumulator = 0n;
|
|
79
|
-
filledBits = 0;
|
|
80
|
-
}
|
|
81
|
-
accumulator <<= 1n;
|
|
82
|
-
}
|
|
83
|
-
accumulator >>= 1n;
|
|
84
|
-
filledBits += 7;
|
|
85
|
-
const tailWriteIf = (bits) => {
|
|
86
|
-
if (filledBits < bits) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (bits === 64) {
|
|
90
|
-
writeU64(accumulator & 0xffffffffffffffffn);
|
|
91
|
-
filledBits -= 64;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (bits === 32) {
|
|
95
|
-
writeU32(Number(accumulator & 0xffffffffn));
|
|
96
|
-
accumulator >>= 32n;
|
|
97
|
-
filledBits -= 32;
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (bits === 16) {
|
|
101
|
-
writeU16(Number(accumulator & 0xffffn));
|
|
102
|
-
accumulator >>= 16n;
|
|
103
|
-
filledBits -= 16;
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
writeU8(Number(accumulator & 0xffn));
|
|
107
|
-
accumulator >>= 8n;
|
|
108
|
-
filledBits -= 8;
|
|
109
|
-
};
|
|
110
|
-
tailWriteIf(64);
|
|
111
|
-
tailWriteIf(32);
|
|
112
|
-
tailWriteIf(16);
|
|
113
|
-
tailWriteIf(8);
|
|
114
|
-
// After tail writes, we must have < 8 "filledBits" left.
|
|
115
|
-
const unusedBitsInLastByte = 7 - filledBits;
|
|
116
|
-
writeU8(unusedBitsInLastByte);
|
|
117
|
-
writeU8(10);
|
|
118
|
-
return buffer;
|
|
119
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isOutOfBufferMemoryYdbError(error: unknown): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function isOutOfBufferMemoryYdbError(error) {
|
|
2
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3
|
+
if (/Out of buffer memory/i.test(msg)) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
if (typeof error === "object" && error !== null && "issues" in error) {
|
|
7
|
+
const issues = error.issues;
|
|
8
|
+
if (issues !== undefined) {
|
|
9
|
+
const issuesText = typeof issues === "string" ? issues : JSON.stringify(issues);
|
|
10
|
+
return /Out of buffer memory/i.test(issuesText);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import { Column, TableDescription, Types, destroyDriver, readyOrThrow, withSession, } from "./client.js";
|
|
2
3
|
function isTableNotFoundError(err) {
|
|
3
4
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -30,7 +31,7 @@ export async function bootstrapMetaTable() {
|
|
|
30
31
|
throw err;
|
|
31
32
|
}
|
|
32
33
|
const td = new TableDescription()
|
|
33
|
-
.withColumns(new Column("collection", Types.UTF8), new Column("table_name", Types.UTF8), new Column("vector_dimension", Types.UINT32), new Column("distance", Types.UTF8), new Column("vector_type", Types.UTF8), new Column("created_at", Types.TIMESTAMP), new Column("last_accessed_at", Types.TIMESTAMP))
|
|
34
|
+
.withColumns(new Column("collection", Types.UTF8), new Column("table_name", Types.UTF8), new Column("vector_dimension", Types.UINT32), new Column("distance", Types.UTF8), new Column("vector_type", Types.UTF8), new Column("created_at", Types.TIMESTAMP), new Column("last_accessed_at", Types.TIMESTAMP), new Column("user_uid", Types.optional(Types.UTF8)))
|
|
34
35
|
.withPrimaryKey("collection");
|
|
35
36
|
try {
|
|
36
37
|
await s.createTable("qdr__collections", td);
|
|
@@ -48,10 +49,21 @@ export async function bootstrapMetaTable() {
|
|
|
48
49
|
if (!hasLastAccessedAt) {
|
|
49
50
|
throw new Error("bootstrapMetaTable: qdr__collections exists but is missing required column last_accessed_at");
|
|
50
51
|
}
|
|
52
|
+
const hasUserUid = cols.some((c) => c.name === "user_uid");
|
|
53
|
+
if (!hasUserUid) {
|
|
54
|
+
throw new Error("bootstrapMetaTable: qdr__collections exists but is missing required column user_uid");
|
|
55
|
+
}
|
|
51
56
|
});
|
|
52
57
|
}
|
|
58
|
+
function isBootstrapMetaTableEntrypoint(argv1) {
|
|
59
|
+
if (!argv1) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const entry = basename(argv1);
|
|
63
|
+
return (entry === "bootstrapMetaTable.ts" || entry === "bootstrapMetaTable.js");
|
|
64
|
+
}
|
|
53
65
|
// CLI entrypoint
|
|
54
|
-
if (process.argv[1]
|
|
66
|
+
if (isBootstrapMetaTableEntrypoint(process.argv[1])) {
|
|
55
67
|
const main = async () => {
|
|
56
68
|
await bootstrapMetaTable();
|
|
57
69
|
// Important: ydb-sdk driver/session pool keeps timers alive; explicitly
|
package/dist/ydb/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Session, IAuthService, ExecuteQuerySettings as YdbExecuteQuerySettings, BulkUpsertSettings as YdbBulkUpsertSettings, QuerySession } from "ydb-sdk";
|
|
2
|
-
declare const Types: typeof import("ydb-sdk").Types, TypedValues: typeof import("ydb-sdk").TypedValues, TableDescription: typeof import("ydb-sdk").TableDescription, Column: typeof import("ydb-sdk").Column, ExecuteQuerySettings: typeof YdbExecuteQuerySettings, BulkUpsertSettings: typeof YdbBulkUpsertSettings, Ydb: typeof import("ydb-sdk-proto").Ydb;
|
|
3
|
-
export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
|
|
2
|
+
declare const Types: typeof import("ydb-sdk").Types, TypedValues: typeof import("ydb-sdk").TypedValues, TableDescription: typeof import("ydb-sdk").TableDescription, Column: typeof import("ydb-sdk").Column, TableIndex: typeof import("ydb-sdk").TableIndex, AlterTableDescription: typeof import("ydb-sdk").AlterTableDescription, ExecuteQuerySettings: typeof YdbExecuteQuerySettings, BulkUpsertSettings: typeof YdbBulkUpsertSettings, Ydb: typeof import("ydb-sdk-proto").Ydb;
|
|
3
|
+
export { Types, TypedValues, TableDescription, Column, TableIndex, AlterTableDescription, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
|
|
4
4
|
export declare function createExecuteQuerySettings(options?: {
|
|
5
5
|
keepInCache?: boolean;
|
|
6
6
|
idempotent?: boolean;
|
|
@@ -26,6 +26,14 @@ export declare function __resetRefreshStateForTests(): void;
|
|
|
26
26
|
export declare function configureDriver(config: DriverConfig): void;
|
|
27
27
|
export declare function readyOrThrow(): Promise<void>;
|
|
28
28
|
export declare function withSession<T>(fn: (s: Session) => Promise<T>): Promise<T>;
|
|
29
|
+
/**
|
|
30
|
+
* Runs a callback in a single session without re-running the callback on session errors.
|
|
31
|
+
*
|
|
32
|
+
* Use this for multi-step callbacks where rerunning from the beginning would cause
|
|
33
|
+
* incorrect counters or repeated side effects. Prefer `withSession()` for single-shot
|
|
34
|
+
* operations to auto-retry on BadSession/SessionBusy via ydb-sdk.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withSessionOnce<T>(fn: (s: Session) => Promise<T>): Promise<T>;
|
|
29
37
|
export declare function withQuerySession<T>(fn: (s: QuerySession) => Promise<T>, options?: {
|
|
30
38
|
timeoutMs?: number;
|
|
31
39
|
idempotent?: boolean;
|
package/dist/ydb/client.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
-
import {
|
|
2
|
+
import { SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, STARTUP_PROBE_SESSION_TIMEOUT_MS, YDB_SESSION_RETRY_MAX_RETRIES, TABLE_SESSION_TIMEOUT_MS, resolveYdbConnectionConfig, } from "../config/env.js";
|
|
3
3
|
import { logger } from "../logging/logger.js";
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
|
-
const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, OperationParams, Ydb, } = require("ydb-sdk");
|
|
6
|
-
export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
|
|
5
|
+
const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, TableIndex, AlterTableDescription, ExecuteQuerySettings, BulkUpsertSettings, OperationParams, Ydb, } = require("ydb-sdk");
|
|
6
|
+
export { Types, TypedValues, TableDescription, Column, TableIndex, AlterTableDescription, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
|
|
7
7
|
export function createExecuteQuerySettings(options) {
|
|
8
8
|
const { keepInCache = true, idempotent = true } = options ?? {};
|
|
9
9
|
const settings = new ExecuteQuerySettings();
|
|
@@ -36,9 +36,9 @@ export function createBulkUpsertSettingsWithTimeout(options) {
|
|
|
36
36
|
return settings;
|
|
37
37
|
}
|
|
38
38
|
const DRIVER_READY_TIMEOUT_MS = 15000;
|
|
39
|
-
const TABLE_SESSION_TIMEOUT_MS = 20000;
|
|
40
39
|
const YDB_HEALTHCHECK_READY_TIMEOUT_MS = 5000;
|
|
41
40
|
const DRIVER_REFRESH_COOLDOWN_MS = 30000;
|
|
41
|
+
const DRIVER_DESTROY_GRACE_PERIOD_MS = TABLE_SESSION_TIMEOUT_MS + 5000;
|
|
42
42
|
let overrideConfig;
|
|
43
43
|
let driver;
|
|
44
44
|
let lastDriverRefreshAt = 0;
|
|
@@ -50,8 +50,8 @@ export function isCompilationTimeoutError(error) {
|
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
52
|
const msg = error.message ?? "";
|
|
53
|
-
if (/
|
|
54
|
-
/
|
|
53
|
+
if (/Query compilation timed out/i.test(msg) ||
|
|
54
|
+
(/Timeout \(code 400090\)/i.test(msg) && /compilation/i.test(msg))) {
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
57
|
// Startup probe uses explicit cancel-after; YDB returns Cancelled with
|
|
@@ -130,17 +130,13 @@ export function configureDriver(config) {
|
|
|
130
130
|
}
|
|
131
131
|
overrideConfig = config;
|
|
132
132
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
endpoint: overrideConfig?.endpoint ?? YDB_ENDPOINT,
|
|
141
|
-
database: overrideConfig?.database ?? YDB_DATABASE,
|
|
142
|
-
};
|
|
143
|
-
const driverConfig = {
|
|
133
|
+
function getDriverConfig() {
|
|
134
|
+
const base = resolveYdbConnectionConfig({
|
|
135
|
+
endpoint: overrideConfig?.endpoint,
|
|
136
|
+
database: overrideConfig?.database,
|
|
137
|
+
connectionString: overrideConfig?.connectionString,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
144
140
|
...base,
|
|
145
141
|
authService: overrideConfig?.authService ?? getCredentialsFromEnv(),
|
|
146
142
|
poolSettings: {
|
|
@@ -149,7 +145,10 @@ function getOrCreateDriver() {
|
|
|
149
145
|
keepAlivePeriod: SESSION_KEEPALIVE_PERIOD_MS,
|
|
150
146
|
},
|
|
151
147
|
};
|
|
152
|
-
|
|
148
|
+
}
|
|
149
|
+
function createDriverInstance() {
|
|
150
|
+
const driverConfig = getDriverConfig();
|
|
151
|
+
const created = driverFactoryOverride
|
|
153
152
|
? driverFactoryOverride(driverConfig)
|
|
154
153
|
: new Driver(driverConfig);
|
|
155
154
|
logger.info({
|
|
@@ -157,6 +156,13 @@ function getOrCreateDriver() {
|
|
|
157
156
|
poolMaxSize: SESSION_POOL_MAX_SIZE,
|
|
158
157
|
keepAlivePeriodMs: SESSION_KEEPALIVE_PERIOD_MS,
|
|
159
158
|
}, "YDB driver created with session pool settings");
|
|
159
|
+
return created;
|
|
160
|
+
}
|
|
161
|
+
function getOrCreateDriver() {
|
|
162
|
+
if (driver) {
|
|
163
|
+
return driver;
|
|
164
|
+
}
|
|
165
|
+
driver = createDriverInstance();
|
|
160
166
|
return driver;
|
|
161
167
|
}
|
|
162
168
|
export async function readyOrThrow() {
|
|
@@ -167,6 +173,23 @@ export async function readyOrThrow() {
|
|
|
167
173
|
}
|
|
168
174
|
}
|
|
169
175
|
export async function withSession(fn) {
|
|
176
|
+
const d = getOrCreateDriver();
|
|
177
|
+
try {
|
|
178
|
+
return await d.tableClient.withSessionRetry(fn, TABLE_SESSION_TIMEOUT_MS, YDB_SESSION_RETRY_MAX_RETRIES);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
void maybeRefreshDriverOnSessionError(err);
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Runs a callback in a single session without re-running the callback on session errors.
|
|
187
|
+
*
|
|
188
|
+
* Use this for multi-step callbacks where rerunning from the beginning would cause
|
|
189
|
+
* incorrect counters or repeated side effects. Prefer `withSession()` for single-shot
|
|
190
|
+
* operations to auto-retry on BadSession/SessionBusy via ydb-sdk.
|
|
191
|
+
*/
|
|
192
|
+
export async function withSessionOnce(fn) {
|
|
170
193
|
const d = getOrCreateDriver();
|
|
171
194
|
try {
|
|
172
195
|
return await d.tableClient.withSession(fn, TABLE_SESSION_TIMEOUT_MS);
|
|
@@ -194,7 +217,7 @@ export async function withQuerySession(fn, options) {
|
|
|
194
217
|
export async function withStartupProbeSession(fn) {
|
|
195
218
|
const d = getOrCreateDriver();
|
|
196
219
|
try {
|
|
197
|
-
return await d.tableClient.
|
|
220
|
+
return await d.tableClient.withSessionRetry(fn, STARTUP_PROBE_SESSION_TIMEOUT_MS, YDB_SESSION_RETRY_MAX_RETRIES);
|
|
198
221
|
}
|
|
199
222
|
catch (err) {
|
|
200
223
|
void maybeRefreshDriverOnSessionError(err);
|
|
@@ -219,13 +242,14 @@ export async function destroyDriver() {
|
|
|
219
242
|
return;
|
|
220
243
|
}
|
|
221
244
|
logger.info("Destroying YDB driver and session pool");
|
|
245
|
+
const toDestroy = driver;
|
|
246
|
+
driver = undefined;
|
|
222
247
|
try {
|
|
223
|
-
await
|
|
248
|
+
await toDestroy.destroy();
|
|
224
249
|
}
|
|
225
250
|
catch (err) {
|
|
226
251
|
logger.warn({ err }, "Error during driver destruction (ignored)");
|
|
227
252
|
}
|
|
228
|
-
driver = undefined;
|
|
229
253
|
}
|
|
230
254
|
/**
|
|
231
255
|
* Destroys the current driver and immediately creates a fresh one.
|
|
@@ -233,7 +257,42 @@ export async function destroyDriver() {
|
|
|
233
257
|
*/
|
|
234
258
|
export async function refreshDriver() {
|
|
235
259
|
logger.info("Refreshing YDB driver");
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
260
|
+
const oldDriver = driver;
|
|
261
|
+
const newDriver = createDriverInstance();
|
|
262
|
+
let ok;
|
|
263
|
+
try {
|
|
264
|
+
ok = await newDriver.ready(DRIVER_READY_TIMEOUT_MS);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
try {
|
|
268
|
+
await newDriver.destroy();
|
|
269
|
+
}
|
|
270
|
+
catch (destroyErr) {
|
|
271
|
+
logger.warn({ err: destroyErr }, "Error during fresh driver destruction after failed ready() (ignored)");
|
|
272
|
+
}
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
if (!ok) {
|
|
276
|
+
try {
|
|
277
|
+
await newDriver.destroy();
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
logger.warn({ err }, "Error during fresh driver destruction after failed ready() (ignored)");
|
|
281
|
+
}
|
|
282
|
+
throw new Error(`YDB driver is not ready in ${DRIVER_READY_TIMEOUT_MS / 1000}s. Check connectivity and credentials.`);
|
|
283
|
+
}
|
|
284
|
+
driver = newDriver;
|
|
285
|
+
logger.info({ gracePeriodMs: DRIVER_DESTROY_GRACE_PERIOD_MS }, "YDB driver swapped successfully; scheduling old driver destruction");
|
|
286
|
+
if (oldDriver) {
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
void (async () => {
|
|
289
|
+
try {
|
|
290
|
+
await oldDriver.destroy();
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
logger.warn({ err }, "Error during delayed old driver destruction (ignored)");
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
296
|
+
}, DRIVER_DESTROY_GRACE_PERIOD_MS);
|
|
297
|
+
}
|
|
239
298
|
}
|
package/dist/ydb/helpers.d.ts
CHANGED
package/dist/ydb/helpers.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { vectorToFloatBinary
|
|
1
|
+
import { vectorToFloatBinary } from "../utils/vectorBinary.js";
|
|
2
2
|
export function buildVectorBinaryParams(vector) {
|
|
3
3
|
return {
|
|
4
4
|
float: vectorToFloatBinary(vector),
|
|
5
|
-
bit: vectorToBitBinary(vector),
|
|
6
5
|
};
|
|
7
6
|
}
|
package/dist/ydb/schema.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const GLOBAL_POINTS_TABLE = "qdrant_all_points";
|
|
2
|
+
export declare const POINTS_BY_FILE_LOOKUP_TABLE = "qdrant_points_by_file";
|
|
2
3
|
export { UPSERT_BATCH_SIZE } from "../config/env.js";
|
|
3
4
|
export declare function ensureMetaTable(): Promise<void>;
|
|
4
5
|
export declare function ensureGlobalPointsTable(): Promise<void>;
|
|
6
|
+
export declare function ensurePointsByFileTable(): Promise<void>;
|