ydb-qdrant 2.1.0 → 2.1.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 +12 -4
- package/dist/SmokeTest.js +1 -3
- package/dist/index.js +1 -2
- package/dist/{Api.d.ts → package/Api.d.ts} +8 -3
- package/dist/{Api.js → package/Api.js} +17 -6
- package/dist/repositories/collectionsRepo.js +6 -5
- package/dist/repositories/pointsRepo.js +9 -6
- package/dist/routes/collections.js +8 -12
- package/dist/routes/points.js +10 -15
- package/dist/services/QdrantService.d.ts +1 -2
- package/dist/services/QdrantService.js +54 -17
- package/dist/ydb/client.d.ts +9 -3
- package/dist/ydb/client.js +29 -8
- package/dist/ydb/helpers.d.ts +2 -2
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
<img src="
|
|
1
|
+
<img src="https://ydb-qdrant.tech/logo.svg" alt="YDB Qdrant logo" height="56">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-ydb-qdrant.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/ydb-qdrant)
|
|
5
|
+
[](https://opensource.org/licenses/ISC)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
# YDB Qdrant-compatible Service
|
|
8
|
+
|
|
9
|
+
Qdrant-compatible Node.js/TypeScript **service and npm library** that stores and searches vectors in YDB using single‑phase top‑k with an automatic YDB vector index (`vector_kmeans_tree`) and table‑scan fallback. Topics: ydb, vector-search, qdrant-compatible, nodejs, typescript, express, yql, ann, semantic-search, rag.
|
|
10
|
+
|
|
11
|
+
Modes:
|
|
12
|
+
- **HTTP server**: Qdrant-compatible REST API (`/collections`, `/points/*`) on top of YDB.
|
|
13
|
+
- **Node.js package**: programmatic client via `createYdbQdrantClient` for direct YDB-backed vector search without running a separate service.
|
|
6
14
|
|
|
7
15
|
Promo site: [ydb-qdrant.tech](http://ydb-qdrant.tech)
|
|
8
16
|
Architecture diagrams: [docs page](http://ydb-qdrant.tech/docs/)
|
|
9
17
|
|
|
10
18
|
## How it works
|
|
11
19
|
|
|
12
|
-

|
|
13
21
|
|
|
14
22
|
## Requirements
|
|
15
23
|
- Node.js 18+
|
package/dist/SmokeTest.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
|
-
import { createYdbQdrantClient } from "./Api.js";
|
|
2
|
+
import { createYdbQdrantClient } from "./package/Api.js";
|
|
3
3
|
async function main() {
|
|
4
4
|
const tenant = process.env.SMOKE_TENANT ?? "smoke";
|
|
5
5
|
const collection = process.env.SMOKE_COLLECTION ?? "demo";
|
|
@@ -30,11 +30,9 @@ async function main() {
|
|
|
30
30
|
top: 2,
|
|
31
31
|
with_payload: true,
|
|
32
32
|
});
|
|
33
|
-
// eslint-disable-next-line no-console
|
|
34
33
|
console.log(JSON.stringify(result, null, 2));
|
|
35
34
|
}
|
|
36
35
|
void main().catch((err) => {
|
|
37
|
-
// eslint-disable-next-line no-console
|
|
38
36
|
console.error(err);
|
|
39
37
|
process.exitCode = 1;
|
|
40
38
|
});
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import { PORT } from "./config/env.js";
|
|
|
4
4
|
import { logger } from "./logging/logger.js";
|
|
5
5
|
import { readyOrThrow } from "./ydb/client.js";
|
|
6
6
|
import { ensureMetaTable } from "./ydb/schema.js";
|
|
7
|
-
let server;
|
|
8
7
|
async function start() {
|
|
9
8
|
try {
|
|
10
9
|
await readyOrThrow();
|
|
@@ -14,7 +13,7 @@ async function start() {
|
|
|
14
13
|
logger.error({ err }, "YDB not ready; startup continues, requests may fail until configured.");
|
|
15
14
|
}
|
|
16
15
|
const app = buildServer();
|
|
17
|
-
|
|
16
|
+
app.listen(PORT, () => {
|
|
18
17
|
logger.info({ port: PORT }, "ydb-qdrant proxy listening");
|
|
19
18
|
});
|
|
20
19
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export {
|
|
1
|
+
import type { IAuthService } from "ydb-sdk";
|
|
2
|
+
import { createCollection as serviceCreateCollection, deleteCollection as serviceDeleteCollection, getCollection as serviceGetCollection, putCollectionIndex as servicePutCollectionIndex, upsertPoints as serviceUpsertPoints, searchPoints as serviceSearchPoints, deletePoints as serviceDeletePoints } from "../services/QdrantService.js";
|
|
3
|
+
export { QdrantServiceError } from "../services/QdrantService.js";
|
|
4
|
+
export { CreateCollectionReq, UpsertPointsReq, SearchReq, DeletePointsReq, } from "../types.js";
|
|
4
5
|
type CreateCollectionResult = Awaited<ReturnType<typeof serviceCreateCollection>>;
|
|
5
6
|
type GetCollectionResult = Awaited<ReturnType<typeof serviceGetCollection>>;
|
|
6
7
|
type DeleteCollectionResult = Awaited<ReturnType<typeof serviceDeleteCollection>>;
|
|
@@ -10,6 +11,10 @@ type SearchPointsResult = Awaited<ReturnType<typeof serviceSearchPoints>>;
|
|
|
10
11
|
type DeletePointsResult = Awaited<ReturnType<typeof serviceDeletePoints>>;
|
|
11
12
|
export interface YdbQdrantClientOptions {
|
|
12
13
|
defaultTenant?: string;
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
database?: string;
|
|
16
|
+
connectionString?: string;
|
|
17
|
+
authService?: IAuthService;
|
|
13
18
|
}
|
|
14
19
|
export interface YdbQdrantTenantClient {
|
|
15
20
|
createCollection(collection: string, body: unknown): Promise<CreateCollectionResult>;
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import { readyOrThrow } from "
|
|
2
|
-
import { ensureMetaTable } from "
|
|
3
|
-
import { createCollection as serviceCreateCollection, deleteCollection as serviceDeleteCollection, getCollection as serviceGetCollection, putCollectionIndex as servicePutCollectionIndex, upsertPoints as serviceUpsertPoints, searchPoints as serviceSearchPoints, deletePoints as serviceDeletePoints, } from "
|
|
4
|
-
export { QdrantServiceError } from "
|
|
5
|
-
export { CreateCollectionReq, UpsertPointsReq, SearchReq, DeletePointsReq, } from "
|
|
1
|
+
import { readyOrThrow, configureDriver } from "../ydb/client.js";
|
|
2
|
+
import { ensureMetaTable } from "../ydb/schema.js";
|
|
3
|
+
import { createCollection as serviceCreateCollection, deleteCollection as serviceDeleteCollection, getCollection as serviceGetCollection, putCollectionIndex as servicePutCollectionIndex, upsertPoints as serviceUpsertPoints, searchPoints as serviceSearchPoints, deletePoints as serviceDeletePoints, } from "../services/QdrantService.js";
|
|
4
|
+
export { QdrantServiceError } from "../services/QdrantService.js";
|
|
5
|
+
export { CreateCollectionReq, UpsertPointsReq, SearchReq, DeletePointsReq, } from "../types.js";
|
|
6
6
|
export async function createYdbQdrantClient(options = {}) {
|
|
7
|
+
if (options.endpoint !== undefined ||
|
|
8
|
+
options.database !== undefined ||
|
|
9
|
+
options.connectionString !== undefined ||
|
|
10
|
+
options.authService !== undefined) {
|
|
11
|
+
configureDriver({
|
|
12
|
+
endpoint: options.endpoint,
|
|
13
|
+
database: options.database,
|
|
14
|
+
connectionString: options.connectionString,
|
|
15
|
+
authService: options.authService,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
7
18
|
await readyOrThrow();
|
|
8
19
|
await ensureMetaTable();
|
|
9
20
|
const defaultTenant = options.defaultTenant ?? "default";
|
|
@@ -38,7 +49,7 @@ export async function createYdbQdrantClient(options = {}) {
|
|
|
38
49
|
return await serviceDeletePoints({ tenant, collection }, body);
|
|
39
50
|
},
|
|
40
51
|
forTenant(tenantId) {
|
|
41
|
-
const tenant =
|
|
52
|
+
const tenant = tenantId;
|
|
42
53
|
return {
|
|
43
54
|
createCollection(collection, body) {
|
|
44
55
|
return serviceCreateCollection({ tenant, collection }, body);
|
|
@@ -72,12 +72,13 @@ export async function buildVectorIndex(tableName, dimension, distance, vectorTyp
|
|
|
72
72
|
await withSession(async (s) => {
|
|
73
73
|
// Drop existing index if present
|
|
74
74
|
const dropDdl = `ALTER TABLE ${tableName} DROP INDEX emb_idx;`;
|
|
75
|
+
const rawSession = s;
|
|
75
76
|
try {
|
|
76
|
-
const dropReq = { sessionId:
|
|
77
|
-
await
|
|
77
|
+
const dropReq = { sessionId: rawSession.sessionId, yqlText: dropDdl };
|
|
78
|
+
await rawSession.api.executeSchemeQuery(dropReq);
|
|
78
79
|
}
|
|
79
80
|
catch (e) {
|
|
80
|
-
const msg =
|
|
81
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
81
82
|
// ignore if index doesn't exist
|
|
82
83
|
if (!/not found|does not exist|no such index/i.test(msg)) {
|
|
83
84
|
throw e;
|
|
@@ -98,8 +99,8 @@ export async function buildVectorIndex(tableName, dimension, distance, vectorTyp
|
|
|
98
99
|
levels=${levels}
|
|
99
100
|
);
|
|
100
101
|
`;
|
|
101
|
-
const createReq = { sessionId:
|
|
102
|
-
await
|
|
102
|
+
const createReq = { sessionId: rawSession.sessionId, yqlText: createDdl };
|
|
103
|
+
await rawSession.api.executeSchemeQuery(createReq);
|
|
103
104
|
});
|
|
104
105
|
}
|
|
105
106
|
function mapDistanceToIndexParam(distance) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { TypedValues, withSession } from "../ydb/client.js";
|
|
2
2
|
import { buildJsonOrEmpty, buildVectorParam } from "../ydb/helpers.js";
|
|
3
3
|
import { logger } from "../logging/logger.js";
|
|
4
|
-
import { APPROX_PRESELECT } from "../config/env.js";
|
|
5
4
|
import { notifyUpsert } from "../indexing/IndexScheduler.js";
|
|
6
5
|
export async function upsertPoints(tableName, points, vectorType, dimension) {
|
|
7
6
|
let upserted = 0;
|
|
@@ -30,14 +29,13 @@ export async function upsertPoints(tableName, points, vectorType, dimension) {
|
|
|
30
29
|
// Retry on transient schema/metadata mismatches during index rebuild
|
|
31
30
|
const maxRetries = 6; // ~ up to ~ (0.25 + jitter) * 2^5 ≈ few seconds
|
|
32
31
|
let attempt = 0;
|
|
33
|
-
// eslint-disable-next-line no-constant-condition
|
|
34
32
|
while (true) {
|
|
35
33
|
try {
|
|
36
34
|
await s.executeQuery(ddl, params);
|
|
37
35
|
break;
|
|
38
36
|
}
|
|
39
37
|
catch (e) {
|
|
40
|
-
const msg =
|
|
38
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
41
39
|
const isTransient = /Aborted|schema version mismatch|Table metadata loading|Failed to load metadata/i.test(msg);
|
|
42
40
|
if (!isTransient || attempt >= maxRetries) {
|
|
43
41
|
throw e;
|
|
@@ -63,7 +61,6 @@ export async function searchPoints(tableName, queryVector, top, withPayload, dis
|
|
|
63
61
|
}
|
|
64
62
|
const { fn, order } = mapDistanceToKnnFn(distance);
|
|
65
63
|
// Single-phase search over embedding using vector index if present
|
|
66
|
-
const preselect = Math.min(APPROX_PRESELECT, Math.max(top * 10, top));
|
|
67
64
|
const qf = buildVectorParam(queryVector, vectorType);
|
|
68
65
|
const params = {
|
|
69
66
|
$qf: qf,
|
|
@@ -87,7 +84,7 @@ export async function searchPoints(tableName, queryVector, top, withPayload, dis
|
|
|
87
84
|
logger.info({ tableName }, "vector index found; using index for search");
|
|
88
85
|
}
|
|
89
86
|
catch (e) {
|
|
90
|
-
const msg =
|
|
87
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
91
88
|
// Fallback to table scan if index not found or not ready
|
|
92
89
|
if (/not found|does not exist|no such index|no global index|is not ready to use/i.test(msg)) {
|
|
93
90
|
logger.info({ tableName }, "vector index not available (missing or building); falling back to table scan");
|
|
@@ -103,6 +100,9 @@ export async function searchPoints(tableName, queryVector, top, withPayload, dis
|
|
|
103
100
|
const rows = (rowset?.rows ?? []);
|
|
104
101
|
return rows.map((row) => {
|
|
105
102
|
const id = row.items?.[0]?.textValue;
|
|
103
|
+
if (typeof id !== "string") {
|
|
104
|
+
throw new Error("point_id is missing in YDB search result");
|
|
105
|
+
}
|
|
106
106
|
let payload;
|
|
107
107
|
let scoreIdx = 1;
|
|
108
108
|
if (withPayload) {
|
|
@@ -129,7 +129,10 @@ export async function deletePoints(tableName, ids) {
|
|
|
129
129
|
DECLARE $id AS Utf8;
|
|
130
130
|
DELETE FROM ${tableName} WHERE point_id = $id;
|
|
131
131
|
`;
|
|
132
|
-
|
|
132
|
+
const params = {
|
|
133
|
+
$id: TypedValues.utf8(String(id)),
|
|
134
|
+
};
|
|
135
|
+
await s.executeQuery(yql, params);
|
|
133
136
|
deleted += 1;
|
|
134
137
|
}
|
|
135
138
|
});
|
|
@@ -16,9 +16,8 @@ collectionsRouter.put("/:collection/index", async (req, res) => {
|
|
|
16
16
|
return res.status(err.statusCode).json(err.payload);
|
|
17
17
|
}
|
|
18
18
|
logger.error({ err }, "build index failed");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.json({ status: "error", error: String(err?.message ?? err) });
|
|
19
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
20
|
+
res.status(500).json({ status: "error", error: errorMessage });
|
|
22
21
|
}
|
|
23
22
|
});
|
|
24
23
|
collectionsRouter.put("/:collection", async (req, res) => {
|
|
@@ -33,9 +32,8 @@ collectionsRouter.put("/:collection", async (req, res) => {
|
|
|
33
32
|
return res.status(err.statusCode).json(err.payload);
|
|
34
33
|
}
|
|
35
34
|
logger.error({ err }, "create collection failed");
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.json({ status: "error", error: String(err?.message ?? err) });
|
|
35
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
36
|
+
res.status(500).json({ status: "error", error: errorMessage });
|
|
39
37
|
}
|
|
40
38
|
});
|
|
41
39
|
collectionsRouter.get("/:collection", async (req, res) => {
|
|
@@ -50,9 +48,8 @@ collectionsRouter.get("/:collection", async (req, res) => {
|
|
|
50
48
|
return res.status(err.statusCode).json(err.payload);
|
|
51
49
|
}
|
|
52
50
|
logger.error({ err }, "get collection failed");
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.json({ status: "error", error: String(err?.message ?? err) });
|
|
51
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
52
|
+
res.status(500).json({ status: "error", error: errorMessage });
|
|
56
53
|
}
|
|
57
54
|
});
|
|
58
55
|
collectionsRouter.delete("/:collection", async (req, res) => {
|
|
@@ -67,8 +64,7 @@ collectionsRouter.delete("/:collection", async (req, res) => {
|
|
|
67
64
|
return res.status(err.statusCode).json(err.payload);
|
|
68
65
|
}
|
|
69
66
|
logger.error({ err }, "delete collection failed");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.json({ status: "error", error: String(err?.message ?? err) });
|
|
67
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
68
|
+
res.status(500).json({ status: "error", error: errorMessage });
|
|
73
69
|
}
|
|
74
70
|
});
|
package/dist/routes/points.js
CHANGED
|
@@ -16,9 +16,8 @@ pointsRouter.put("/:collection/points", async (req, res) => {
|
|
|
16
16
|
return res.status(err.statusCode).json(err.payload);
|
|
17
17
|
}
|
|
18
18
|
logger.error({ err }, "upsert points (PUT) failed");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.json({ status: "error", error: String(err?.message ?? err) });
|
|
19
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
20
|
+
res.status(500).json({ status: "error", error: errorMessage });
|
|
22
21
|
}
|
|
23
22
|
});
|
|
24
23
|
pointsRouter.post("/:collection/points/upsert", async (req, res) => {
|
|
@@ -34,9 +33,8 @@ pointsRouter.post("/:collection/points/upsert", async (req, res) => {
|
|
|
34
33
|
return res.status(err.statusCode).json(err.payload);
|
|
35
34
|
}
|
|
36
35
|
logger.error({ err }, "upsert points failed");
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.json({ status: "error", error: String(err?.message ?? err) });
|
|
36
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
37
|
+
res.status(500).json({ status: "error", error: errorMessage });
|
|
40
38
|
}
|
|
41
39
|
});
|
|
42
40
|
pointsRouter.post("/:collection/points/search", async (req, res) => {
|
|
@@ -52,9 +50,8 @@ pointsRouter.post("/:collection/points/search", async (req, res) => {
|
|
|
52
50
|
return res.status(err.statusCode).json(err.payload);
|
|
53
51
|
}
|
|
54
52
|
logger.error({ err }, "search points failed");
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.json({ status: "error", error: String(err?.message ?? err) });
|
|
53
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
54
|
+
res.status(500).json({ status: "error", error: errorMessage });
|
|
58
55
|
}
|
|
59
56
|
});
|
|
60
57
|
// Compatibility: some clients call POST /collections/:collection/points/query
|
|
@@ -71,9 +68,8 @@ pointsRouter.post("/:collection/points/query", async (req, res) => {
|
|
|
71
68
|
return res.status(err.statusCode).json(err.payload);
|
|
72
69
|
}
|
|
73
70
|
logger.error({ err }, "search points (query) failed");
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.json({ status: "error", error: String(err?.message ?? err) });
|
|
71
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
72
|
+
res.status(500).json({ status: "error", error: errorMessage });
|
|
77
73
|
}
|
|
78
74
|
});
|
|
79
75
|
pointsRouter.post("/:collection/points/delete", async (req, res) => {
|
|
@@ -89,8 +85,7 @@ pointsRouter.post("/:collection/points/delete", async (req, res) => {
|
|
|
89
85
|
return res.status(err.statusCode).json(err.payload);
|
|
90
86
|
}
|
|
91
87
|
logger.error({ err }, "delete points failed");
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.json({ status: "error", error: String(err?.message ?? err) });
|
|
88
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
89
|
+
res.status(500).json({ status: "error", error: errorMessage });
|
|
95
90
|
}
|
|
96
91
|
});
|
|
@@ -30,8 +30,7 @@ export declare function getCollection(ctx: CollectionContextInput): Promise<{
|
|
|
30
30
|
export declare function deleteCollection(ctx: CollectionContextInput): Promise<{
|
|
31
31
|
acknowledged: boolean;
|
|
32
32
|
}>;
|
|
33
|
-
|
|
34
|
-
}
|
|
33
|
+
type PointsContextInput = CollectionContextInput;
|
|
35
34
|
export declare function upsertPoints(ctx: PointsContextInput, body: unknown): Promise<{
|
|
36
35
|
upserted: number;
|
|
37
36
|
}>;
|
|
@@ -101,10 +101,10 @@ function extractVectorLoose(body, depth = 0) {
|
|
|
101
101
|
return obj.embedding;
|
|
102
102
|
const query = obj.query;
|
|
103
103
|
if (query) {
|
|
104
|
-
const queryVector = query
|
|
104
|
+
const queryVector = query["vector"];
|
|
105
105
|
if (isNumberArray(queryVector))
|
|
106
106
|
return queryVector;
|
|
107
|
-
const nearest = query
|
|
107
|
+
const nearest = query["nearest"];
|
|
108
108
|
if (nearest && isNumberArray(nearest.vector)) {
|
|
109
109
|
return nearest.vector;
|
|
110
110
|
}
|
|
@@ -131,39 +131,68 @@ function extractVectorLoose(body, depth = 0) {
|
|
|
131
131
|
return undefined;
|
|
132
132
|
}
|
|
133
133
|
function normalizeSearchBodyForSearch(body) {
|
|
134
|
+
if (!body || typeof body !== "object") {
|
|
135
|
+
return {
|
|
136
|
+
vector: undefined,
|
|
137
|
+
top: undefined,
|
|
138
|
+
withPayload: undefined,
|
|
139
|
+
scoreThreshold: undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
134
142
|
const b = body;
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
143
|
+
const rawVector = b["vector"];
|
|
144
|
+
const vector = isNumberArray(rawVector) ? rawVector : undefined;
|
|
145
|
+
const rawTop = b["top"];
|
|
146
|
+
const rawLimit = b["limit"];
|
|
147
|
+
const topFromTop = typeof rawTop === "number" ? rawTop : undefined;
|
|
148
|
+
const topFromLimit = typeof rawLimit === "number" ? rawLimit : undefined;
|
|
138
149
|
const top = topFromTop ?? topFromLimit;
|
|
139
150
|
let withPayload;
|
|
140
|
-
const rawWithPayload = b
|
|
151
|
+
const rawWithPayload = b["with_payload"];
|
|
141
152
|
if (typeof rawWithPayload === "boolean") {
|
|
142
153
|
withPayload = rawWithPayload;
|
|
143
154
|
}
|
|
144
|
-
else if (Array.isArray(rawWithPayload) ||
|
|
155
|
+
else if (Array.isArray(rawWithPayload) ||
|
|
156
|
+
typeof rawWithPayload === "object") {
|
|
145
157
|
withPayload = true;
|
|
146
158
|
}
|
|
147
|
-
const
|
|
148
|
-
const
|
|
159
|
+
const thresholdRaw = b["score_threshold"];
|
|
160
|
+
const thresholdValue = typeof thresholdRaw === "number" ? thresholdRaw : Number(thresholdRaw);
|
|
161
|
+
const scoreThreshold = Number.isFinite(thresholdValue)
|
|
162
|
+
? thresholdValue
|
|
163
|
+
: undefined;
|
|
149
164
|
return { vector, top, withPayload, scoreThreshold };
|
|
150
165
|
}
|
|
151
166
|
function normalizeSearchBodyForQuery(body) {
|
|
167
|
+
if (!body || typeof body !== "object") {
|
|
168
|
+
return {
|
|
169
|
+
vector: undefined,
|
|
170
|
+
top: undefined,
|
|
171
|
+
withPayload: undefined,
|
|
172
|
+
scoreThreshold: undefined,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
152
175
|
const b = body;
|
|
153
176
|
const vector = extractVectorLoose(b);
|
|
154
|
-
const
|
|
155
|
-
const
|
|
177
|
+
const rawTop = b["top"];
|
|
178
|
+
const rawLimit = b["limit"];
|
|
179
|
+
const topFromTop = typeof rawTop === "number" ? rawTop : undefined;
|
|
180
|
+
const topFromLimit = typeof rawLimit === "number" ? rawLimit : undefined;
|
|
156
181
|
const top = topFromTop ?? topFromLimit;
|
|
157
182
|
let withPayload;
|
|
158
|
-
const rawWithPayload = b
|
|
183
|
+
const rawWithPayload = b["with_payload"];
|
|
159
184
|
if (typeof rawWithPayload === "boolean") {
|
|
160
185
|
withPayload = rawWithPayload;
|
|
161
186
|
}
|
|
162
|
-
else if (Array.isArray(rawWithPayload) ||
|
|
187
|
+
else if (Array.isArray(rawWithPayload) ||
|
|
188
|
+
typeof rawWithPayload === "object") {
|
|
163
189
|
withPayload = true;
|
|
164
190
|
}
|
|
165
|
-
const
|
|
166
|
-
const
|
|
191
|
+
const thresholdRaw = b["score_threshold"];
|
|
192
|
+
const thresholdValue = typeof thresholdRaw === "number" ? thresholdRaw : Number(thresholdRaw);
|
|
193
|
+
const scoreThreshold = Number.isFinite(thresholdValue)
|
|
194
|
+
? thresholdValue
|
|
195
|
+
: undefined;
|
|
167
196
|
return { vector, top, withPayload, scoreThreshold };
|
|
168
197
|
}
|
|
169
198
|
export async function upsertPoints(ctx, body) {
|
|
@@ -193,7 +222,11 @@ async function executeSearch(ctx, normalizedSearch, source) {
|
|
|
193
222
|
logger.info({ tenant: normalized.tenant, collection: normalized.collection }, `${source}: resolve collection meta`);
|
|
194
223
|
const meta = await getCollectionMeta(normalized.metaKey);
|
|
195
224
|
if (!meta) {
|
|
196
|
-
logger.warn({
|
|
225
|
+
logger.warn({
|
|
226
|
+
tenant: normalized.tenant,
|
|
227
|
+
collection: normalized.collection,
|
|
228
|
+
metaKey: normalized.metaKey,
|
|
229
|
+
}, `${source}: collection not found`);
|
|
197
230
|
throw new QdrantServiceError(404, {
|
|
198
231
|
status: "error",
|
|
199
232
|
error: "collection not found",
|
|
@@ -235,7 +268,11 @@ async function executeSearch(ctx, normalizedSearch, source) {
|
|
|
235
268
|
}
|
|
236
269
|
return hit.score <= threshold;
|
|
237
270
|
});
|
|
238
|
-
logger.info({
|
|
271
|
+
logger.info({
|
|
272
|
+
tenant: normalized.tenant,
|
|
273
|
+
collection: normalized.collection,
|
|
274
|
+
hits: hits.length,
|
|
275
|
+
}, `${source}: completed`);
|
|
239
276
|
return { points: filtered };
|
|
240
277
|
}
|
|
241
278
|
export async function searchPoints(ctx, body) {
|
package/dist/ydb/client.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import type { Session } from "ydb-sdk";
|
|
2
|
-
declare const Types:
|
|
1
|
+
import type { Session, IAuthService } 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;
|
|
3
3
|
export { Types, TypedValues, TableDescription, Column };
|
|
4
|
-
|
|
4
|
+
type DriverConfig = {
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
database?: string;
|
|
7
|
+
connectionString?: string;
|
|
8
|
+
authService?: IAuthService;
|
|
9
|
+
};
|
|
10
|
+
export declare function configureDriver(config: DriverConfig): void;
|
|
5
11
|
export declare function readyOrThrow(): Promise<void>;
|
|
6
12
|
export declare function withSession<T>(fn: (s: Session) => Promise<T>): Promise<T>;
|
package/dist/ydb/client.js
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
2
|
import { YDB_DATABASE, YDB_ENDPOINT } from "../config/env.js";
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
5
4
|
const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, } = require("ydb-sdk");
|
|
6
5
|
export { Types, TypedValues, TableDescription, Column };
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
let overrideConfig;
|
|
7
|
+
let driver;
|
|
8
|
+
export function configureDriver(config) {
|
|
9
|
+
if (driver) {
|
|
10
|
+
// Driver already created; keep existing connection settings.
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
overrideConfig = config;
|
|
14
|
+
}
|
|
15
|
+
function getOrCreateDriver() {
|
|
16
|
+
if (driver) {
|
|
17
|
+
return driver;
|
|
18
|
+
}
|
|
19
|
+
const base = overrideConfig?.connectionString != null
|
|
20
|
+
? { connectionString: overrideConfig.connectionString }
|
|
21
|
+
: {
|
|
22
|
+
endpoint: overrideConfig?.endpoint ?? YDB_ENDPOINT,
|
|
23
|
+
database: overrideConfig?.database ?? YDB_DATABASE,
|
|
24
|
+
};
|
|
25
|
+
driver = new Driver({
|
|
26
|
+
...base,
|
|
27
|
+
authService: overrideConfig?.authService ?? getCredentialsFromEnv(),
|
|
28
|
+
});
|
|
29
|
+
return driver;
|
|
30
|
+
}
|
|
12
31
|
export async function readyOrThrow() {
|
|
13
|
-
const
|
|
32
|
+
const d = getOrCreateDriver();
|
|
33
|
+
const ok = await d.ready(10000);
|
|
14
34
|
if (!ok) {
|
|
15
35
|
throw new Error("YDB driver is not ready in 10s. Check connectivity and credentials.");
|
|
16
36
|
}
|
|
17
37
|
}
|
|
18
38
|
export async function withSession(fn) {
|
|
19
|
-
|
|
39
|
+
const d = getOrCreateDriver();
|
|
40
|
+
return await d.tableClient.withSession(fn, 15000);
|
|
20
41
|
}
|
package/dist/ydb/helpers.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function buildVectorParam(vector: number[], vectorType: "float" | "uint8"):
|
|
2
|
-
export declare function buildJsonOrEmpty(payload?: Record<string, unknown>):
|
|
1
|
+
export declare function buildVectorParam(vector: number[], vectorType: "float" | "uint8"): import("ydb-sdk-proto").Ydb.ITypedValue;
|
|
2
|
+
export declare function buildJsonOrEmpty(payload?: Record<string, unknown>): import("ydb-sdk-proto").Ydb.ITypedValue;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ydb-qdrant",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"main": "dist/Api.js",
|
|
5
|
-
"types": "dist/Api.d.ts",
|
|
3
|
+
"version": "2.1.3",
|
|
4
|
+
"main": "dist/package/Api.js",
|
|
5
|
+
"types": "dist/package/Api.d.ts",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./dist/Api.js",
|
|
7
|
+
".": "./dist/package/Api.js",
|
|
8
8
|
"./server": "./dist/server.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
19
20
|
"dev": "tsx watch src/index.ts",
|
|
20
21
|
"start": "node --experimental-specifier-resolution=node --enable-source-maps dist/index.js",
|
|
21
22
|
"smoke": "npm run build && node --experimental-specifier-resolution=node --enable-source-maps dist/SmokeTest.js",
|
|
22
|
-
"lint": "
|
|
23
|
+
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
24
|
+
"prepare": "husky",
|
|
23
25
|
"prepublishOnly": "npm test && npm run build"
|
|
24
26
|
},
|
|
25
27
|
"keywords": [
|
|
@@ -62,13 +64,16 @@
|
|
|
62
64
|
"zod": "^4.1.12"
|
|
63
65
|
},
|
|
64
66
|
"devDependencies": {
|
|
65
|
-
"@
|
|
67
|
+
"@eslint/js": "^9.39.1",
|
|
66
68
|
"@types/express": "^5.0.3",
|
|
67
69
|
"@types/node": "^24.9.1",
|
|
68
70
|
"docsify-cli": "^4.4.4",
|
|
71
|
+
"eslint": "^9.39.1",
|
|
72
|
+
"husky": "^9.1.7",
|
|
69
73
|
"node-plantuml-latest": "^2.4.0",
|
|
70
74
|
"tsx": "^4.20.6",
|
|
71
75
|
"typescript": "^5.9.3",
|
|
76
|
+
"typescript-eslint": "^8.47.0",
|
|
72
77
|
"vitest": "^4.0.12"
|
|
73
78
|
}
|
|
74
79
|
}
|