ydb-qdrant 3.0.0 → 4.1.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.
Files changed (54) hide show
  1. package/README.md +7 -3
  2. package/dist/SmokeTest.js +1 -1
  3. package/dist/config/env.d.ts +6 -0
  4. package/dist/config/env.js +17 -0
  5. package/dist/index.js +5 -2
  6. package/dist/indexing/IndexScheduler.js +6 -51
  7. package/dist/indexing/IndexScheduler.multi-table.d.ts +12 -0
  8. package/dist/indexing/IndexScheduler.multi-table.js +54 -0
  9. package/dist/indexing/IndexScheduler.one-table.d.ts +1 -0
  10. package/dist/indexing/IndexScheduler.one-table.js +4 -0
  11. package/dist/package/{Api.d.ts → api.d.ts} +3 -2
  12. package/dist/package/api.js +55 -0
  13. package/dist/repositories/collectionsRepo.d.ts +3 -2
  14. package/dist/repositories/collectionsRepo.js +27 -53
  15. package/dist/repositories/collectionsRepo.multi-table.d.ts +3 -0
  16. package/dist/repositories/collectionsRepo.multi-table.js +23 -0
  17. package/dist/repositories/collectionsRepo.one-table.d.ts +3 -0
  18. package/dist/repositories/collectionsRepo.one-table.js +25 -0
  19. package/dist/repositories/collectionsRepo.shared.d.ts +2 -0
  20. package/dist/repositories/collectionsRepo.shared.js +23 -0
  21. package/dist/repositories/pointsRepo.d.ts +3 -3
  22. package/dist/repositories/pointsRepo.js +15 -158
  23. package/dist/repositories/pointsRepo.multi-table.d.ts +12 -0
  24. package/dist/repositories/pointsRepo.multi-table.js +129 -0
  25. package/dist/repositories/pointsRepo.one-table.d.ts +12 -0
  26. package/dist/repositories/pointsRepo.one-table.js +111 -0
  27. package/dist/routes/collections.js +2 -1
  28. package/dist/routes/points.js +2 -1
  29. package/dist/services/CollectionService.d.ts +35 -0
  30. package/dist/services/CollectionService.js +85 -0
  31. package/dist/services/CollectionService.multi-table.d.ts +5 -0
  32. package/dist/services/CollectionService.multi-table.js +7 -0
  33. package/dist/services/CollectionService.one-table.d.ts +5 -0
  34. package/dist/services/CollectionService.one-table.js +9 -0
  35. package/dist/services/CollectionService.shared.d.ts +11 -0
  36. package/dist/services/CollectionService.shared.js +17 -0
  37. package/dist/services/PointsService.d.ts +23 -0
  38. package/dist/services/PointsService.js +128 -0
  39. package/dist/services/errors.d.ts +9 -0
  40. package/dist/services/errors.js +9 -0
  41. package/dist/utils/distance.d.ts +6 -0
  42. package/dist/utils/distance.js +28 -0
  43. package/dist/utils/normalization.d.ts +10 -0
  44. package/dist/utils/normalization.js +92 -0
  45. package/dist/utils/retry.d.ts +8 -0
  46. package/dist/utils/retry.js +28 -0
  47. package/dist/utils/tenant.d.ts +1 -0
  48. package/dist/utils/tenant.js +3 -0
  49. package/dist/ydb/schema.d.ts +2 -0
  50. package/dist/ydb/schema.js +27 -0
  51. package/package.json +4 -4
  52. package/dist/package/Api.js +0 -79
  53. package/dist/services/QdrantService.d.ts +0 -54
  54. package/dist/services/QdrantService.js +0 -313
