ydb-qdrant 5.0.0 → 5.2.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.
@@ -4,6 +4,7 @@ export declare const YDB_DATABASE: string;
4
4
  export declare const PORT: number;
5
5
  export declare const LOG_LEVEL: string;
6
6
  export declare const GLOBAL_POINTS_AUTOMIGRATE_ENABLED: boolean;
7
+ export declare const USE_BATCH_DELETE_FOR_COLLECTIONS: boolean;
7
8
  export declare enum SearchMode {
8
9
  Exact = "exact",
9
10
  Approximate = "approximate"
@@ -16,3 +17,6 @@ export declare const UPSERT_BATCH_SIZE: number;
16
17
  export declare const SESSION_POOL_MIN_SIZE: number;
17
18
  export declare const SESSION_POOL_MAX_SIZE: number;
18
19
  export declare const SESSION_KEEPALIVE_PERIOD_MS: number;
20
+ export declare const STARTUP_PROBE_SESSION_TIMEOUT_MS: number;
21
+ export declare const UPSERT_OPERATION_TIMEOUT_MS: number;
22
+ export declare const SEARCH_OPERATION_TIMEOUT_MS: number;
@@ -35,6 +35,7 @@ function parseBooleanEnv(value, defaultValue) {
35
35
  return true;
36
36
  }
37
37
  export const GLOBAL_POINTS_AUTOMIGRATE_ENABLED = parseBooleanEnv(process.env.YDB_QDRANT_GLOBAL_POINTS_AUTOMIGRATE, false);
38
+ export const USE_BATCH_DELETE_FOR_COLLECTIONS = parseBooleanEnv(process.env.YDB_QDRANT_USE_BATCH_DELETE, true);
38
39
  export var SearchMode;
39
40
  (function (SearchMode) {
40
41
  SearchMode["Exact"] = "exact";
@@ -67,3 +68,6 @@ const NORMALIZED_SESSION_POOL_MIN_SIZE = RAW_SESSION_POOL_MIN_SIZE > RAW_SESSION
67
68
  export const SESSION_POOL_MIN_SIZE = NORMALIZED_SESSION_POOL_MIN_SIZE;
68
69
  export const SESSION_POOL_MAX_SIZE = RAW_SESSION_POOL_MAX_SIZE;
69
70
  export const SESSION_KEEPALIVE_PERIOD_MS = parseIntegerEnv(process.env.YDB_SESSION_KEEPALIVE_PERIOD_MS, 5000, { min: 1000, max: 60000 });
71
+ export const STARTUP_PROBE_SESSION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_STARTUP_PROBE_SESSION_TIMEOUT_MS, 5000, { min: 1000 });
72
+ export const UPSERT_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_TIMEOUT_MS, 5000, { min: 1000 });
73
+ export const SEARCH_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_SEARCH_TIMEOUT_MS, 10000, { min: 1000 });
@@ -1,6 +1,8 @@
1
1
  import { TypedValues, withSession, createExecuteQuerySettings, withStartupProbeSession, createExecuteQuerySettingsWithTimeout, } from "../ydb/client.js";
2
+ import { STARTUP_PROBE_SESSION_TIMEOUT_MS } from "../config/env.js";
2
3
  import { uidFor } from "../utils/tenant.js";
3
4
  import { createCollectionOneTable, deleteCollectionOneTable, } from "./collectionsRepo.one-table.js";
5
+ import { withRetry, isTransientYdbError } from "../utils/retry.js";
4
6
  export async function createCollection(metaKey, dim, distance, vectorType) {
5
7
  await createCollectionOneTable(metaKey, dim, distance, vectorType);
6
8
  }
