ydb-qdrant 4.1.3 → 4.3.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 CHANGED
@@ -311,6 +311,57 @@ docker run -d --name ydb-qdrant \
311
311
  ydb-qdrant:latest
312
312
  ```
313
313
 
314
+ #### Docker (all-in-one: local YDB + ydb-qdrant)
315
+
316
+ For a single-container local dev/demo setup with both YDB and ydb-qdrant inside:
317
+
318
+ ```bash
319
+ docker pull ghcr.io/astandrik/ydb-qdrant-local:latest
320
+
321
+ docker run -d --name ydb-qdrant-local \
322
+ -p 8080:8080 \
323
+ -p 8765:8765 \
324
+ ghcr.io/astandrik/ydb-qdrant-local:latest
325
+ ```
326
+
327
+ Key env vars (all optional; the image provides sensible defaults, override only when you need custom tuning):
328
+
329
+ - YDB / local YDB:
330
+ - `YDB_LOCAL_GRPC_PORT` (default `2136`): internal YDB gRPC port.
331
+ - `YDB_LOCAL_MON_PORT` (default `8765`): internal YDB Embedded UI HTTP port.
332
+ - `YDB_DATABASE` (default `/local`).
333
+ - `YDB_ANONYMOUS_CREDENTIALS` (default `1` inside this image).
334
+ - `YDB_USE_IN_MEMORY_PDISKS` (default `0`, values `0`/`1`): store data in RAM only when `1` (fast, non-persistent).
335
+ - `YDB_LOCAL_SURVIVE_RESTART` (default `0`, values `0`/`1`): control persistence across restarts when using a mounted data volume.
336
+ - `YDB_DEFAULT_LOG_LEVEL`, `YDB_FEATURE_FLAGS`, `YDB_ENABLE_COLUMN_TABLES`, `YDB_KAFKA_PROXY_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD` – passed through to YDB as in the official `local-ydb` image.
337
+
338
+ - ydb-qdrant:
339
+ - `PORT` (default `8080`): HTTP port inside the container.
340
+ - `LOG_LEVEL` (default `info`).
341
+ - `VECTOR_INDEX_BUILD_ENABLED`.
342
+ - `YDB_QDRANT_COLLECTION_STORAGE_MODE` / `YDB_QDRANT_TABLE_LAYOUT` (`multi_table` or `one_table`).
343
+ - `YDB_QDRANT_GLOBAL_POINTS_AUTOMIGRATE`.
344
+
345
+ > Note: In the `ydb-qdrant-local` image, `YDB_ENDPOINT` is unconditionally set to `grpc://localhost:<YDB_LOCAL_GRPC_PORT>` by the entrypoint — any user-provided value is ignored. Use the standalone `ydb-qdrant` image if you need to connect to an external YDB.
346
+
347
+ #### Apple Silicon (Mac) notes
348
+
349
+ The `ydb-qdrant-local` image is built on top of the `local-ydb` Docker image, which is x86_64/amd64-only. On Apple Silicon (M1/M2/M3) you need to run it under x86_64/amd64 emulation:
350
+
351
+ - Enable Rosetta (x86_64/amd64 emulation) in your Docker backend:
352
+ - Docker Desktop: enable Rosetta to run x86_64/amd64 containers.
353
+ - Or use Colima as in the YDB docs:
354
+ - `colima start --arch aarch64 --vm-type=vz --vz-rosetta`
355
+ - When running the container, force the amd64 platform explicitly:
356
+
357
+ ```bash
358
+ docker run --platform linux/amd64 -d --name ydb-qdrant-local \
359
+ -p 8080:8080 -p 8765:8765 \
360
+ ghcr.io/astandrik/ydb-qdrant-local:latest
361
+ ```
362
+
363
+ This keeps behavior aligned with the official YDB `local-ydb` image recommendations for macOS/Apple Silicon.
364
+
314
365
  #### Docker Compose
