ydb-qdrant 4.6.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/config/env.d.ts +8 -0
- package/dist/config/env.js +42 -0
- package/dist/repositories/pointsRepo.js +3 -1
- package/dist/repositories/pointsRepo.multi-table.js +35 -20
- package/dist/repositories/pointsRepo.one-table.d.ts +2 -1
- package/dist/repositories/pointsRepo.one-table.js +360 -34
- package/dist/utils/vectorBinary.d.ts +2 -0
- package/dist/utils/vectorBinary.js +35 -0
- package/dist/ydb/helpers.d.ts +4 -0
- package/dist/ydb/helpers.js +7 -0
- package/dist/ydb/schema.d.ts +1 -0
- package/dist/ydb/schema.js +11 -34
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-build.yml)
|
|
4
4
|
[](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-tests.yml)
|
|
5
5
|
[](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-integration.yml)
|
|
6
|
+
[](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-load-soak.yml)
|
|
7
|
+
[](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-load-stress.yml)
|
|
6
8
|
[](https://coveralls.io/github/astandrik/ydb-qdrant?branch=main)
|
|
7
9
|
|
|
8
10
|
[](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-recall.yml)
|
|
@@ -90,6 +92,9 @@ export PORT=8080
|
|
|
90
92
|
export LOG_LEVEL=info
|
|
91
93
|
# Collection storage mode (optional; default is multi_table)
|
|
92
94
|
export YDB_QDRANT_COLLECTION_STORAGE_MODE=multi_table # or one_table
|
|
95
|
+
# One-table search tuning (one_table mode only)
|
|
96
|
+
export YDB_QDRANT_SEARCH_MODE=approximate # or exact
|
|
97
|
+
export YDB_QDRANT_OVERFETCH_MULTIPLIER=10 # candidate multiplier in approximate mode
|
|
93
98
|
```
|
|
94
99
|
|
|
95
100
|
## Use as a Node.js library (npm package)
|
package/dist/config/env.d.ts
CHANGED
|
@@ -11,3 +11,11 @@ export declare const COLLECTION_STORAGE_MODE: CollectionStorageMode;
|
|
|
11
11
|
export declare const GLOBAL_POINTS_AUTOMIGRATE_ENABLED: boolean;
|
|
12
12
|
export declare const VECTOR_INDEX_BUILD_ENABLED: boolean;
|
|
13
13
|
export declare function isOneTableMode(mode: CollectionStorageMode): mode is CollectionStorageMode.OneTable;
|
|
14
|
+
export declare enum SearchMode {
|
|
15
|
+
Exact = "exact",
|
|
16
|
+
Approximate = "approximate"
|
|
17
|
+
}
|
|
18
|
+
export declare const SEARCH_MODE: SearchMode;
|
|
19
|
+
export declare const OVERFETCH_MULTIPLIER: number;
|
|
20
|
+
export declare const CLIENT_SIDE_SERIALIZATION_ENABLED: boolean;
|
|
21
|
+
export declare const UPSERT_BATCH_SIZE: number;
|
package/dist/config/env.js
CHANGED
|
@@ -3,6 +3,23 @@ export const YDB_ENDPOINT = process.env.YDB_ENDPOINT ?? "";
|
|
|
3
3
|
export const YDB_DATABASE = process.env.YDB_DATABASE ?? "";
|
|
4
4
|
export const PORT = process.env.PORT ? Number(process.env.PORT) : 8080;
|
|
5
5
|
export const LOG_LEVEL = process.env.LOG_LEVEL ?? "info";
|
|
6
|
+
function parseIntegerEnv(value, defaultValue, opts) {
|
|
7
|
+
if (value === undefined) {
|
|
8
|
+
return defaultValue;
|
|
9
|
+
}
|
|
10
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
11
|
+
if (!Number.isFinite(parsed)) {
|
|
12
|
+
return defaultValue;
|
|
13
|
+
}
|
|
14
|
+
let result = parsed;
|
|
15
|
+
if (opts?.min !== undefined && result < opts.min) {
|
|
16
|
+
result = opts.min;
|
|
17
|
+
}
|
|
18
|
+
if (opts?.max !== undefined && result > opts.max) {
|
|
19
|
+
result = opts.max;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
6
23
|
function parseBooleanEnv(value, defaultValue) {
|
|
7
24
|
if (value === undefined) {
|
|
8
25
|
return defaultValue;
|
|
@@ -36,3 +53,28 @@ export const VECTOR_INDEX_BUILD_ENABLED = parseBooleanEnv(process.env.VECTOR_IND
|
|
|
36
53
|
export function isOneTableMode(mode) {
|
|
37
54
|
return mode === CollectionStorageMode.OneTable;
|
|
38
55
|
}
|
|
56
|
+
export var SearchMode;
|
|
57
|
+
(function (SearchMode) {
|
|
58
|
+
SearchMode["Exact"] = "exact";
|
|
59
|
+
SearchMode["Approximate"] = "approximate";
|
|
60
|
+
})(SearchMode || (SearchMode = {}));
|
|
61
|
+
function resolveSearchModeEnv(mode) {
|
|
62
|
+
const raw = process.env.YDB_QDRANT_SEARCH_MODE;
|
|
63
|
+
const normalized = raw?.trim().toLowerCase();
|
|
64
|
+
if (normalized === SearchMode.Exact) {
|
|
65
|
+
return SearchMode.Exact;
|
|
66
|
+
}
|
|
67
|
+
if (normalized === SearchMode.Approximate) {
|
|
68
|
+
return SearchMode.Approximate;
|
|
69
|
+
}
|
|
70
|
+
// Default: keep current behavior for one-table (approximate two-phase search).
|
|
71
|
+
if (isOneTableMode(mode)) {
|
|
72
|
+
return SearchMode.Approximate;
|
|
73
|
+
}
|
|
74
|
+
// For multi-table, this value is currently unused but defaults to approximate.
|
|
75
|
+
return SearchMode.Approximate;
|
|
76
|
+
}
|
|
77
|
+
export const SEARCH_MODE = resolveSearchModeEnv(COLLECTION_STORAGE_MODE);
|
|
78
|
+
export const OVERFETCH_MULTIPLIER = parseIntegerEnv(process.env.YDB_QDRANT_OVERFETCH_MULTIPLIER, 10, { min: 1 });
|
|
79
|
+
export const CLIENT_SIDE_SERIALIZATION_ENABLED = parseBooleanEnv(process.env.YDB_QDRANT_CLIENT_SIDE_SERIALIZATION_ENABLED, false);
|
|
80
|
+
export const UPSERT_BATCH_SIZE = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_BATCH_SIZE, 100, { min: 1 });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { upsertPointsMultiTable, searchPointsMultiTable, deletePointsMultiTable, } from "./pointsRepo.multi-table.js";
|
|
2
|
+
import { SEARCH_MODE, OVERFETCH_MULTIPLIER, } from "../config/env.js";
|
|
2
3
|
import { upsertPointsOneTable, searchPointsOneTable, deletePointsOneTable, } from "./pointsRepo.one-table.js";
|
|
3
4
|
export async function upsertPoints(tableName, points, dimension, uid) {
|
|
4
5
|
if (uid) {
|
|
@@ -7,8 +8,9 @@ export async function upsertPoints(tableName, points, dimension, uid) {
|
|
|
7
8
|
return await upsertPointsMultiTable(tableName, points, dimension);
|
|
8
9
|
}
|
|
9
10
|
export async function searchPoints(tableName, queryVector, top, withPayload, distance, dimension, uid) {
|
|
11
|
+
const mode = SEARCH_MODE;
|
|
10
12
|
if (uid) {
|
|
11
|
-
return await searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid);
|
|
13
|
+
return await searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid, mode, OVERFETCH_MULTIPLIER);
|
|
12
14
|
}
|
|
13
15
|
return await searchPointsMultiTable(tableName, queryVector, top, withPayload, distance, dimension);
|
|
14
16
|
}
|
|
@@ -1,39 +1,54 @@
|
|
|
1
|
-
import { TypedValues, withSession } from "../ydb/client.js";
|
|
2
|
-
import {
|
|
1
|
+
import { Types, TypedValues, withSession } from "../ydb/client.js";
|
|
2
|
+
import { buildVectorParam } from "../ydb/helpers.js";
|
|
3
3
|
import { logger } from "../logging/logger.js";
|
|
4
4
|
import { notifyUpsert } from "../indexing/IndexScheduler.js";
|
|
5
5
|
import { VECTOR_INDEX_BUILD_ENABLED } from "../config/env.js";
|
|
6
6
|
import { mapDistanceToKnnFn } from "../utils/distance.js";
|
|
7
7
|
import { withRetry, isTransientYdbError } from "../utils/retry.js";
|
|
8
|
+
import { UPSERT_BATCH_SIZE } from "../ydb/schema.js";
|
|
8
9
|
export async function upsertPointsMultiTable(tableName, points, dimension) {
|
|
10
|
+
for (const p of points) {
|
|
11
|
+
const id = String(p.id);
|
|
12
|
+
if (p.vector.length !== dimension) {
|
|
13
|
+
throw new Error(`Vector dimension mismatch for id=${id}: got ${p.vector.length}, expected ${dimension}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
9
16
|
let upserted = 0;
|
|
10
17
|
await withSession(async (s) => {
|
|
11
|
-
for (
|
|
12
|
-
const
|
|
13
|
-
if (p.vector.length !== dimension) {
|
|
14
|
-
throw new Error(`Vector dimension mismatch for id=${id}: got ${p.vector.length}, expected ${dimension}`);
|
|
15
|
-
}
|
|
18
|
+
for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
|
|
19
|
+
const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
|
|
16
20
|
const ddl = `
|
|
17
|
-
DECLARE $
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
DECLARE $rows AS List<Struct<
|
|
22
|
+
point_id: Utf8,
|
|
23
|
+
vec: List<Float>,
|
|
24
|
+
payload: JsonDocument
|
|
25
|
+
>>;
|
|
26
|
+
|
|
20
27
|
UPSERT INTO ${tableName} (point_id, embedding, payload)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Untag(Knn::ToBinaryStringFloat(
|
|
24
|
-
|
|
25
|
-
);
|
|
28
|
+
SELECT
|
|
29
|
+
point_id,
|
|
30
|
+
Untag(Knn::ToBinaryStringFloat(vec), "FloatVector") AS embedding,
|
|
31
|
+
payload
|
|
32
|
+
FROM AS_TABLE($rows);
|
|
26
33
|
`;
|
|
34
|
+
const rowType = Types.struct({
|
|
35
|
+
point_id: Types.UTF8,
|
|
36
|
+
vec: Types.list(Types.FLOAT),
|
|
37
|
+
payload: Types.JSON_DOCUMENT,
|
|
38
|
+
});
|
|
39
|
+
const rowsValue = TypedValues.list(rowType, batch.map((p) => ({
|
|
40
|
+
point_id: String(p.id),
|
|
41
|
+
vec: p.vector,
|
|
42
|
+
payload: JSON.stringify(p.payload ?? {}),
|
|
43
|
+
})));
|
|
27
44
|
const params = {
|
|
28
|
-
$
|
|
29
|
-
$vec: buildVectorParam(p.vector),
|
|
30
|
-
$payload: buildJsonOrEmpty(p.payload),
|
|
45
|
+
$rows: rowsValue,
|
|
31
46
|
};
|
|
32
47
|
await withRetry(() => s.executeQuery(ddl, params), {
|
|
33
48
|
isTransient: isTransientYdbError,
|
|
34
|
-
context: { tableName,
|
|
49
|
+
context: { tableName, batchSize: batch.length },
|
|
35
50
|
});
|
|
36
|
-
upserted +=
|
|
51
|
+
upserted += batch.length;
|
|
37
52
|
}
|
|
38
53
|
});
|
|
39
54
|
notifyUpsert(tableName, upserted);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { DistanceKind } from "../types";
|
|
2
|
+
import { SearchMode } from "../config/env.js";
|
|
2
3
|
export declare function upsertPointsOneTable(tableName: string, points: Array<{
|
|
3
4
|
id: string | number;
|
|
4
5
|
vector: number[];
|
|
5
6
|
payload?: Record<string, unknown>;
|
|
6
7
|
}>, dimension: number, uid: string): Promise<number>;
|
|
7
|
-
export declare function searchPointsOneTable(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number, uid: string): Promise<Array<{
|
|
8
|
+
export declare function searchPointsOneTable(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number, uid: string, mode: SearchMode | undefined, overfetchMultiplier: number): Promise<Array<{
|
|
8
9
|
id: string;
|
|
9
10
|
score: number;
|
|
10
11
|
payload?: Record<string, unknown>;
|
|
@@ -1,56 +1,345 @@
|
|
|
1
1
|
import { TypedValues, Types, withSession } from "../ydb/client.js";
|
|
2
|
-
import {
|
|
2
|
+
import { buildVectorParam, buildVectorBinaryParams } from "../ydb/helpers.js";
|
|
3
3
|
import { notifyUpsert } from "../indexing/IndexScheduler.js";
|
|
4
4
|
import { mapDistanceToKnnFn, mapDistanceToBitKnnFn, } from "../utils/distance.js";
|
|
5
5
|
import { withRetry, isTransientYdbError } from "../utils/retry.js";
|
|
6
|
+
import { UPSERT_BATCH_SIZE } from "../ydb/schema.js";
|
|
7
|
+
import { CLIENT_SIDE_SERIALIZATION_ENABLED, SearchMode, } from "../config/env.js";
|
|
8
|
+
import { logger } from "../logging/logger.js";
|
|
6
9
|
export async function upsertPointsOneTable(tableName, points, dimension, uid) {
|
|
10
|
+
for (const p of points) {
|
|
11
|
+
const id = String(p.id);
|
|
12
|
+
if (p.vector.length !== dimension) {
|
|
13
|
+
const previewLength = Math.min(16, p.vector.length);
|
|
14
|
+
const vectorPreview = previewLength > 0 ? p.vector.slice(0, previewLength) : [];
|
|
15
|
+
logger.warn({
|
|
16
|
+
tableName,
|
|
17
|
+
uid,
|
|
18
|
+
pointId: id,
|
|
19
|
+
vectorLen: p.vector.length,
|
|
20
|
+
expectedDimension: dimension,
|
|
21
|
+
vectorPreview,
|
|
22
|
+
}, "upsertPointsOneTable: vector dimension mismatch");
|
|
23
|
+
throw new Error(`Vector dimension mismatch for id=${id}: got ${p.vector.length}, expected ${dimension}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
7
26
|
let upserted = 0;
|
|
8
27
|
await withSession(async (s) => {
|
|
9
|
-
for (
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
28
|
+
for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
|
|
29
|
+
const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
|
|
30
|
+
let ddl;
|
|
31
|
+
let params;
|
|
32
|
+
if (CLIENT_SIDE_SERIALIZATION_ENABLED) {
|
|
33
|
+
ddl = `
|
|
34
|
+
DECLARE $rows AS List<Struct<
|
|
35
|
+
uid: Utf8,
|
|
36
|
+
point_id: Utf8,
|
|
37
|
+
embedding: String,
|
|
38
|
+
embedding_quantized: String,
|
|
39
|
+
payload: JsonDocument
|
|
40
|
+
>>;
|
|
41
|
+
|
|
42
|
+
UPSERT INTO ${tableName} (uid, point_id, embedding, embedding_quantized, payload)
|
|
43
|
+
SELECT
|
|
44
|
+
uid,
|
|
45
|
+
point_id,
|
|
46
|
+
embedding,
|
|
47
|
+
embedding_quantized,
|
|
48
|
+
payload
|
|
49
|
+
FROM AS_TABLE($rows);
|
|
50
|
+
`;
|
|
51
|
+
const rowType = Types.struct({
|
|
52
|
+
uid: Types.UTF8,
|
|
53
|
+
point_id: Types.UTF8,
|
|
54
|
+
embedding: Types.BYTES,
|
|
55
|
+
embedding_quantized: Types.BYTES,
|
|
56
|
+
payload: Types.JSON_DOCUMENT,
|
|
57
|
+
});
|
|
58
|
+
const rowsValue = TypedValues.list(rowType, batch.map((p) => {
|
|
59
|
+
const binaries = buildVectorBinaryParams(p.vector);
|
|
60
|
+
return {
|
|
61
|
+
uid,
|
|
62
|
+
point_id: String(p.id),
|
|
63
|
+
embedding: binaries.float,
|
|
64
|
+
embedding_quantized: binaries.bit,
|
|
65
|
+
payload: JSON.stringify(p.payload ?? {}),
|
|
66
|
+
};
|
|
67
|
+
}));
|
|
68
|
+
params = {
|
|
69
|
+
$rows: rowsValue,
|
|
70
|
+
};
|
|
13
71
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
72
|
+
else {
|
|
73
|
+
ddl = `
|
|
74
|
+
DECLARE $rows AS List<Struct<
|
|
75
|
+
uid: Utf8,
|
|
76
|
+
point_id: Utf8,
|
|
77
|
+
vec: List<Float>,
|
|
78
|
+
payload: JsonDocument
|
|
79
|
+
>>;
|
|
80
|
+
|
|
81
|
+
UPSERT INTO ${tableName} (uid, point_id, embedding, embedding_quantized, payload)
|
|
82
|
+
SELECT
|
|
83
|
+
uid,
|
|
84
|
+
point_id,
|
|
85
|
+
Untag(Knn::ToBinaryStringFloat(vec), "FloatVector") AS embedding,
|
|
86
|
+
Untag(Knn::ToBinaryStringBit(vec), "BitVector") AS embedding_quantized,
|
|
87
|
+
payload
|
|
88
|
+
FROM AS_TABLE($rows);
|
|
89
|
+
`;
|
|
90
|
+
const rowType = Types.struct({
|
|
91
|
+
uid: Types.UTF8,
|
|
92
|
+
point_id: Types.UTF8,
|
|
93
|
+
vec: Types.list(Types.FLOAT),
|
|
94
|
+
payload: Types.JSON_DOCUMENT,
|
|
95
|
+
});
|
|
96
|
+
const rowsValue = TypedValues.list(rowType, batch.map((p) => ({
|
|
97
|
+
uid,
|
|
98
|
+
point_id: String(p.id),
|
|
99
|
+
vec: p.vector,
|
|
100
|
+
payload: JSON.stringify(p.payload ?? {}),
|
|
101
|
+
})));
|
|
102
|
+
params = {
|
|
103
|
+
$rows: rowsValue,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
logger.debug({
|
|
107
|
+
tableName,
|
|
108
|
+
mode: CLIENT_SIDE_SERIALIZATION_ENABLED
|
|
109
|
+
? "one_table_upsert_client_side_serialization"
|
|
110
|
+
: "one_table_upsert_server_side_knn",
|
|
111
|
+
batchSize: batch.length,
|
|
112
|
+
yql: ddl,
|
|
113
|
+
params: {
|
|
114
|
+
rows: batch.map((p) => ({
|
|
115
|
+
uid,
|
|
116
|
+
point_id: String(p.id),
|
|
117
|
+
vectorLength: p.vector.length,
|
|
118
|
+
vectorPreview: p.vector.slice(0, 3),
|
|
119
|
+
payload: p.payload ?? {},
|
|
120
|
+
})),
|
|
121
|
+
},
|
|
122
|
+
}, "one_table upsert: executing YQL");
|
|
34
123
|
await withRetry(() => s.executeQuery(ddl, params), {
|
|
35
124
|
isTransient: isTransientYdbError,
|
|
36
|
-
context: { tableName,
|
|
125
|
+
context: { tableName, batchSize: batch.length },
|
|
37
126
|
});
|
|
38
|
-
upserted +=
|
|
127
|
+
upserted += batch.length;
|
|
39
128
|
}
|
|
40
129
|
});
|
|
41
130
|
notifyUpsert(tableName, upserted);
|
|
42
131
|
return upserted;
|
|
43
132
|
}
|
|
44
|
-
|
|
133
|
+
async function searchPointsOneTableExact(tableName, queryVector, top, withPayload, distance, dimension, uid) {
|
|
134
|
+
if (queryVector.length !== dimension) {
|
|
135
|
+
throw new Error(`Vector dimension mismatch: got ${queryVector.length}, expected ${dimension}`);
|
|
136
|
+
}
|
|
137
|
+
const { fn, order } = mapDistanceToKnnFn(distance);
|
|
138
|
+
const results = await withSession(async (s) => {
|
|
139
|
+
let yql;
|
|
140
|
+
let params;
|
|
141
|
+
if (CLIENT_SIDE_SERIALIZATION_ENABLED) {
|
|
142
|
+
const binaries = buildVectorBinaryParams(queryVector);
|
|
143
|
+
yql = `
|
|
144
|
+
DECLARE $qbinf AS String;
|
|
145
|
+
DECLARE $k AS Uint32;
|
|
146
|
+
DECLARE $uid AS Utf8;
|
|
147
|
+
SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
|
|
148
|
+
FROM ${tableName}
|
|
149
|
+
WHERE uid = $uid
|
|
150
|
+
ORDER BY score ${order}
|
|
151
|
+
LIMIT $k;
|
|
152
|
+
`;
|
|
153
|
+
params = {
|
|
154
|
+
$qbinf: typeof TypedValues.bytes === "function"
|
|
155
|
+
? TypedValues.bytes(binaries.float)
|
|
156
|
+
: binaries.float,
|
|
157
|
+
$k: TypedValues.uint32(top),
|
|
158
|
+
$uid: TypedValues.utf8(uid),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const qf = buildVectorParam(queryVector);
|
|
163
|
+
yql = `
|
|
164
|
+
DECLARE $qf AS List<Float>;
|
|
165
|
+
DECLARE $k AS Uint32;
|
|
166
|
+
DECLARE $uid AS Utf8;
|
|
167
|
+
$qbinf = Knn::ToBinaryStringFloat($qf);
|
|
168
|
+
SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
|
|
169
|
+
FROM ${tableName}
|
|
170
|
+
WHERE uid = $uid
|
|
171
|
+
ORDER BY score ${order}
|
|
172
|
+
LIMIT $k;
|
|
173
|
+
`;
|
|
174
|
+
params = {
|
|
175
|
+
$qf: qf,
|
|
176
|
+
$k: TypedValues.uint32(top),
|
|
177
|
+
$uid: TypedValues.utf8(uid),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
logger.debug({
|
|
181
|
+
tableName,
|
|
182
|
+
distance,
|
|
183
|
+
top,
|
|
184
|
+
withPayload,
|
|
185
|
+
mode: CLIENT_SIDE_SERIALIZATION_ENABLED
|
|
186
|
+
? "one_table_exact_client_side_serialization"
|
|
187
|
+
: "one_table_exact",
|
|
188
|
+
yql,
|
|
189
|
+
params: {
|
|
190
|
+
uid,
|
|
191
|
+
top,
|
|
192
|
+
vectorLength: queryVector.length,
|
|
193
|
+
vectorPreview: queryVector.slice(0, 3),
|
|
194
|
+
},
|
|
195
|
+
}, "one_table search (exact): executing YQL");
|
|
196
|
+
const rs = await s.executeQuery(yql, params);
|
|
197
|
+
const rowset = rs.resultSets?.[0];
|
|
198
|
+
const rows = (rowset?.rows ?? []);
|
|
199
|
+
return rows.map((row) => {
|
|
200
|
+
const id = row.items?.[0]?.textValue;
|
|
201
|
+
if (typeof id !== "string") {
|
|
202
|
+
throw new Error("point_id is missing in YDB search result");
|
|
203
|
+
}
|
|
204
|
+
let payload;
|
|
205
|
+
let scoreIdx = 1;
|
|
206
|
+
if (withPayload) {
|
|
207
|
+
const payloadText = row.items?.[1]?.textValue;
|
|
208
|
+
if (payloadText) {
|
|
209
|
+
try {
|
|
210
|
+
payload = JSON.parse(payloadText);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
payload = undefined;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
scoreIdx = 2;
|
|
217
|
+
}
|
|
218
|
+
const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
|
|
219
|
+
return { id, score, ...(payload ? { payload } : {}) };
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
return results;
|
|
223
|
+
}
|
|
224
|
+
async function searchPointsOneTableApproximate(tableName, queryVector, top, withPayload, distance, dimension, uid, overfetchMultiplier) {
|
|
45
225
|
if (queryVector.length !== dimension) {
|
|
46
226
|
throw new Error(`Vector dimension mismatch: got ${queryVector.length}, expected ${dimension}`);
|
|
47
227
|
}
|
|
48
228
|
const { fn, order } = mapDistanceToKnnFn(distance);
|
|
49
229
|
const { fn: bitFn, order: bitOrder } = mapDistanceToBitKnnFn(distance);
|
|
50
|
-
const
|
|
51
|
-
const
|
|
230
|
+
const safeTop = top > 0 ? top : 1;
|
|
231
|
+
const rawCandidateLimit = safeTop * overfetchMultiplier;
|
|
232
|
+
const candidateLimit = Math.max(safeTop, rawCandidateLimit);
|
|
52
233
|
const results = await withSession(async (s) => {
|
|
53
|
-
|
|
234
|
+
if (CLIENT_SIDE_SERIALIZATION_ENABLED) {
|
|
235
|
+
const binaries = buildVectorBinaryParams(queryVector);
|
|
236
|
+
// Phase 1: approximate candidate selection using embedding_quantized
|
|
237
|
+
const phase1Query = `
|
|
238
|
+
DECLARE $qbin_bit AS String;
|
|
239
|
+
DECLARE $k AS Uint32;
|
|
240
|
+
DECLARE $uid AS Utf8;
|
|
241
|
+
SELECT point_id
|
|
242
|
+
FROM ${tableName}
|
|
243
|
+
WHERE uid = $uid AND embedding_quantized IS NOT NULL
|
|
244
|
+
ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
|
|
245
|
+
LIMIT $k;
|
|
246
|
+
`;
|
|
247
|
+
logger.debug({
|
|
248
|
+
tableName,
|
|
249
|
+
distance,
|
|
250
|
+
top,
|
|
251
|
+
safeTop,
|
|
252
|
+
candidateLimit,
|
|
253
|
+
mode: "one_table_approximate_phase1_client_side_serialization",
|
|
254
|
+
yql: phase1Query,
|
|
255
|
+
params: {
|
|
256
|
+
uid,
|
|
257
|
+
top: candidateLimit,
|
|
258
|
+
vectorLength: queryVector.length,
|
|
259
|
+
vectorPreview: queryVector.slice(0, 3),
|
|
260
|
+
},
|
|
261
|
+
}, "one_table search (approximate, phase 1): executing YQL");
|
|
262
|
+
const phase1Params = {
|
|
263
|
+
$qbin_bit: typeof TypedValues.bytes === "function"
|
|
264
|
+
? TypedValues.bytes(binaries.bit)
|
|
265
|
+
: binaries.bit,
|
|
266
|
+
$k: TypedValues.uint32(candidateLimit),
|
|
267
|
+
$uid: TypedValues.utf8(uid),
|
|
268
|
+
};
|
|
269
|
+
const rs1 = await s.executeQuery(phase1Query, phase1Params);
|
|
270
|
+
const rowset1 = rs1.resultSets?.[0];
|
|
271
|
+
const rows1 = (rowset1?.rows ?? []);
|
|
272
|
+
const candidateIds = rows1
|
|
273
|
+
.map((row) => row.items?.[0]?.textValue)
|
|
274
|
+
.filter((id) => typeof id === "string");
|
|
275
|
+
if (candidateIds.length === 0) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
// Phase 2: exact re-ranking on full-precision embedding for candidates only
|
|
279
|
+
const phase2Query = `
|
|
280
|
+
DECLARE $qbinf AS String;
|
|
281
|
+
DECLARE $k AS Uint32;
|
|
282
|
+
DECLARE $uid AS Utf8;
|
|
283
|
+
DECLARE $ids AS List<Utf8>;
|
|
284
|
+
SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
|
|
285
|
+
FROM ${tableName}
|
|
286
|
+
WHERE uid = $uid AND point_id IN $ids
|
|
287
|
+
ORDER BY score ${order}
|
|
288
|
+
LIMIT $k;
|
|
289
|
+
`;
|
|
290
|
+
logger.debug({
|
|
291
|
+
tableName,
|
|
292
|
+
distance,
|
|
293
|
+
top,
|
|
294
|
+
safeTop,
|
|
295
|
+
candidateCount: candidateIds.length,
|
|
296
|
+
mode: "one_table_approximate_phase2_client_side_serialization",
|
|
297
|
+
yql: phase2Query,
|
|
298
|
+
params: {
|
|
299
|
+
uid,
|
|
300
|
+
top: safeTop,
|
|
301
|
+
vectorLength: queryVector.length,
|
|
302
|
+
vectorPreview: queryVector.slice(0, 3),
|
|
303
|
+
ids: candidateIds,
|
|
304
|
+
},
|
|
305
|
+
}, "one_table search (approximate, phase 2): executing YQL");
|
|
306
|
+
const idsParam = TypedValues.list(Types.UTF8, candidateIds);
|
|
307
|
+
const phase2Params = {
|
|
308
|
+
$qbinf: typeof TypedValues.bytes === "function"
|
|
309
|
+
? TypedValues.bytes(binaries.float)
|
|
310
|
+
: binaries.float,
|
|
311
|
+
$k: TypedValues.uint32(safeTop),
|
|
312
|
+
$uid: TypedValues.utf8(uid),
|
|
313
|
+
$ids: idsParam,
|
|
314
|
+
};
|
|
315
|
+
const rs2 = await s.executeQuery(phase2Query, phase2Params);
|
|
316
|
+
const rowset2 = rs2.resultSets?.[0];
|
|
317
|
+
const rows2 = (rowset2?.rows ?? []);
|
|
318
|
+
return rows2.map((row) => {
|
|
319
|
+
const id = row.items?.[0]?.textValue;
|
|
320
|
+
if (typeof id !== "string") {
|
|
321
|
+
throw new Error("point_id is missing in YDB search result");
|
|
322
|
+
}
|
|
323
|
+
let payload;
|
|
324
|
+
let scoreIdx = 1;
|
|
325
|
+
if (withPayload) {
|
|
326
|
+
const payloadText = row.items?.[1]?.textValue;
|
|
327
|
+
if (payloadText) {
|
|
328
|
+
try {
|
|
329
|
+
payload = JSON.parse(payloadText);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
payload = undefined;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
scoreIdx = 2;
|
|
336
|
+
}
|
|
337
|
+
const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
|
|
338
|
+
return { id, score, ...(payload ? { payload } : {}) };
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
const qf = buildVectorParam(queryVector);
|
|
342
|
+
// Phase 1: approximate candidate selection using embedding_quantized
|
|
54
343
|
const phase1Query = `
|
|
55
344
|
DECLARE $qf AS List<Float>;
|
|
56
345
|
DECLARE $k AS Uint32;
|
|
@@ -58,10 +347,25 @@ export async function searchPointsOneTable(tableName, queryVector, top, withPayl
|
|
|
58
347
|
$qbin_bit = Knn::ToBinaryStringBit($qf);
|
|
59
348
|
SELECT point_id
|
|
60
349
|
FROM ${tableName}
|
|
61
|
-
WHERE uid = $uid AND
|
|
62
|
-
ORDER BY ${bitFn}(
|
|
350
|
+
WHERE uid = $uid AND embedding_quantized IS NOT NULL
|
|
351
|
+
ORDER BY ${bitFn}(embedding_quantized, $qbin_bit) ${bitOrder}
|
|
63
352
|
LIMIT $k;
|
|
64
353
|
`;
|
|
354
|
+
logger.debug({
|
|
355
|
+
tableName,
|
|
356
|
+
distance,
|
|
357
|
+
top,
|
|
358
|
+
safeTop,
|
|
359
|
+
candidateLimit,
|
|
360
|
+
mode: "one_table_approximate_phase1",
|
|
361
|
+
yql: phase1Query,
|
|
362
|
+
params: {
|
|
363
|
+
uid,
|
|
364
|
+
top: candidateLimit,
|
|
365
|
+
vectorLength: queryVector.length,
|
|
366
|
+
vectorPreview: queryVector.slice(0, 3),
|
|
367
|
+
},
|
|
368
|
+
}, "one_table search (approximate, phase 1): executing YQL");
|
|
65
369
|
const phase1Params = {
|
|
66
370
|
$qf: qf,
|
|
67
371
|
$k: TypedValues.uint32(candidateLimit),
|
|
@@ -89,10 +393,26 @@ export async function searchPointsOneTable(tableName, queryVector, top, withPayl
|
|
|
89
393
|
ORDER BY score ${order}
|
|
90
394
|
LIMIT $k;
|
|
91
395
|
`;
|
|
396
|
+
logger.debug({
|
|
397
|
+
tableName,
|
|
398
|
+
distance,
|
|
399
|
+
top,
|
|
400
|
+
safeTop,
|
|
401
|
+
candidateCount: candidateIds.length,
|
|
402
|
+
mode: "one_table_approximate_phase2",
|
|
403
|
+
yql: phase2Query,
|
|
404
|
+
params: {
|
|
405
|
+
uid,
|
|
406
|
+
top: safeTop,
|
|
407
|
+
vectorLength: queryVector.length,
|
|
408
|
+
vectorPreview: queryVector.slice(0, 3),
|
|
409
|
+
ids: candidateIds,
|
|
410
|
+
},
|
|
411
|
+
}, "one_table search (approximate, phase 2): executing YQL");
|
|
92
412
|
const idsParam = TypedValues.list(Types.UTF8, candidateIds);
|
|
93
413
|
const phase2Params = {
|
|
94
414
|
$qf: qf,
|
|
95
|
-
$k: TypedValues.uint32(
|
|
415
|
+
$k: TypedValues.uint32(safeTop),
|
|
96
416
|
$uid: TypedValues.utf8(uid),
|
|
97
417
|
$ids: idsParam,
|
|
98
418
|
};
|
|
@@ -124,6 +444,12 @@ export async function searchPointsOneTable(tableName, queryVector, top, withPayl
|
|
|
124
444
|
});
|
|
125
445
|
return results;
|
|
126
446
|
}
|
|
447
|
+
export async function searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid, mode, overfetchMultiplier) {
|
|
448
|
+
if (mode === SearchMode.Exact) {
|
|
449
|
+
return await searchPointsOneTableExact(tableName, queryVector, top, withPayload, distance, dimension, uid);
|
|
450
|
+
}
|
|
451
|
+
return await searchPointsOneTableApproximate(tableName, queryVector, top, withPayload, distance, dimension, uid, overfetchMultiplier);
|
|
452
|
+
}
|
|
127
453
|
export async function deletePointsOneTable(tableName, ids, uid) {
|
|
128
454
|
let deleted = 0;
|
|
129
455
|
await withSession(async (s) => {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function vectorToFloatBinary(vector) {
|
|
2
|
+
if (vector.length === 0) {
|
|
3
|
+
return Buffer.from([1]);
|
|
4
|
+
}
|
|
5
|
+
const buffer = Buffer.alloc(vector.length * 4 + 1);
|
|
6
|
+
for (let i = 0; i < vector.length; i += 1) {
|
|
7
|
+
const value = vector[i];
|
|
8
|
+
if (!Number.isFinite(value)) {
|
|
9
|
+
throw new Error(`Non-finite value in vector at index ${i}: ${value}`);
|
|
10
|
+
}
|
|
11
|
+
buffer.writeFloatLE(value, i * 4);
|
|
12
|
+
}
|
|
13
|
+
buffer.writeUInt8(1, vector.length * 4);
|
|
14
|
+
return buffer;
|
|
15
|
+
}
|
|
16
|
+
export function vectorToBitBinary(vector) {
|
|
17
|
+
if (vector.length === 0) {
|
|
18
|
+
return Buffer.from([10]);
|
|
19
|
+
}
|
|
20
|
+
const byteCount = Math.ceil(vector.length / 8);
|
|
21
|
+
const buffer = Buffer.alloc(byteCount + 1);
|
|
22
|
+
for (let i = 0; i < vector.length; i += 1) {
|
|
23
|
+
const value = vector[i];
|
|
24
|
+
if (!Number.isFinite(value)) {
|
|
25
|
+
throw new Error(`Non-finite value in vector at index ${i}: ${value}`);
|
|
26
|
+
}
|
|
27
|
+
if (value > 0) {
|
|
28
|
+
const byteIndex = Math.floor(i / 8);
|
|
29
|
+
const bitIndex = i % 8;
|
|
30
|
+
buffer[byteIndex] |= 1 << bitIndex;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
buffer.writeUInt8(10, byteCount);
|
|
34
|
+
return buffer;
|
|
35
|
+
}
|
package/dist/ydb/helpers.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export declare function buildVectorParam(vector: number[]): import("ydb-sdk-proto").Ydb.ITypedValue;
|
|
2
2
|
export declare function buildJsonOrEmpty(payload?: Record<string, unknown>): import("ydb-sdk-proto").Ydb.ITypedValue;
|
|
3
|
+
export declare function buildVectorBinaryParams(vector: number[]): {
|
|
4
|
+
float: Buffer<ArrayBufferLike>;
|
|
5
|
+
bit: Buffer<ArrayBufferLike>;
|
|
6
|
+
};
|
package/dist/ydb/helpers.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { Types, TypedValues } from "./client.js";
|
|
2
|
+
import { vectorToFloatBinary, vectorToBitBinary, } from "../utils/vectorBinary.js";
|
|
2
3
|
export function buildVectorParam(vector) {
|
|
3
4
|
return TypedValues.list(Types.FLOAT, vector);
|
|
4
5
|
}
|
|
5
6
|
export function buildJsonOrEmpty(payload) {
|
|
6
7
|
return TypedValues.jsonDocument(JSON.stringify(payload ?? {}));
|
|
7
8
|
}
|
|
9
|
+
export function buildVectorBinaryParams(vector) {
|
|
10
|
+
return {
|
|
11
|
+
float: vectorToFloatBinary(vector),
|
|
12
|
+
bit: vectorToBitBinary(vector),
|
|
13
|
+
};
|
|
14
|
+
}
|
package/dist/ydb/schema.d.ts
CHANGED
package/dist/ydb/schema.js
CHANGED
|
@@ -2,6 +2,8 @@ import { withSession, TableDescription, Column, Types } from "./client.js";
|
|
|
2
2
|
import { logger } from "../logging/logger.js";
|
|
3
3
|
import { GLOBAL_POINTS_AUTOMIGRATE_ENABLED } from "../config/env.js";
|
|
4
4
|
export const GLOBAL_POINTS_TABLE = "qdrant_all_points";
|
|
5
|
+
// Shared YDB-related constants for repositories.
|
|
6
|
+
export { UPSERT_BATCH_SIZE } from "../config/env.js";
|
|
5
7
|
let globalPointsTableReady = false;
|
|
6
8
|
function throwMigrationRequired(message) {
|
|
7
9
|
logger.error(message);
|
|
@@ -39,59 +41,34 @@ export async function ensureGlobalPointsTable() {
|
|
|
39
41
|
tableDescription = await s.describeTable(GLOBAL_POINTS_TABLE);
|
|
40
42
|
}
|
|
41
43
|
catch {
|
|
42
|
-
// Table doesn't exist, create it with all columns
|
|
44
|
+
// Table doesn't exist, create it with all columns using the new schema.
|
|
43
45
|
const desc = new TableDescription()
|
|
44
|
-
.withColumns(new Column("uid", Types.UTF8), new Column("point_id", Types.UTF8), new Column("embedding", Types.BYTES), new Column("
|
|
46
|
+
.withColumns(new Column("uid", Types.UTF8), new Column("point_id", Types.UTF8), new Column("embedding", Types.BYTES), new Column("embedding_quantized", Types.BYTES), new Column("payload", Types.JSON_DOCUMENT))
|
|
45
47
|
.withPrimaryKeys("uid", "point_id");
|
|
46
48
|
await s.createTable(GLOBAL_POINTS_TABLE, desc);
|
|
47
49
|
globalPointsTableReady = true;
|
|
48
50
|
logger.info(`created global points table ${GLOBAL_POINTS_TABLE}`);
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
|
-
// Table exists,
|
|
53
|
+
// Table exists, require the new embedding_quantized column.
|
|
52
54
|
const columns = tableDescription.columns ?? [];
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
if (!hasEmbeddingBit) {
|
|
55
|
+
const hasEmbeddingQuantized = columns.some((col) => col.name === "embedding_quantized");
|
|
56
|
+
if (!hasEmbeddingQuantized) {
|
|
56
57
|
if (!GLOBAL_POINTS_AUTOMIGRATE_ENABLED) {
|
|
57
|
-
throwMigrationRequired(`Global points table ${GLOBAL_POINTS_TABLE} is missing required column
|
|
58
|
+
throwMigrationRequired(`Global points table ${GLOBAL_POINTS_TABLE} is missing required column embedding_quantized; apply the migration (e.g., ALTER TABLE ${GLOBAL_POINTS_TABLE} RENAME COLUMN embedding_bit TO embedding_quantized) or set YDB_QDRANT_GLOBAL_POINTS_AUTOMIGRATE=true after backup to allow automatic migration.`);
|
|
58
59
|
}
|
|
59
60
|
const alterDdl = `
|
|
60
61
|
ALTER TABLE ${GLOBAL_POINTS_TABLE}
|
|
61
|
-
ADD COLUMN
|
|
62
|
+
ADD COLUMN embedding_quantized String;
|
|
62
63
|
`;
|
|
63
64
|
const rawSession = s;
|
|
64
65
|
await rawSession.api.executeSchemeQuery({
|
|
65
66
|
sessionId: rawSession.sessionId,
|
|
66
67
|
yqlText: alterDdl,
|
|
67
68
|
});
|
|
68
|
-
logger.info(`added
|
|
69
|
-
needsBackfill = true;
|
|
69
|
+
logger.info(`added embedding_quantized column to existing table ${GLOBAL_POINTS_TABLE}`);
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
const checkNullsDdl = `
|
|
73
|
-
SELECT 1 AS has_null
|
|
74
|
-
FROM ${GLOBAL_POINTS_TABLE}
|
|
75
|
-
WHERE embedding_bit IS NULL
|
|
76
|
-
LIMIT 1;
|
|
77
|
-
`;
|
|
78
|
-
const checkRes = await s.executeQuery(checkNullsDdl);
|
|
79
|
-
const rows = checkRes?.resultSets?.[0]?.rows ?? [];
|
|
80
|
-
needsBackfill = rows.length > 0;
|
|
81
|
-
}
|
|
82
|
-
if (needsBackfill) {
|
|
83
|
-
if (!GLOBAL_POINTS_AUTOMIGRATE_ENABLED) {
|
|
84
|
-
throwMigrationRequired(`Global points table ${GLOBAL_POINTS_TABLE} requires backfill for embedding_bit; set YDB_QDRANT_GLOBAL_POINTS_AUTOMIGRATE=true after backup to apply the migration manually.`);
|
|
85
|
-
}
|
|
86
|
-
const backfillDdl = `
|
|
87
|
-
UPDATE ${GLOBAL_POINTS_TABLE}
|
|
88
|
-
SET embedding_bit = Untag(Knn::ToBinaryStringBit(Knn::FloatFromBinaryString(embedding)), "BitVector")
|
|
89
|
-
WHERE embedding_bit IS NULL;
|
|
90
|
-
`;
|
|
91
|
-
await s.executeQuery(backfillDdl);
|
|
92
|
-
logger.info(`backfilled embedding_bit column from embedding in ${GLOBAL_POINTS_TABLE}`);
|
|
93
|
-
}
|
|
94
|
-
// Mark table ready only after schema (and any required backfill) succeed
|
|
71
|
+
// Mark table ready after schema checks/migrations succeed.
|
|
95
72
|
globalPointsTableReady = true;
|
|
96
73
|
});
|
|
97
74
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ydb-qdrant",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"main": "dist/package/api.js",
|
|
5
5
|
"types": "dist/package/api.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "vitest run --exclude \"test/integration/**\"",
|
|
18
18
|
"test:coverage": "vitest run --coverage --exclude \"test/integration/**\"",
|
|
19
|
-
"test:integration": "VECTOR_INDEX_BUILD_ENABLED=false vitest run test/integration/YdbRealIntegration.index-disabled.test.ts && VECTOR_INDEX_BUILD_ENABLED=true vitest run test/integration/YdbRealIntegration.test.ts test/integration/YdbRecallIntegration.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table vitest run test/integration/YdbRealIntegration.one-table.test.ts",
|
|
20
|
-
"test:recall": "VECTOR_INDEX_BUILD_ENABLED=true vitest run test/integration/YdbRecallIntegration.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table vitest run test/integration/YdbRealIntegration.one-table.test.ts",
|
|
19
|
+
"test:integration": "VECTOR_INDEX_BUILD_ENABLED=false vitest run test/integration/YdbRealIntegration.index-disabled.test.ts && VECTOR_INDEX_BUILD_ENABLED=true vitest run test/integration/YdbRealIntegration.test.ts test/integration/YdbRecallIntegration.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table YDB_QDRANT_SEARCH_MODE=approximate vitest run test/integration/YdbRealIntegration.one-table.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table YDB_QDRANT_SEARCH_MODE=exact vitest run test/integration/YdbRealIntegration.one-table.test.ts",
|
|
20
|
+
"test:recall": "VECTOR_INDEX_BUILD_ENABLED=true vitest run test/integration/YdbRecallIntegration.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table YDB_QDRANT_SEARCH_MODE=approximate vitest run test/integration/YdbRealIntegration.one-table.test.ts && YDB_QDRANT_COLLECTION_STORAGE_MODE=one_table YDB_QDRANT_SEARCH_MODE=exact vitest run test/integration/YdbRealIntegration.one-table.test.ts",
|
|
21
|
+
"load:soak": "k6 run loadtest/soak-test.js",
|
|
22
|
+
"load:stress": "k6 run loadtest/stress-test.js",
|
|
21
23
|
"build": "tsc -p tsconfig.json",
|
|
22
24
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
23
25
|
"dev": "tsx watch src/index.ts",
|
|
@@ -81,4 +83,4 @@
|
|
|
81
83
|
"typescript-eslint": "^8.47.0",
|
|
82
84
|
"vitest": "^4.0.12"
|
|
83
85
|
}
|
|
84
|
-
}
|
|
86
|
+
}
|