@@ -35,15 +37,22 @@ export async function verifyCollectionsQueryCompilationForStartup() {
35
37
  FROM qdr__collections
36
38
  WHERE collection = $collection;
37
39
  `;
38
- await withStartupProbeSession(async (s) => {
39
- const settings = createExecuteQuerySettingsWithTimeout({
40
- keepInCache: true,
41
- idempotent: true,
42
- timeoutMs: 3000,
40
+ await withRetry(async () => {
41
+ await withStartupProbeSession(async (s) => {
42
+ const settings = createExecuteQuerySettingsWithTimeout({
43
+ keepInCache: true,
44
+ idempotent: true,
45
+ timeoutMs: STARTUP_PROBE_SESSION_TIMEOUT_MS,
46
+ });
47
+ await s.executeQuery(qry, {
48
+ $collection: TypedValues.utf8(probeKey),
49
+ }, undefined, settings);
43
50
  });
44
- await s.executeQuery(qry, {
45
- $collection: TypedValues.utf8(probeKey),
46
- }, undefined, settings);
51
+ }, {
52
+ isTransient: isTransientYdbError,
53
+ maxRetries: 2,
54
+ baseDelayMs: 200,
55
+ context: { probe: "collections_startup_compilation" },
47
56
  });
48
57
  }
49
58
  export async function deleteCollection(metaKey, uid) {
@@ -2,6 +2,7 @@ import { TypedValues, Types, withSession, createExecuteQuerySettings, } from "..
2
2
  import { GLOBAL_POINTS_TABLE, ensureGlobalPointsTable } from "../ydb/schema.js";
3
3
  import { upsertCollectionMeta } from "./collectionsRepo.shared.js";
4
4
  import { withRetry, isTransientYdbError } from "../utils/retry.js";
5
+ import { USE_BATCH_DELETE_FOR_COLLECTIONS } from "../config/env.js";
5
6
  const DELETE_COLLECTION_BATCH_SIZE = 10000;
6
7
  function isOutOfBufferMemoryYdbError(error) {
7
8
  const msg = error instanceof Error ? error.message : String(error);
@@ -63,32 +64,68 @@ export async function createCollectionOneTable(metaKey, dim, distance, vectorTyp
63
64
  }
64
65
  export async function deleteCollectionOneTable(metaKey, uid) {
65
66
  await ensureGlobalPointsTable();
66
- const deletePointsYql = `
67
- DECLARE $uid AS Utf8;
68
- DELETE FROM ${GLOBAL_POINTS_TABLE} WHERE uid = $uid;
69
- `;
70
- await withRetry(() => withSession(async (s) => {
71
- const settings = createExecuteQuerySettings();
72
- try {
73
- await s.executeQuery(deletePointsYql, {
74
- $uid: TypedValues.utf8(uid),
75
- }, undefined, settings);
76
- }
77
- catch (err) {
78
- if (!isOutOfBufferMemoryYdbError(err)) {
79
- throw err;
67
+ if (USE_BATCH_DELETE_FOR_COLLECTIONS) {
68
+ const batchDeletePointsYql = `
69
+ DECLARE $uid AS Utf8;
70
+ BATCH DELETE FROM ${GLOBAL_POINTS_TABLE}
71
+ WHERE uid = $uid;
72
+ `;
73
+ await withRetry(() => withSession(async (s) => {
74
+ const settings = createExecuteQuerySettings();
75
+ try {
76
+ await s.executeQuery(batchDeletePointsYql, {
77
+ $uid: TypedValues.utf8(uid),
78
+ }, undefined, settings);
80
79
  }
81
- await deletePointsForUidInChunks(s, uid);
82
- }
83
- }), {
84
- isTransient: isTransientYdbError,
85
- context: {
86
- operation: "deleteCollectionOneTable",
87
- tableName: GLOBAL_POINTS_TABLE,
88
- metaKey,
89
- uid,
90
- },
91
- });
80
+ catch (err) {
81
+ if (!isOutOfBufferMemoryYdbError(err)) {
82
+ throw err;
83
+ }
84
+ // BATCH DELETE already deletes in chunks per partition, but if YDB
85
+ // still reports an out-of-buffer-memory condition, fall back to
86
+ // the same per-uid chunked deletion strategy as the legacy path.
87
+ await deletePointsForUidInChunks(s, uid);
88
+ }
89
+ }), {
90
+ isTransient: isTransientYdbError,
91
+ context: {
92
+ operation: "deleteCollectionOneTable",
93
+ tableName: GLOBAL_POINTS_TABLE,
94
+ metaKey,
95
+ uid,
96
+ mode: "batch_delete",
97
+ },
98
+ });
99
+ }
100
+ else {
101
+ const deletePointsYql = `
102
+ DECLARE $uid AS Utf8;
103
+ DELETE FROM ${GLOBAL_POINTS_TABLE} WHERE uid = $uid;
104
+ `;
105
+ await withRetry(() => withSession(async (s) => {
106
+ const settings = createExecuteQuerySettings();
107
+ try {
108
+ await s.executeQuery(deletePointsYql, {
109
+ $uid: TypedValues.utf8(uid),
110
+ }, undefined, settings);
111
+ }
112
+ catch (err) {
113
+ if (!isOutOfBufferMemoryYdbError(err)) {
114
+ throw err;
115
+ }
116
+ await deletePointsForUidInChunks(s, uid);
117
+ }
118
+ }), {
119
+ isTransient: isTransientYdbError,
120
+ context: {
121
+ operation: "deleteCollectionOneTable",
122
+ tableName: GLOBAL_POINTS_TABLE,
123
+ metaKey,
124
+ uid,
125
+ mode: "legacy_chunked",
126
+ },
127
+ });
128
+ }
92
129
  const delMeta = `
93
130
  DECLARE $collection AS Utf8;
94
131
  DELETE FROM qdr__collections WHERE collection = $collection;
@@ -1,9 +1,9 @@
1
- import { TypedValues, Types, withSession, createExecuteQuerySettings, } from "../ydb/client.js";
1
+ import { TypedValues, Types, withSession, createExecuteQuerySettings, createExecuteQuerySettingsWithTimeout, } from "../ydb/client.js";
2
2
  import { buildVectorParam, buildVectorBinaryParams } from "../ydb/helpers.js";
3
3
  import { mapDistanceToKnnFn, mapDistanceToBitKnnFn, } from "../utils/distance.js";
4
4
  import { withRetry, isTransientYdbError } from "../utils/retry.js";
5
5
  import { UPSERT_BATCH_SIZE } from "../ydb/schema.js";
6
- import { CLIENT_SIDE_SERIALIZATION_ENABLED, SearchMode, } from "../config/env.js";
6
+ import { CLIENT_SIDE_SERIALIZATION_ENABLED, SearchMode, UPSERT_OPERATION_TIMEOUT_MS, SEARCH_OPERATION_TIMEOUT_MS, } from "../config/env.js";
7
7
  import { logger } from "../logging/logger.js";
8
8
  export async function upsertPointsOneTable(tableName, points, dimension, uid) {
9
9
  for (const p of points) {
@@ -24,7 +24,11 @@ export async function upsertPointsOneTable(tableName, points, dimension, uid) {
24
24
  }
25
25
  let upserted = 0;
26
26
  await withSession(async (s) => {
27
- const settings = createExecuteQuerySettings();
27
+ const settings = createExecuteQuerySettingsWithTimeout({
28
+ keepInCache: true,
29
+ idempotent: true,
30
+ timeoutMs: UPSERT_OPERATION_TIMEOUT_MS,
31
+ });
28
32
  for (let i = 0; i < points.length; i += UPSERT_BATCH_SIZE) {
29
33
  const batch = points.slice(i, i + UPSERT_BATCH_SIZE);
30
34
  let ddl;
@@ -192,7 +196,11 @@ async function searchPointsOneTableExact(tableName, queryVector, top, withPayloa
192
196
  vectorPreview: queryVector.slice(0, 3),
193
197
  },
194
198
  }, "one_table search (exact): executing YQL");
195
- const settings = createExecuteQuerySettings();
199
+ const settings = createExecuteQuerySettingsWithTimeout({
200
+ keepInCache: true,
201
+ idempotent: true,
202
+ timeoutMs: SEARCH_OPERATION_TIMEOUT_MS,
203
+ });
196
204
  const rs = await s.executeQuery(yql, params, undefined, settings);
197
205
  const rowset = rs.resultSets?.[0];
198
206
  const rows = (rowset?.rows ?? []);
@@ -334,7 +342,11 @@ async function searchPointsOneTableApproximate(tableName, queryVector, top, with
334
342
  },
335
343
  }, "one_table search (approximate): executing YQL");
336
344
  }
337
- const settings = createExecuteQuerySettings();
345
+ const settings = createExecuteQuerySettingsWithTimeout({
346
+ keepInCache: true,
347
+ idempotent: true,
348
+ timeoutMs: SEARCH_OPERATION_TIMEOUT_MS,
349
+ });
338
350
  const rs = await s.executeQuery(yql, params, undefined, settings);
339
351
  const rowset = rs.resultSets?.[0];
340
352
  const rows = (rowset?.rows ?? []);
@@ -2,6 +2,8 @@ import { Router } from "express";
2
2
  import { upsertPoints, searchPoints, queryPoints, deletePoints, } from "../services/PointsService.js";
3
3
  import { QdrantServiceError } from "../services/errors.js";
4
4
  import { logger } from "../logging/logger.js";
5
+ import { isCompilationTimeoutError } from "../ydb/client.js";
6
+ import { scheduleExit } from "../utils/exit.js";
5
7
  export const pointsRouter = Router();
6
8
  // Qdrant-compatible: PUT /collections/:collection/points (upsert)
7
9
  pointsRouter.put("/:collection/points", async (req, res) => {
@@ -18,8 +20,14 @@ pointsRouter.put("/:collection/points", async (req, res) => {
18
20
  if (err instanceof QdrantServiceError) {
19
21
  return res.status(err.statusCode).json(err.payload);
20
22
  }
21
- logger.error({ err }, "upsert points (PUT) failed");
22
23
  const errorMessage = err instanceof Error ? err.message : String(err);
24
+ if (isCompilationTimeoutError(err)) {
25
+ logger.error({ err }, "YDB compilation error during upsert points (PUT); scheduling process exit");
26
+ res.status(500).json({ status: "error", error: errorMessage });
27
+ scheduleExit(1);
28
+ return;
29
+ }
30
+ logger.error({ err }, "upsert points (PUT) failed");
23
31
  res.status(500).json({ status: "error", error: errorMessage });
24
32
  }
25
33
  });
@@ -37,8 +45,14 @@ pointsRouter.post("/:collection/points/upsert", async (req, res) => {
37
45
  if (err instanceof QdrantServiceError) {
38
46
  return res.status(err.statusCode).json(err.payload);
39
47
  }
40
- logger.error({ err }, "upsert points failed");
41
48
  const errorMessage = err instanceof Error ? err.message : String(err);
49
+ if (isCompilationTimeoutError(err)) {
50
+ logger.error({ err }, "YDB compilation error during upsert points; scheduling process exit");
51
+ res.status(500).json({ status: "error", error: errorMessage });
52
+ scheduleExit(1);
53
+ return;
54
+ }
55
+ logger.error({ err }, "upsert points failed");
42
56
  res.status(500).json({ status: "error", error: errorMessage });
43
57
  }
44
58
  });
@@ -56,8 +70,14 @@ pointsRouter.post("/:collection/points/search", async (req, res) => {
56
70
  if (err instanceof QdrantServiceError) {
57
71
  return res.status(err.statusCode).json(err.payload);
58
72
  }
59
- logger.error({ err }, "search points failed");
60
73
  const errorMessage = err instanceof Error ? err.message : String(err);
74
+ if (isCompilationTimeoutError(err)) {
75
+ logger.error({ err }, "YDB compilation error during search points; scheduling process exit");
76
+ res.status(500).json({ status: "error", error: errorMessage });
77
+ scheduleExit(1);
78
+ return;
79
+ }
80
+ logger.error({ err }, "search points failed");
61
81
  res.status(500).json({ status: "error", error: errorMessage });
62
82
  }
63
83
  });