315
366
 
316
367
  Example `docker-compose.yml` (can be used instead of raw `docker run`):
@@ -1,5 +1,4 @@
1
1
  import { Router } from "express";
2
- import { sanitizeCollectionName, sanitizeTenantId } from "../utils/tenant.js";
3
2
  import { putCollectionIndex, createCollection, getCollection, deleteCollection, } from "../services/CollectionService.js";
4
3
  import { QdrantServiceError } from "../services/errors.js";
5
4
  import { logger } from "../logging/logger.js";
@@ -9,6 +8,7 @@ collectionsRouter.put("/:collection/index", async (req, res) => {
9
8
  const result = await putCollectionIndex({
10
9
  tenant: req.header("X-Tenant-Id") ?? undefined,
11
10
  collection: String(req.params.collection),
11
+ apiKey: req.header("api-key") ?? undefined,
12
12
  });
13
13
  res.json({ status: "ok", result });
14
14
  }
@@ -23,9 +23,11 @@ collectionsRouter.put("/:collection/index", async (req, res) => {
23
23
  });
24
24
  collectionsRouter.put("/:collection", async (req, res) => {
25
25
  try {
26
- const tenant = sanitizeTenantId(req.header("X-Tenant-Id") ?? "default");
27
- const collection = sanitizeCollectionName(String(req.params.collection));
28
- const result = await createCollection({ tenant, collection }, req.body);
26
+ const result = await createCollection({
27
+ tenant: req.header("X-Tenant-Id") ?? undefined,
28
+ collection: String(req.params.collection),
29
+ apiKey: req.header("api-key") ?? undefined,
30
+ }, req.body);
29
31
  res.json({ status: "ok", result });
30
32
  }
31
33
  catch (err) {
@@ -39,9 +41,11 @@ collectionsRouter.put("/:collection", async (req, res) => {
39
41
  });
40
42
  collectionsRouter.get("/:collection", async (req, res) => {
41
43
  try {
42
- const tenant = sanitizeTenantId(req.header("X-Tenant-Id") ?? "default");
43
- const collection = sanitizeCollectionName(String(req.params.collection));
44
- const result = await getCollection({ tenant, collection });
44
+ const result = await getCollection({
45
+ tenant: req.header("X-Tenant-Id") ?? undefined,
46
+ collection: String(req.params.collection),
47
+ apiKey: req.header("api-key") ?? undefined,
48
+ });
45
49
  res.json({ status: "ok", result });
46
50
  }
47
51
  catch (err) {
@@ -55,9 +59,11 @@ collectionsRouter.get("/:collection", async (req, res) => {
55
59
  });
56
60
  collectionsRouter.delete("/:collection", async (req, res) => {
57
61
  try {
58
- const tenant = sanitizeTenantId(req.header("X-Tenant-Id") ?? "default");
59
- const collection = sanitizeCollectionName(String(req.params.collection));
60
- const result = await deleteCollection({ tenant, collection });
62
+ const result = await deleteCollection({
63
+ tenant: req.header("X-Tenant-Id") ?? undefined,
64
+ collection: String(req.params.collection),
65
+ apiKey: req.header("api-key") ?? undefined,
66
+ });
61
67
  res.json({ status: "ok", result });
62
68
  }
63
69
  catch (err) {
@@ -9,6 +9,7 @@ pointsRouter.put("/:collection/points", async (req, res) => {
9
9
  const result = await upsertPoints({
10
10
  tenant: req.header("X-Tenant-Id") ?? undefined,
11
11
  collection: String(req.params.collection),
12
+ apiKey: req.header("api-key") ?? undefined,
12
13
  }, req.body);
13
14
  res.json({ status: "ok", result });
14
15
  }
@@ -26,6 +27,7 @@ pointsRouter.post("/:collection/points/upsert", async (req, res) => {
26
27
  const result = await upsertPoints({
27
28
  tenant: req.header("X-Tenant-Id") ?? undefined,
28
29
  collection: String(req.params.collection),
30
+ apiKey: req.header("api-key") ?? undefined,
29
31
  }, req.body);
30
32
  res.json({ status: "ok", result });
31
33
  }
@@ -43,6 +45,7 @@ pointsRouter.post("/:collection/points/search", async (req, res) => {
43
45
  const result = await searchPoints({
44
46
  tenant: req.header("X-Tenant-Id") ?? undefined,
45
47
  collection: String(req.params.collection),
48
+ apiKey: req.header("api-key") ?? undefined,
46
49
  }, req.body);
47
50
  res.json({ status: "ok", result });
48
51
  }
@@ -61,6 +64,7 @@ pointsRouter.post("/:collection/points/query", async (req, res) => {
61
64
  const result = await queryPoints({
62
65
  tenant: req.header("X-Tenant-Id") ?? undefined,
63
66
  collection: String(req.params.collection),
67
+ apiKey: req.header("api-key") ?? undefined,
64
68
  }, req.body);
65
69
  res.json({ status: "ok", result });
66
70
  }
@@ -78,6 +82,7 @@ pointsRouter.post("/:collection/points/delete", async (req, res) => {
78
82
  const result = await deletePoints({
79
83
  tenant: req.header("X-Tenant-Id") ?? undefined,
80
84
  collection: String(req.params.collection),
85
+ apiKey: req.header("api-key") ?? undefined,
81
86
  }, req.body);
82
87
  res.json({ status: "ok", result });
83
88
  }
@@ -2,6 +2,7 @@ import { type DistanceKind } from "../types.js";
2
2
  export interface CollectionContextInput {
3
3
  tenant: string | undefined;
4
4
  collection: string;
5
+ apiKey?: string;
5
6
  }
6
7
  export interface NormalizedCollectionContext {
7
8
  tenant: string;
@@ -5,7 +5,7 @@ import { QdrantServiceError } from "./errors.js";
5
5
  import { normalizeCollectionContextShared, tableNameFor, } from "./CollectionService.shared.js";
6
6
  import { resolvePointsTableAndUidOneTable } from "./CollectionService.one-table.js";
7
7
  export function normalizeCollectionContext(input) {
8
- return normalizeCollectionContextShared(input.tenant, input.collection);
8
+ return normalizeCollectionContextShared(input.tenant, input.collection, input.apiKey);
9
9
  }
10
10
  export async function resolvePointsTableAndUid(ctx, meta) {
11
11
  if (meta?.table === GLOBAL_POINTS_TABLE) {
@@ -4,7 +4,7 @@ export interface NormalizedCollectionContextLike {
4
4
  }
5
5
  export declare function tableNameFor(tenantId: string, collection: string): string;
6
6
  export declare function uidFor(tenantId: string, collection: string): string;
7
- export declare function normalizeCollectionContextShared(tenant: string | undefined, collection: string): {
7
+ export declare function normalizeCollectionContextShared(tenant: string | undefined, collection: string, apiKey?: string): {
8
8
  tenant: string;
9
9
  collection: string;
10
10
  metaKey: string;
@@ -1,13 +1,14 @@
1
- import { sanitizeCollectionName, sanitizeTenantId, metaKeyFor, tableNameFor as tableNameForInternal, uidFor as uidForInternal, } from "../utils/tenant.js";
1
+ import { sanitizeCollectionName, sanitizeTenantId, metaKeyFor, tableNameFor as tableNameForInternal, uidFor as uidForInternal, hashApiKey, } from "../utils/tenant.js";
2
2
  export function tableNameFor(tenantId, collection) {
3
3
  return tableNameForInternal(tenantId, collection);
4
4
  }
5
5
  export function uidFor(tenantId, collection) {
6
6
  return uidForInternal(tenantId, collection);
7
7
  }
8
- export function normalizeCollectionContextShared(tenant, collection) {
8
+ export function normalizeCollectionContextShared(tenant, collection, apiKey) {
9
9
  const normalizedTenant = sanitizeTenantId(tenant);
10
- const normalizedCollection = sanitizeCollectionName(collection);
10
+ const apiKeyHash = hashApiKey(apiKey);
11
+ const normalizedCollection = sanitizeCollectionName(collection, apiKeyHash);
11
12
  const metaKey = metaKeyFor(normalizedTenant, normalizedCollection);
12
13
  return {
13
14
  tenant: normalizedTenant,
@@ -1,5 +1,6 @@
1
- export declare function sanitizeCollectionName(name: string): string;
1
+ export declare function hashApiKey(apiKey: string | undefined): string | undefined;
2
+ export declare function sanitizeCollectionName(name: string, apiKeyHash?: string): string;
2
3
  export declare function sanitizeTenantId(tenantId: string | undefined): string;
3
- export declare function tableNameFor(tenantId: string, collection: string): string;
4
- export declare function metaKeyFor(tenantId: string, collection: string): string;
5
- export declare function uidFor(tenantId: string, collectionName: string): string;
4
+ export declare function tableNameFor(sanitizedTenant: string, sanitizedCollection: string): string;
5
+ export declare function metaKeyFor(sanitizedTenant: string, sanitizedCollection: string): string;
6
+ export declare function uidFor(sanitizedTenant: string, sanitizedCollection: string): string;
@@ -1,7 +1,16 @@
1
- export function sanitizeCollectionName(name) {
1
+ import { createHash } from "crypto";
2
+ export function hashApiKey(apiKey) {
3
+ if (!apiKey || apiKey.trim() === "")
4
+ return undefined;
5
+ const hash = createHash("sha256").update(apiKey).digest("hex");
6
+ return hash.slice(0, 8);
7
+ }
8
+ export function sanitizeCollectionName(name, apiKeyHash) {
2
9
  const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_");
3
10
  const lowered = cleaned.toLowerCase().replace(/^_+/, "");
4
- return lowered.length > 0 ? lowered : "collection";
11
+ const base = lowered.length > 0 ? lowered : "collection";
12
+ const hasHash = apiKeyHash !== undefined && apiKeyHash.trim().length > 0;
13
+ return hasHash ? `${base}_${apiKeyHash}` : base;
5
14
  }
6
15
  export function sanitizeTenantId(tenantId) {
7
16
  const raw = (tenantId ?? "default").toString();
@@ -9,12 +18,12 @@ export function sanitizeTenantId(tenantId) {
9
18
  const lowered = cleaned.toLowerCase().replace(/^_+/, "");
10
19
  return lowered.length > 0 ? lowered : "default";
11
20
  }
12
- export function tableNameFor(tenantId, collection) {
13
- return `qdr_${sanitizeTenantId(tenantId)}__${sanitizeCollectionName(collection)}`;
21
+ export function tableNameFor(sanitizedTenant, sanitizedCollection) {
22
+ return `qdr_${sanitizedTenant}__${sanitizedCollection}`;
14
23
  }
15
- export function metaKeyFor(tenantId, collection) {
16
- return `${sanitizeTenantId(tenantId)}/${sanitizeCollectionName(collection)}`;
24
+ export function metaKeyFor(sanitizedTenant, sanitizedCollection) {
25
+ return `${sanitizedTenant}/${sanitizedCollection}`;
17
26
  }
18
- export function uidFor(tenantId, collectionName) {
19
- return tableNameFor(tenantId, collectionName);
27
+ export function uidFor(sanitizedTenant, sanitizedCollection) {
28
+ return tableNameFor(sanitizedTenant, sanitizedCollection);
20
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-qdrant",
3
- "version": "4.1.3",
3
+ "version": "4.3.0",
4
4
  "main": "dist/package/api.js",
5
5
  "types": "dist/package/api.d.ts",
6
6
  "exports": {