@@ -1,163 +1,20 @@
1
- import { TypedValues, withSession } from "../ydb/client.js";
2
- import { buildJsonOrEmpty, buildVectorParam } from "../ydb/helpers.js";
3
- import { logger } from "../logging/logger.js";
4
- import { notifyUpsert } from "../indexing/IndexScheduler.js";
5
- import { VECTOR_INDEX_BUILD_ENABLED } from "../config/env.js";
6
- export async function upsertPoints(tableName, points, dimension) {
7
- let upserted = 0;
8
- await withSession(async (s) => {
9
- for (const p of points) {
10
- const id = String(p.id);
11
- if (p.vector.length !== dimension) {
12
- throw new Error(`Vector dimension mismatch for id=${id}: got ${p.vector.length}, expected ${dimension}`);
13
- }
14
- const ddl = `
15
- DECLARE $id AS Utf8;
16
- DECLARE $vec AS List<Float>;
17
- DECLARE $payload AS JsonDocument;
18
- UPSERT INTO ${tableName} (point_id, embedding, payload)
19
- VALUES (
20
- $id,
21
- Untag(Knn::ToBinaryStringFloat($vec), "FloatVector"),
22
- $payload
23
- );
24
- `;
25
- const params = {
26
- $id: TypedValues.utf8(id),
27
- $vec: buildVectorParam(p.vector),
28
- $payload: buildJsonOrEmpty(p.payload),
29
- };
30
- // Retry on transient schema/metadata mismatches during index rebuild
31
- const maxRetries = 6; // ~ up to ~ (0.25 + jitter) * 2^5 ≈ few seconds
32
- let attempt = 0;
33
- while (true) {
34
- try {
35
- await s.executeQuery(ddl, params);
36
- break;
37
- }
38
- catch (e) {
39
- const msg = e instanceof Error ? e.message : String(e);
40
- const isTransient = /Aborted|schema version mismatch|Table metadata loading|Failed to load metadata/i.test(msg);
41
- if (!isTransient || attempt >= maxRetries) {
42
- throw e;
43
- }
44
- const backoffMs = Math.floor(250 * Math.pow(2, attempt) + Math.random() * 100);
45
- logger.warn({ tableName, id, attempt, backoffMs }, "upsert aborted due to schema/metadata change; retrying");
46
- await new Promise((r) => setTimeout(r, backoffMs));
47
- attempt += 1;
48
- }
49
- }
50
- upserted += 1;
51
- }
52
- });
53
- // notify scheduler for potential end-of-batch index build
54
- notifyUpsert(tableName, upserted);
55
- // No index rebuild; approximate search does not require it
56
- return upserted;
57
- }
58
- // Removed legacy index backfill helper
59
- export async function searchPoints(tableName, queryVector, top, withPayload, distance, dimension) {
60
- if (queryVector.length !== dimension) {
61
- throw new Error(`Vector dimension mismatch: got ${queryVector.length}, expected ${dimension}`);
62
- }
63
- const { fn, order } = mapDistanceToKnnFn(distance);
64
- // Single-phase search over embedding using vector index if present
65
- const qf = buildVectorParam(queryVector);
66
- const params = {
67
- $qf: qf,
68
- $k2: TypedValues.uint32(top),
69
- };
70
- const buildQuery = (useIndex) => `
71
- DECLARE $qf AS List<Float>;
72
- DECLARE $k2 AS Uint32;
73
- $qbinf = Knn::ToBinaryStringFloat($qf);
74
- SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
75
- FROM ${tableName}${useIndex ? " VIEW emb_idx" : ""}
76
- ORDER BY score ${order}
77
- LIMIT $k2;
78
- `;
79
- let rs;
80
- if (VECTOR_INDEX_BUILD_ENABLED) {
81
- try {
82
- // Try with vector index first
83
- rs = await withSession(async (s) => {
84
- return await s.executeQuery(buildQuery(true), params);
85
- });
86
- logger.info({ tableName }, "vector index found; using index for search");
87
- }
88
- catch (e) {
89
- const msg = e instanceof Error ? e.message : String(e);
90
- const indexUnavailable = /not found|does not exist|no such index|no global index|is not ready to use/i.test(msg);
91
- if (indexUnavailable) {
92
- logger.info({ tableName }, "vector index not available (missing or building); falling back to table scan");
93
- rs = await withSession(async (s) => {
94
- return await s.executeQuery(buildQuery(false), params);
95
- });
96
- }
97
- else {
98
- throw e;
99
- }
100
- }
1
+ import { upsertPointsMultiTable, searchPointsMultiTable, deletePointsMultiTable, } from "./pointsRepo.multi-table.js";
2
+ import { upsertPointsOneTable, searchPointsOneTable, deletePointsOneTable, } from "./pointsRepo.one-table.js";
3
+ export async function upsertPoints(tableName, points, dimension, uid) {
4
+ if (uid) {
5
+ return await upsertPointsOneTable(tableName, points, dimension, uid);
101
6
  }
102
- else {
103
- // Vector index usage disabled: always use table scan
104
- rs = await withSession(async (s) => {
105
- return await s.executeQuery(buildQuery(false), params);
106
- });
107
- }
108
- const rowset = rs.resultSets?.[0];
109
- const rows = (rowset?.rows ?? []);
110
- return rows.map((row) => {
111
- const id = row.items?.[0]?.textValue;
112
- if (typeof id !== "string") {
113
- throw new Error("point_id is missing in YDB search result");
114
- }
115
- let payload;
116
- let scoreIdx = 1;
117
- if (withPayload) {
118
- const payloadText = row.items?.[1]?.textValue;
119
- if (payloadText) {
120
- try {
121
- payload = JSON.parse(payloadText);
122
- }
123
- catch {
124
- payload = undefined;
125
- }
126
- }
127
- scoreIdx = 2;
128
- }
129
- const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
130
- return { id, score, ...(payload ? { payload } : {}) };
131
- });
7
+ return await upsertPointsMultiTable(tableName, points, dimension);
132
8
  }
133
- export async function deletePoints(tableName, ids) {
134
- let deleted = 0;
135
- await withSession(async (s) => {
136
- for (const id of ids) {
137
- const yql = `
138
- DECLARE $id AS Utf8;
139
- DELETE FROM ${tableName} WHERE point_id = $id;
140
- `;
141
- const params = {
142
- $id: TypedValues.utf8(String(id)),
143
- };
144
- await s.executeQuery(yql, params);
145
- deleted += 1;
146
- }
147
- });
148
- return deleted;
9
+ export async function searchPoints(tableName, queryVector, top, withPayload, distance, dimension, uid) {
10
+ if (uid) {
11
+ return await searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid);
12
+ }
13
+ return await searchPointsMultiTable(tableName, queryVector, top, withPayload, distance, dimension);
149
14
  }
150
- function mapDistanceToKnnFn(distance) {
151
- switch (distance) {
152
- case "Cosine":
153
- return { fn: "Knn::CosineSimilarity", order: "DESC" };
154
- case "Dot":
155
- return { fn: "Knn::InnerProductSimilarity", order: "DESC" };
156
- case "Euclid":
157
- return { fn: "Knn::EuclideanDistance", order: "ASC" };
158
- case "Manhattan":
159
- return { fn: "Knn::ManhattanDistance", order: "ASC" };
160
- default:
161
- return { fn: "Knn::CosineSimilarity", order: "DESC" };
15
+ export async function deletePoints(tableName, ids, uid) {
16
+ if (uid) {
17
+ return await deletePointsOneTable(tableName, ids, uid);
162
18
  }
19
+ return await deletePointsMultiTable(tableName, ids);
163
20
  }
@@ -0,0 +1,12 @@
1
+ import type { DistanceKind } from "../types";
2
+ export declare function upsertPointsMultiTable(tableName: string, points: Array<{
3
+ id: string | number;
4
+ vector: number[];
5
+ payload?: Record<string, unknown>;
6
+ }>, dimension: number): Promise<number>;
7
+ export declare function searchPointsMultiTable(tableName: string, queryVector: number[], top: number, withPayload: boolean | undefined, distance: DistanceKind, dimension: number): Promise<Array<{
8
+ id: string;
9
+ score: number;
10
+ payload?: Record<string, unknown>;
11
+ }>>;
12
+ export declare function deletePointsMultiTable(tableName: string, ids: Array<string | number>): Promise<number>;
@@ -0,0 +1,129 @@
1
+ import { TypedValues, withSession } from "../ydb/client.js";
2
+ import { buildJsonOrEmpty, buildVectorParam } from "../ydb/helpers.js";
3
+ import { logger } from "../logging/logger.js";
4
+ import { notifyUpsert } from "../indexing/IndexScheduler.js";
5
+ import { VECTOR_INDEX_BUILD_ENABLED } from "../config/env.js";
6
+ import { mapDistanceToKnnFn } from "../utils/distance.js";
7
+ import { withRetry, isTransientYdbError } from "../utils/retry.js";
8
+ export async function upsertPointsMultiTable(tableName, points, dimension) {
9
+ let upserted = 0;
10
+ await withSession(async (s) => {
11
+ for (const p of points) {
12
+ const id = String(p.id);
13
+ if (p.vector.length !== dimension) {
14
+ throw new Error(`Vector dimension mismatch for id=${id}: got ${p.vector.length}, expected ${dimension}`);
15
+ }
16
+ const ddl = `
17
+ DECLARE $id AS Utf8;
18
+ DECLARE $vec AS List<Float>;
19
+ DECLARE $payload AS JsonDocument;
20
+ UPSERT INTO ${tableName} (point_id, embedding, payload)
21
+ VALUES (
22
+ $id,
23
+ Untag(Knn::ToBinaryStringFloat($vec), "FloatVector"),
24
+ $payload
25
+ );
26
+ `;
27
+ const params = {
28
+ $id: TypedValues.utf8(id),
29
+ $vec: buildVectorParam(p.vector),
30
+ $payload: buildJsonOrEmpty(p.payload),
31
+ };
32
+ await withRetry(() => s.executeQuery(ddl, params), {
33
+ isTransient: isTransientYdbError,
34
+ context: { tableName, id },
35
+ });
36
+ upserted += 1;
37
+ }
38
+ });
39
+ notifyUpsert(tableName, upserted);
40
+ return upserted;
41
+ }
42
+ export async function searchPointsMultiTable(tableName, queryVector, top, withPayload, distance, dimension) {
43
+ if (queryVector.length !== dimension) {
44
+ throw new Error(`Vector dimension mismatch: got ${queryVector.length}, expected ${dimension}`);
45
+ }
46
+ const { fn, order } = mapDistanceToKnnFn(distance);
47
+ const qf = buildVectorParam(queryVector);
48
+ const params = {
49
+ $qf: qf,
50
+ $k2: TypedValues.uint32(top),
51
+ };
52
+ const buildQuery = (useIndex) => `
53
+ DECLARE $qf AS List<Float>;
54
+ DECLARE $k2 AS Uint32;
55
+ $qbinf = Knn::ToBinaryStringFloat($qf);
56
+ SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
57
+ FROM ${tableName}${useIndex ? " VIEW emb_idx" : ""}
58
+ ORDER BY score ${order}
59
+ LIMIT $k2;
60
+ `;
61
+ let rs;
62
+ if (VECTOR_INDEX_BUILD_ENABLED) {
63
+ try {
64
+ rs = await withSession(async (s) => {
65
+ return await s.executeQuery(buildQuery(true), params);
66
+ });
67
+ logger.info({ tableName }, "vector index found; using index for search");
68
+ }
69
+ catch (e) {
70
+ const msg = e instanceof Error ? e.message : String(e);
71
+ const indexUnavailable = /not found|does not exist|no such index|no global index|is not ready to use/i.test(msg);
72
+ if (indexUnavailable) {
73
+ logger.info({ tableName }, "vector index not available (missing or building); falling back to table scan");
74
+ rs = await withSession(async (s) => {
75
+ return await s.executeQuery(buildQuery(false), params);
76
+ });
77
+ }
78
+ else {
79
+ throw e;
80
+ }
81
+ }
82
+ }
83
+ else {
84
+ rs = await withSession(async (s) => {
85
+ return await s.executeQuery(buildQuery(false), params);
86
+ });
87
+ }
88
+ const rowset = rs.resultSets?.[0];
89
+ const rows = (rowset?.rows ?? []);
90
+ return rows.map((row) => {
91
+ const id = row.items?.[0]?.textValue;
92
+ if (typeof id !== "string") {
93
+ throw new Error("point_id is missing in YDB search result");
94
+ }
95
+ let payload;
96
+ let scoreIdx = 1;
97
+ if (withPayload) {
98
+ const payloadText = row.items?.[1]?.textValue;
99
+ if (payloadText) {
100
+ try {
101
+ payload = JSON.parse(payloadText);
102
+ }
103
+ catch {
104
+ payload = undefined;
105
+ }
106
+ }
107
+ scoreIdx = 2;
108
+ }
109
+ const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
110
+ return { id, score, ...(payload ? { payload } : {}) };
111
+ });
112
+ }
113
+ export async function deletePointsMultiTable(tableName, ids) {
114
+ let deleted = 0;
115
+ await withSession(async (s) => {
116
+ for (const id of ids) {
117
+ const yql = `
118
+ DECLARE $id AS Utf8;
119
+ DELETE FROM ${tableName} WHERE point_id = $id;
120
+ `;
121
+ const params = {
122
+ $id: TypedValues.utf8(String(id)),
123
+ };
124
+ await s.executeQuery(yql, params);
125
+ deleted += 1;
126
+ }
127
+ });
128
+ return deleted;
129
+ }
@@ -0,0 +1,12 @@
1
+ import type { DistanceKind } from "../types";
2
+ export declare function upsertPointsOneTable(tableName: string, points: Array<{
3
+ id: string | number;
4
+ vector: number[];
5
+ payload?: Record<string, unknown>;
6
+ }>, 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
+ id: string;
9
+ score: number;
10
+ payload?: Record<string, unknown>;
11
+ }>>;
12
+ export declare function deletePointsOneTable(tableName: string, ids: Array<string | number>, uid: string): Promise<number>;
@@ -0,0 +1,111 @@
1
+ import { TypedValues, withSession } from "../ydb/client.js";
2
+ import { buildJsonOrEmpty, buildVectorParam } from "../ydb/helpers.js";
3
+ import { notifyUpsert } from "../indexing/IndexScheduler.js";
4
+ import { mapDistanceToKnnFn } from "../utils/distance.js";
5
+ import { withRetry, isTransientYdbError } from "../utils/retry.js";
6
+ export async function upsertPointsOneTable(tableName, points, dimension, uid) {
7
+ let upserted = 0;
8
+ await withSession(async (s) => {
9
+ for (const p of points) {
10
+ const id = String(p.id);
11
+ if (p.vector.length !== dimension) {
12
+ throw new Error(`Vector dimension mismatch for id=${id}: got ${p.vector.length}, expected ${dimension}`);
13
+ }
14
+ const ddl = `
15
+ DECLARE $uid AS Utf8;
16
+ DECLARE $id AS Utf8;
17
+ DECLARE $vec AS List<Float>;
18
+ DECLARE $payload AS JsonDocument;
19
+ UPSERT INTO ${tableName} (uid, point_id, embedding, payload)
20
+ VALUES (
21
+ $uid,
22
+ $id,
23
+ Untag(Knn::ToBinaryStringFloat($vec), "FloatVector"),
24
+ $payload
25
+ );
26
+ `;
27
+ const params = {
28
+ $uid: TypedValues.utf8(uid),
29
+ $id: TypedValues.utf8(id),
30
+ $vec: buildVectorParam(p.vector),
31
+ $payload: buildJsonOrEmpty(p.payload),
32
+ };
33
+ await withRetry(() => s.executeQuery(ddl, params), {
34
+ isTransient: isTransientYdbError,
35
+ context: { tableName, id },
36
+ });
37
+ upserted += 1;
38
+ }
39
+ });
40
+ notifyUpsert(tableName, upserted);
41
+ return upserted;
42
+ }
43
+ export async function searchPointsOneTable(tableName, queryVector, top, withPayload, distance, dimension, uid) {
44
+ if (queryVector.length !== dimension) {
45
+ throw new Error(`Vector dimension mismatch: got ${queryVector.length}, expected ${dimension}`);
46
+ }
47
+ const { fn, order } = mapDistanceToKnnFn(distance);
48
+ const qf = buildVectorParam(queryVector);
49
+ const params = {
50
+ $qf: qf,
51
+ $k2: TypedValues.uint32(top),
52
+ $uid: TypedValues.utf8(uid),
53
+ };
54
+ const query = `
55
+ DECLARE $qf AS List<Float>;
56
+ DECLARE $k2 AS Uint32;
57
+ DECLARE $uid AS Utf8;
58
+ $qbinf = Knn::ToBinaryStringFloat($qf);
59
+ SELECT point_id, ${withPayload ? "payload, " : ""}${fn}(embedding, $qbinf) AS score
60
+ FROM ${tableName}
61
+ WHERE uid = $uid
62
+ ORDER BY score ${order}
63
+ LIMIT $k2;
64
+ `;
65
+ const rs = await withSession(async (s) => {
66
+ return await s.executeQuery(query, params);
67
+ });
68
+ const rowset = rs.resultSets?.[0];
69
+ const rows = (rowset?.rows ?? []);
70
+ return rows.map((row) => {
71
+ const id = row.items?.[0]?.textValue;
72
+ if (typeof id !== "string") {
73
+ throw new Error("point_id is missing in YDB search result");
74
+ }
75
+ let payload;
76
+ let scoreIdx = 1;
77
+ if (withPayload) {
78
+ const payloadText = row.items?.[1]?.textValue;
79
+ if (payloadText) {
80
+ try {
81
+ payload = JSON.parse(payloadText);
82
+ }
83
+ catch {
84
+ payload = undefined;
85
+ }
86
+ }
87
+ scoreIdx = 2;
88
+ }
89
+ const score = Number(row.items?.[scoreIdx]?.floatValue ?? row.items?.[scoreIdx]?.textValue);
90
+ return { id, score, ...(payload ? { payload } : {}) };
91
+ });
92
+ }
93
+ export async function deletePointsOneTable(tableName, ids, uid) {
94
+ let deleted = 0;
95
+ await withSession(async (s) => {
96
+ for (const id of ids) {
97
+ const yql = `
98
+ DECLARE $uid AS Utf8;
99
+ DECLARE $id AS Utf8;
100
+ DELETE FROM ${tableName} WHERE uid = $uid AND point_id = $id;
101
+ `;
102
+ const params = {
103
+ $uid: TypedValues.utf8(uid),
104
+ $id: TypedValues.utf8(String(id)),
105
+ };
106
+ await s.executeQuery(yql, params);
107
+ deleted += 1;
108
+ }
109
+ });
110
+ return deleted;
111
+ }
@@ -1,6 +1,7 @@
1
1
  import { Router } from "express";
2
2
  import { sanitizeCollectionName, sanitizeTenantId } from "../utils/tenant.js";
3
- import { QdrantServiceError, putCollectionIndex, createCollection, getCollection, deleteCollection, } from "../services/QdrantService.js";
3
+ import { putCollectionIndex, createCollection, getCollection, deleteCollection, } from "../services/CollectionService.js";
4
+ import { QdrantServiceError } from "../services/errors.js";
4
5
  import { logger } from "../logging/logger.js";
5
6
  export const collectionsRouter = Router();
6
7
  collectionsRouter.put("/:collection/index", async (req, res) => {
@@ -1,5 +1,6 @@
1
1
  import { Router } from "express";
2
- import { QdrantServiceError, upsertPoints, searchPoints, queryPoints, deletePoints, } from "../services/QdrantService.js";
2
+ import { upsertPoints, searchPoints, queryPoints, deletePoints, } from "../services/PointsService.js";
3
+ import { QdrantServiceError } from "../services/errors.js";
3
4
  import { logger } from "../logging/logger.js";
4
5
  export const pointsRouter = Router();
5
6
  // Qdrant-compatible: PUT /collections/:collection/points (upsert)
@@ -0,0 +1,35 @@
1
+ import { type DistanceKind } from "../types.js";
2
+ export interface CollectionContextInput {
3
+ tenant: string | undefined;
4
+ collection: string;
5
+ }
6
+ export interface NormalizedCollectionContext {
7
+ tenant: string;
8
+ collection: string;
9
+ metaKey: string;
10
+ }
11
+ export declare function normalizeCollectionContext(input: CollectionContextInput): NormalizedCollectionContext;
12
+ export declare function resolvePointsTableAndUid(ctx: NormalizedCollectionContext, meta: {
13
+ table: string;
14
+ }): Promise<{
15
+ tableName: string;
16
+ uid: string | undefined;
17
+ }>;
18
+ export declare function putCollectionIndex(ctx: CollectionContextInput): Promise<{
19
+ acknowledged: boolean;
20
+ }>;
21
+ export declare function createCollection(ctx: CollectionContextInput, body: unknown): Promise<{
22
+ name: string;
23
+ tenant: string;
24
+ }>;
25
+ export declare function getCollection(ctx: CollectionContextInput): Promise<{
26
+ name: string;
27
+ vectors: {
28
+ size: number;
29
+ distance: DistanceKind;
30
+ data_type: string;
31
+ };
32
+ }>;
33
+ export declare function deleteCollection(ctx: CollectionContextInput): Promise<{
34
+ acknowledged: boolean;
35
+ }>;
@@ -0,0 +1,85 @@
1
+ import { CreateCollectionReq } from "../types.js";
2
+ import { ensureMetaTable, GLOBAL_POINTS_TABLE } from "../ydb/schema.js";
3
+ import { createCollection as repoCreateCollection, deleteCollection as repoDeleteCollection, getCollectionMeta, } from "../repositories/collectionsRepo.js";
4
+ import { QdrantServiceError } from "./errors.js";
5
+ import { normalizeCollectionContextShared, tableNameFor, } from "./CollectionService.shared.js";
6
+ import { resolvePointsTableAndUidOneTable } from "./CollectionService.one-table.js";
7
+ export function normalizeCollectionContext(input) {
8
+ return normalizeCollectionContextShared(input.tenant, input.collection);
9
+ }
10
+ export async function resolvePointsTableAndUid(ctx, meta) {
11
+ if (meta?.table === GLOBAL_POINTS_TABLE) {
12
+ return await resolvePointsTableAndUidOneTable(ctx);
13
+ }
14
+ return {
15
+ tableName: meta.table,
16
+ uid: undefined,
17
+ };
18
+ }
19
+ export async function putCollectionIndex(ctx) {
20
+ await ensureMetaTable();
21
+ const normalized = normalizeCollectionContext(ctx);
22
+ const meta = await getCollectionMeta(normalized.metaKey);
23
+ if (!meta) {
24
+ throw new QdrantServiceError(404, {
25
+ status: "error",
26
+ error: "collection not found",
27
+ });
28
+ }
29
+ return { acknowledged: true };
30
+ }
31
+ export async function createCollection(ctx, body) {
32
+ await ensureMetaTable();
33
+ const normalized = normalizeCollectionContext(ctx);
34
+ const parsed = CreateCollectionReq.safeParse(body);
35
+ if (!parsed.success) {
36
+ throw new QdrantServiceError(400, {
37
+ status: "error",
38
+ error: parsed.error.flatten(),
39
+ });
40
+ }
41
+ const dim = parsed.data.vectors.size;
42
+ const distance = parsed.data.vectors.distance;
43
+ const vectorType = parsed.data.vectors.data_type ?? "float";
44
+ const existing = await getCollectionMeta(normalized.metaKey);
45
+ if (existing) {
46
+ if (existing.dimension === dim &&
47
+ existing.distance === distance &&
48
+ existing.vectorType === vectorType) {
49
+ return { name: normalized.collection, tenant: normalized.tenant };
50
+ }
51
+ const errorMessage = `Collection already exists with different config: dimension=${existing.dimension}, distance=${existing.distance}, type=${existing.vectorType}`;
52
+ throw new QdrantServiceError(400, {
53
+ status: "error",
54
+ error: errorMessage,
55
+ });
56
+ }
57
+ const tableName = tableNameFor(normalized.tenant, normalized.collection);
58
+ await repoCreateCollection(normalized.metaKey, dim, distance, vectorType, tableName);
59
+ return { name: normalized.collection, tenant: normalized.tenant };
60
+ }
61
+ export async function getCollection(ctx) {
62
+ await ensureMetaTable();
63
+ const normalized = normalizeCollectionContext(ctx);
64
+ const meta = await getCollectionMeta(normalized.metaKey);
65
+ if (!meta) {
66
+ throw new QdrantServiceError(404, {
67
+ status: "error",
68
+ error: "collection not found",
69
+ });
70
+ }
71
+ return {
72
+ name: normalized.collection,
73
+ vectors: {
74
+ size: meta.dimension,
75
+ distance: meta.distance,
76
+ data_type: meta.vectorType,
77
+ },
78
+ };
79
+ }
80
+ export async function deleteCollection(ctx) {
81
+ await ensureMetaTable();
82
+ const normalized = normalizeCollectionContext(ctx);
83
+ await repoDeleteCollection(normalized.metaKey);
84
+ return { acknowledged: true };
85
+ }
@@ -0,0 +1,5 @@
1
+ import { type NormalizedCollectionContextLike } from "./CollectionService.shared.js";
2
+ export declare function resolvePointsTableAndUidMultiTable(ctx: NormalizedCollectionContextLike): {
3
+ tableName: string;
4
+ uid: string | undefined;
5
+ };
@@ -0,0 +1,7 @@
1
+ import { tableNameFor, } from "./CollectionService.shared.js";
2
+ export function resolvePointsTableAndUidMultiTable(ctx) {
3
+ return {
4
+ tableName: tableNameFor(ctx.tenant, ctx.collection),
5
+ uid: undefined,
6
+ };
7
+ }
@@ -0,0 +1,5 @@
1
+ import { type NormalizedCollectionContextLike } from "./CollectionService.shared.js";
2
+ export declare function resolvePointsTableAndUidOneTable(ctx: NormalizedCollectionContextLike): Promise<{
3
+ tableName: string;
4
+ uid: string | undefined;
5
+ }>;
@@ -0,0 +1,9 @@
1
+ import { uidFor, } from "./CollectionService.shared.js";
2
+ import { GLOBAL_POINTS_TABLE, ensureGlobalPointsTable } from "../ydb/schema.js";
3
+ export async function resolvePointsTableAndUidOneTable(ctx) {
4
+ await ensureGlobalPointsTable();
5
+ return {
6
+ tableName: GLOBAL_POINTS_TABLE,
7
+ uid: uidFor(ctx.tenant, ctx.collection),
8
+ };
9
+ }
@@ -0,0 +1,11 @@
1
+ export interface NormalizedCollectionContextLike {
2
+ tenant: string;
3
+ collection: string;
4
+ }
5
+ export declare function tableNameFor(tenantId: string, collection: string): string;
6
+ export declare function uidFor(tenantId: string, collection: string): string;
7
+ export declare function normalizeCollectionContextShared(tenant: string | undefined, collection: string): {
8
+ tenant: string;
9
+ collection: string;
10
+ metaKey: string;
11
+ };
@@ -0,0 +1,17 @@
1
+ import { sanitizeCollectionName, sanitizeTenantId, metaKeyFor, tableNameFor as tableNameForInternal, uidFor as uidForInternal, } from "../utils/tenant.js";
2
+ export function tableNameFor(tenantId, collection) {
3
+ return tableNameForInternal(tenantId, collection);
4
+ }
5
+ export function uidFor(tenantId, collection) {
6
+ return uidForInternal(tenantId, collection);
7
+ }
8
+ export function normalizeCollectionContextShared(tenant, collection) {
9
+ const normalizedTenant = sanitizeTenantId(tenant);
10
+ const normalizedCollection = sanitizeCollectionName(collection);
11
+ const metaKey = metaKeyFor(normalizedTenant, normalizedCollection);
12
+ return {
13
+ tenant: normalizedTenant,
14
+ collection: normalizedCollection,
15
+ metaKey,
16
+ };
17
+ }
@@ -0,0 +1,23 @@
1
+ import { type CollectionContextInput } from "./CollectionService.js";
2
+ type PointsContextInput = CollectionContextInput;
3
+ export declare function upsertPoints(ctx: PointsContextInput, body: unknown): Promise<{
4
+ upserted: number;
5
+ }>;
6
+ export declare function searchPoints(ctx: PointsContextInput, body: unknown): Promise<{
7
+ points: Array<{
8
+ id: string;
9
+ score: number;
10
+ payload?: Record<string, unknown>;
11
+ }>;
12
+ }>;
13
+ export declare function queryPoints(ctx: PointsContextInput, body: unknown): Promise<{
14
+ points: Array<{
15
+ id: string;
16
+ score: number;
17
+ payload?: Record<string, unknown>;
18
+ }>;
19
+ }>;
20
+ export declare function deletePoints(ctx: PointsContextInput, body: unknown): Promise<{
21
+ deleted: number;
22
+ }>;
23
+ export {};