@@ -76,8 +96,14 @@ pointsRouter.post("/:collection/points/query", async (req, res) => {
76
96
  if (err instanceof QdrantServiceError) {
77
97
  return res.status(err.statusCode).json(err.payload);
78
98
  }
79
- logger.error({ err }, "search points (query) failed");
80
99
  const errorMessage = err instanceof Error ? err.message : String(err);
100
+ if (isCompilationTimeoutError(err)) {
101
+ logger.error({ err }, "YDB compilation error during search points (query); scheduling process exit");
102
+ res.status(500).json({ status: "error", error: errorMessage });
103
+ scheduleExit(1);
104
+ return;
105
+ }
106
+ logger.error({ err }, "search points (query) failed");
81
107
  res.status(500).json({ status: "error", error: errorMessage });
82
108
  }
83
109
  });
package/dist/server.js CHANGED
@@ -2,11 +2,28 @@ import express from "express";
2
2
  import { collectionsRouter } from "./routes/collections.js";
3
3
  import { pointsRouter } from "./routes/points.js";
4
4
  import { requestLogger } from "./middleware/requestLogger.js";
5
- import { isYdbAvailable } from "./ydb/client.js";
5
+ import { isYdbAvailable, isCompilationTimeoutError } from "./ydb/client.js";
6
+ import { verifyCollectionsQueryCompilationForStartup } from "./repositories/collectionsRepo.js";
7
+ import { logger } from "./logging/logger.js";
8
+ import { scheduleExit } from "./utils/exit.js";
6
9
  export async function healthHandler(_req, res) {
7
10
  const ok = await isYdbAvailable();
8
11
  if (!ok) {
12
+ logger.error("YDB unavailable during health check; scheduling process exit");
9
13
  res.status(503).json({ status: "error", error: "YDB unavailable" });
14
+ scheduleExit(1);
15
+ return;
16
+ }
17
+ try {
18
+ await verifyCollectionsQueryCompilationForStartup();
19
+ }
20
+ catch (err) {
21
+ const isTimeout = isCompilationTimeoutError(err);
22
+ logger.error({ err }, isTimeout
23
+ ? "YDB compilation timeout during health probe; scheduling process exit"
24
+ : "YDB health probe failed; scheduling process exit");
25
+ res.status(503).json({ status: "error", error: "YDB health probe failed" });
26
+ scheduleExit(1);
10
27
  return;
11
28
  }
12
29
  res.json({ status: "ok" });
@@ -0,0 +1,2 @@
1
+ export declare function scheduleExit(code: number): void;
2
+ export declare function __setExitFnForTests(fn: (code: number) => void): void;
@@ -0,0 +1,12 @@
1
+ let exitFn = (code) => {
2
+ // Use process.exit in production; this will be overridden in tests.
3
+ process.exit(code);
4
+ };
5
+ export function scheduleExit(code) {
6
+ // Schedule exit on the next tick so HTTP responses can be flushed first.
7
+ setImmediate(() => exitFn(code));
8
+ }
9
+ // Test-only: allow overriding the underlying exit behavior.
10
+ export function __setExitFnForTests(fn) {
11
+ exitFn = fn;
12
+ }
@@ -32,7 +32,14 @@ export async function withRetry(fn, options = {}) {
32
32
  throw e;
33
33
  }
34
34
  const backoffMs = Math.floor(baseDelayMs * Math.pow(2, attempt) + Math.random() * 100);
35
- logger.warn({ ...context, attempt, backoffMs }, "operation aborted due to transient error; retrying");
35
+ logger.warn({
36
+ ...context,
37
+ attempt,
38
+ backoffMs,
39
+ err: e instanceof Error
40
+ ? e
41
+ : new Error(typeof e === "string" ? e : JSON.stringify(e)),
42
+ }, "operation aborted due to transient error; retrying");
36
43
  await new Promise((r) => setTimeout(r, backoffMs));
37
44
  attempt += 1;
38
45
  }
@@ -1,6 +1,6 @@
1
1
  import type { Session, IAuthService, ExecuteQuerySettings as YdbExecuteQuerySettings } 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;
3
- export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings };
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, Ydb: typeof import("ydb-sdk-proto").Ydb;
3
+ export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, Ydb, };
4
4
  export declare function createExecuteQuerySettings(options?: {
5
5
  keepInCache?: boolean;
6
6
  idempotent?: boolean;
@@ -1,9 +1,9 @@
1
1
  import { createRequire } from "module";
2
- import { YDB_DATABASE, YDB_ENDPOINT, SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, } from "../config/env.js";
2
+ import { YDB_DATABASE, YDB_ENDPOINT, SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, STARTUP_PROBE_SESSION_TIMEOUT_MS, } 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, OperationParams, } = require("ydb-sdk");
6
- export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings };
5
+ const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, OperationParams, Ydb, } = require("ydb-sdk");
6
+ export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, Ydb, };
7
7
  export function createExecuteQuerySettings(options) {
8
8
  const { keepInCache = true, idempotent = true } = options ?? {};
9
9
  const settings = new ExecuteQuerySettings();
@@ -30,7 +30,6 @@ const DRIVER_READY_TIMEOUT_MS = 15000;
30
30
  const TABLE_SESSION_TIMEOUT_MS = 20000;
31
31
  const YDB_HEALTHCHECK_READY_TIMEOUT_MS = 5000;
32
32
  const DRIVER_REFRESH_COOLDOWN_MS = 30000;
33
- const STARTUP_PROBE_SESSION_TIMEOUT_MS = 3000;
34
33
  let overrideConfig;
35
34
  let driver;
36
35
  let lastDriverRefreshAt = 0;
@@ -1,4 +1,4 @@
1
- import { withSession, TableDescription, Column, Types } from "./client.js";
1
+ import { withSession, TableDescription, Column, Types, Ydb } 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";
@@ -41,10 +41,16 @@ export async function ensureGlobalPointsTable() {
41
41
  tableDescription = await s.describeTable(GLOBAL_POINTS_TABLE);
42
42
  }
43
43
  catch {
44
- // Table doesn't exist, create it with all columns using the new schema.
44
+ // Table doesn't exist, create it with all columns using the new schema and
45
+ // auto-partitioning enabled.
45
46
  const desc = new TableDescription()
46
47
  .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))
47
48
  .withPrimaryKeys("uid", "point_id");
49
+ desc.withPartitioningSettings({
50
+ partitioningByLoad: Ydb.FeatureFlag.Status.ENABLED,
51
+ partitioningBySize: Ydb.FeatureFlag.Status.ENABLED,
52
+ partitionSizeMb: 100,
53
+ });
48
54
  await s.createTable(GLOBAL_POINTS_TABLE, desc);
49
55
  globalPointsTableReady = true;
50
56
  logger.info(`created global points table ${GLOBAL_POINTS_TABLE}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-qdrant",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "main": "dist/package/api.js",
5
5
  "types": "dist/package/api.d.ts",
6
6
  "exports": {
@@ -50,6 +50,14 @@
50
50
  "author": "",
51
51
  "license": "Apache-2.0",
52
52
  "description": "Qdrant-compatible Node.js/TypeScript API that stores/searches embeddings in YDB using a global one-table layout with exact and approximate KNN search over serialized vectors.",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/astandrik/ydb-qdrant.git"
56
+ },
57
+ "homepage": "https://github.com/astandrik/ydb-qdrant#readme",
58
+ "bugs": {
59
+ "url": "https://github.com/astandrik/ydb-qdrant/issues"
60
+ },
53
61
  "type": "module",
54
62
  "publishConfig": {
55
63
  "access": "public"
@@ -83,4 +91,4 @@
83
91
  "typescript-eslint": "^8.47.0",
84
92
  "vitest": "^4.0.12"
85
93
  }
86
- }
94
+ }