@memberjunction/ai-vectors-sqlserver 0.0.1 → 5.39.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 +86 -43
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/models/SQLServerVectorDatabase.d.ts +109 -0
- package/dist/models/SQLServerVectorDatabase.d.ts.map +1 -0
- package/dist/models/SQLServerVectorDatabase.js +704 -0
- package/dist/models/SQLServerVectorDatabase.js.map +1 -0
- package/dist/models/sqlserverColocatedSQL.d.ts +95 -0
- package/dist/models/sqlserverColocatedSQL.d.ts.map +1 -0
- package/dist/models/sqlserverColocatedSQL.js +255 -0
- package/dist/models/sqlserverColocatedSQL.js.map +1 -0
- package/package.json +30 -7
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var SQLServerVectorDatabase_1;
|
|
11
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
12
|
+
import { ValidateSqlIdentifier, VectorDBBase, VectorMetadataFilter, } from '@memberjunction/ai-vectordb';
|
|
13
|
+
import { LogError, LogStatus } from '@memberjunction/core';
|
|
14
|
+
import { BuildApproximateVectorQuery, BuildExactVectorQuery, BuildFilterCountQuery, DeriveContent, DistanceToScore, ParseVectorJson, SafeTopK, VectorJson, DEFAULT_ITERATIVE_FILTER_THRESHOLD, SQLSERVER_2025_MAJOR_VERSION, } from './sqlserverColocatedSQL.js';
|
|
15
|
+
/** Registry table tracking colocated vector indexes (and their storage mapping) in the host schema. */
|
|
16
|
+
const INDEX_REGISTRY_TABLE = '_mj_vector_indexes';
|
|
17
|
+
/**
|
|
18
|
+
* Colocated MemberJunction vector provider backed by **SQL Server 2025 native vectors**
|
|
19
|
+
* (`VECTOR(N)` + the `VECTOR_SEARCH` DiskANN table-valued function), stored in the application's
|
|
20
|
+
* own database. Borrows the active data provider's connection via {@link IColocatedVectorHost}.
|
|
21
|
+
*
|
|
22
|
+
* **Two storage modes** (per index, via `MJVectorIndex.ProviderConfig` → `CreateIndexParams.additionalParams`):
|
|
23
|
+
* - `sibling` (default): MJ creates and owns a sibling table (`id`/`embedding`/`metadata`/`content`).
|
|
24
|
+
* Generic, entity-agnostic, multi-model. Filters resolve against the JSON `metadata` column.
|
|
25
|
+
* - `entityColumn`: vectors live on an existing entity table's `VECTOR` column. Filters resolve
|
|
26
|
+
* against **live entity columns**, and results project real entity fields. This is the migration
|
|
27
|
+
* target for systems that already store embeddings on their own tables (e.g. a `Content.Embedding`
|
|
28
|
+
* column) — point MJ at the column instead of re-vectorizing.
|
|
29
|
+
*
|
|
30
|
+
* **Query path** prefers the DiskANN-aware `VECTOR_SEARCH` TVF (never `ORDER BY VECTOR_DISTANCE`,
|
|
31
|
+
* which doesn't engage the index). But that surface isn't in every 2025 build — verified live, boxed
|
|
32
|
+
* SQL Server 2025 RTM lacks it — so the provider lazily detects (per host) whether `VECTOR_SEARCH`
|
|
33
|
+
* works and **falls back to an exact `VECTOR_DISTANCE` scan** when it doesn't. Where the TVF exists,
|
|
34
|
+
* filtered queries dispatch on cardinality: small filtered sets use the exact path (DiskANN doesn't
|
|
35
|
+
* converge when the filter cluster is disjoint from the query neighborhood), large sets use DiskANN.
|
|
36
|
+
* See {@link sqlserverColocatedSQL} for the production-verified SQL shapes and the reasoning behind each.
|
|
37
|
+
*
|
|
38
|
+
* Validated end-to-end against a live SQL Server 2025 RTM container in sibling and entityColumn modes
|
|
39
|
+
* (exact fallback path). The approximate path on Azure SQL Database remains untested.
|
|
40
|
+
*
|
|
41
|
+
* Registered with the MJ class factory as `'SQLServerVectorDatabase'`.
|
|
42
|
+
*/
|
|
43
|
+
let SQLServerVectorDatabase = class SQLServerVectorDatabase extends VectorDBBase {
|
|
44
|
+
static { SQLServerVectorDatabase_1 = this; }
|
|
45
|
+
constructor(apiKey) {
|
|
46
|
+
super(apiKey && apiKey.trim().length > 0 ? apiKey : 'colocated');
|
|
47
|
+
}
|
|
48
|
+
get SupportsColocatedQuery() {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// Hybrid keyword search needs a SQL Server full-text catalog/index — future enhancement.
|
|
52
|
+
get SupportsHybridSearch() {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// ----------------------------------------------------------------
|
|
56
|
+
// Host connection access
|
|
57
|
+
// ----------------------------------------------------------------
|
|
58
|
+
get Host() {
|
|
59
|
+
if (!this.ColocatedHost) {
|
|
60
|
+
throw new Error('SQLServerVectorDatabase requires a host connection. Call TryWireColocatedHost()/SetColocatedHost() with the active data provider before use.');
|
|
61
|
+
}
|
|
62
|
+
return this.ColocatedHost;
|
|
63
|
+
}
|
|
64
|
+
get Schema() {
|
|
65
|
+
return this.Host.ColocatedSchema;
|
|
66
|
+
}
|
|
67
|
+
Qualify(tableName) {
|
|
68
|
+
return `[${ValidateSqlIdentifier(this.Schema, 'schema')}].[${ValidateSqlIdentifier(tableName, 'table')}]`;
|
|
69
|
+
}
|
|
70
|
+
Run(sql, params) {
|
|
71
|
+
return this.Host.RunColocatedSQL(sql, params);
|
|
72
|
+
}
|
|
73
|
+
// ----------------------------------------------------------------
|
|
74
|
+
// Capability detection (fail loud)
|
|
75
|
+
// ----------------------------------------------------------------
|
|
76
|
+
async AssertVectorCapable() {
|
|
77
|
+
const rows = await this.Run(`SELECT CAST(SERVERPROPERTY('ProductMajorVersion') AS INT) AS v`);
|
|
78
|
+
const major = rows[0]?.v != null ? Number(rows[0].v) : 0;
|
|
79
|
+
if (!(major >= SQLSERVER_2025_MAJOR_VERSION)) {
|
|
80
|
+
throw new Error(`Colocated SQL Server vectors are unavailable: the host SQL Server major version is ${major}, ` +
|
|
81
|
+
`but the native VECTOR type requires ${SQLSERVER_2025_MAJOR_VERSION} (SQL Server 2025) or later. ` +
|
|
82
|
+
`Upgrade the server or configure this index to use an external vector database instead.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async EnsureInfrastructure() {
|
|
86
|
+
await this.AssertVectorCapable();
|
|
87
|
+
await this.Run(`IF OBJECT_ID('${this.Schema}.${INDEX_REGISTRY_TABLE}') IS NULL
|
|
88
|
+
CREATE TABLE ${this.Qualify(INDEX_REGISTRY_TABLE)} (
|
|
89
|
+
name NVARCHAR(450) NOT NULL PRIMARY KEY,
|
|
90
|
+
dimension INT NOT NULL,
|
|
91
|
+
metric NVARCHAR(50) NOT NULL CONSTRAINT DF_${INDEX_REGISTRY_TABLE}_metric DEFAULT 'cosine',
|
|
92
|
+
storage_mode NVARCHAR(20) NOT NULL CONSTRAINT DF_${INDEX_REGISTRY_TABLE}_mode DEFAULT 'sibling',
|
|
93
|
+
source_table NVARCHAR(400) NULL,
|
|
94
|
+
vector_column NVARCHAR(200) NOT NULL CONSTRAINT DF_${INDEX_REGISTRY_TABLE}_vcol DEFAULT 'embedding',
|
|
95
|
+
key_column NVARCHAR(200) NOT NULL CONSTRAINT DF_${INDEX_REGISTRY_TABLE}_kcol DEFAULT 'id',
|
|
96
|
+
entity_name NVARCHAR(400) NULL,
|
|
97
|
+
select_columns NVARCHAR(MAX) NULL,
|
|
98
|
+
iterative_threshold INT NULL,
|
|
99
|
+
created_at DATETIMEOFFSET NOT NULL CONSTRAINT DF_${INDEX_REGISTRY_TABLE}_created DEFAULT SYSUTCDATETIME()
|
|
100
|
+
)`);
|
|
101
|
+
}
|
|
102
|
+
// ----------------------------------------------------------------
|
|
103
|
+
// Registry resolution
|
|
104
|
+
// ----------------------------------------------------------------
|
|
105
|
+
async GetResolvedIndex(indexName) {
|
|
106
|
+
const rows = await this.Run(`SELECT dimension, metric, storage_mode, source_table, vector_column, key_column,
|
|
107
|
+
entity_name, select_columns, iterative_threshold
|
|
108
|
+
FROM ${this.Qualify(INDEX_REGISTRY_TABLE)} WHERE name = @p0`, [indexName]);
|
|
109
|
+
if (rows.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const row = rows[0];
|
|
113
|
+
const storageMode = row.storage_mode === 'entityColumn' ? 'entityColumn' : 'sibling';
|
|
114
|
+
const selectColumns = this.ParseSelectColumns(row.select_columns, storageMode);
|
|
115
|
+
const qualifiedTable = row.source_table && row.source_table.trim().length > 0
|
|
116
|
+
? row.source_table
|
|
117
|
+
: this.Qualify(indexName);
|
|
118
|
+
return {
|
|
119
|
+
dimension: Number(row.dimension),
|
|
120
|
+
metric: row.metric,
|
|
121
|
+
storageMode,
|
|
122
|
+
entityName: row.entity_name ?? undefined,
|
|
123
|
+
keyColumn: row.key_column,
|
|
124
|
+
threshold: row.iterative_threshold != null ? Number(row.iterative_threshold) : DEFAULT_ITERATIVE_FILTER_THRESHOLD,
|
|
125
|
+
target: {
|
|
126
|
+
qualifiedTable,
|
|
127
|
+
vectorColumn: row.vector_column,
|
|
128
|
+
keyColumn: row.key_column,
|
|
129
|
+
selectColumns,
|
|
130
|
+
filterMode: storageMode === 'entityColumn' ? 'column' : 'jsonMetadata',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Validate & narrow the untyped `CreateIndexParams.additionalParams` bag into a typed config,
|
|
135
|
+
* throwing on malformed shapes rather than asserting (`as`) a shape that may not hold. */
|
|
136
|
+
NormalizeIndexConfig(raw) {
|
|
137
|
+
const obj = raw && typeof raw === 'object' ? raw : {};
|
|
138
|
+
const cfg = {};
|
|
139
|
+
if (obj['storageMode'] === 'entityColumn' || obj['storageMode'] === 'sibling') {
|
|
140
|
+
cfg.storageMode = obj['storageMode'];
|
|
141
|
+
}
|
|
142
|
+
else if (obj['storageMode'] !== undefined) {
|
|
143
|
+
throw new Error(`Invalid storageMode ${JSON.stringify(obj['storageMode'])} — expected 'sibling' or 'entityColumn'.`);
|
|
144
|
+
}
|
|
145
|
+
if (typeof obj['sourceTable'] === 'string')
|
|
146
|
+
cfg.sourceTable = obj['sourceTable'];
|
|
147
|
+
if (typeof obj['schema'] === 'string')
|
|
148
|
+
cfg.schema = obj['schema'];
|
|
149
|
+
if (typeof obj['vectorColumn'] === 'string')
|
|
150
|
+
cfg.vectorColumn = obj['vectorColumn'];
|
|
151
|
+
if (typeof obj['keyColumn'] === 'string')
|
|
152
|
+
cfg.keyColumn = obj['keyColumn'];
|
|
153
|
+
if (typeof obj['entityName'] === 'string')
|
|
154
|
+
cfg.entityName = obj['entityName'];
|
|
155
|
+
if (Array.isArray(obj['selectColumns'])) {
|
|
156
|
+
cfg.selectColumns = obj['selectColumns'].map(String);
|
|
157
|
+
}
|
|
158
|
+
else if (obj['selectColumns'] !== undefined) {
|
|
159
|
+
throw new Error('selectColumns must be an array of column names.');
|
|
160
|
+
}
|
|
161
|
+
if (typeof obj['iterativeFilterThreshold'] === 'number')
|
|
162
|
+
cfg.iterativeFilterThreshold = obj['iterativeFilterThreshold'];
|
|
163
|
+
if (typeof obj['createVectorIndex'] === 'boolean')
|
|
164
|
+
cfg.createVectorIndex = obj['createVectorIndex'];
|
|
165
|
+
return cfg;
|
|
166
|
+
}
|
|
167
|
+
ParseSelectColumns(raw, storageMode) {
|
|
168
|
+
if (storageMode === 'sibling') {
|
|
169
|
+
return ['metadata'];
|
|
170
|
+
}
|
|
171
|
+
if (!raw) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(raw);
|
|
176
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Resolve the schema-qualified table string for an entityColumn source spec, validating each
|
|
183
|
+
* identifier segment (accepts `[schema].[table]`, `schema.table`, or bare `table`). */
|
|
184
|
+
QualifyEntityTable(cfg) {
|
|
185
|
+
const raw = (cfg.sourceTable ?? '').trim().replace(/[[\]]/g, '');
|
|
186
|
+
const parts = raw.includes('.') ? raw.split('.') : [cfg.schema ?? this.Schema, raw];
|
|
187
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
188
|
+
throw new Error(`Invalid sourceTable ${JSON.stringify(cfg.sourceTable)} — expected "schema.table" or "table".`);
|
|
189
|
+
}
|
|
190
|
+
return `[${ValidateSqlIdentifier(parts[0], 'schema')}].[${ValidateSqlIdentifier(parts[1], 'table')}]`;
|
|
191
|
+
}
|
|
192
|
+
// ----------------------------------------------------------------
|
|
193
|
+
// Index operations
|
|
194
|
+
// ----------------------------------------------------------------
|
|
195
|
+
async ListIndexes() {
|
|
196
|
+
try {
|
|
197
|
+
await this.EnsureInfrastructure();
|
|
198
|
+
const rows = await this.Run(`SELECT name, dimension, metric FROM ${this.Qualify(INDEX_REGISTRY_TABLE)} ORDER BY name`);
|
|
199
|
+
const indexes = rows.map(row => ({
|
|
200
|
+
name: row.name,
|
|
201
|
+
dimension: Number(row.dimension),
|
|
202
|
+
metric: row.metric,
|
|
203
|
+
host: this.Schema,
|
|
204
|
+
}));
|
|
205
|
+
return { indexes };
|
|
206
|
+
}
|
|
207
|
+
catch (ex) {
|
|
208
|
+
LogError('SQLServerVectorDatabase.ListIndexes error', undefined, ex);
|
|
209
|
+
return { indexes: [] };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async GetIndex(params) {
|
|
213
|
+
try {
|
|
214
|
+
await this.EnsureInfrastructure();
|
|
215
|
+
const resolved = await this.GetResolvedIndex(params.id);
|
|
216
|
+
if (!resolved) {
|
|
217
|
+
return this.Failure(`Index "${params.id}" not found`);
|
|
218
|
+
}
|
|
219
|
+
const desc = {
|
|
220
|
+
name: params.id,
|
|
221
|
+
dimension: resolved.dimension,
|
|
222
|
+
metric: resolved.metric,
|
|
223
|
+
host: this.Schema,
|
|
224
|
+
};
|
|
225
|
+
return this.Success(desc);
|
|
226
|
+
}
|
|
227
|
+
catch (ex) {
|
|
228
|
+
LogError('SQLServerVectorDatabase.GetIndex error', undefined, ex);
|
|
229
|
+
return this.Failure('Error getting index');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async CreateIndex(params) {
|
|
233
|
+
try {
|
|
234
|
+
await this.EnsureInfrastructure();
|
|
235
|
+
ValidateSqlIdentifier(params.id, 'index name');
|
|
236
|
+
const cfg = this.NormalizeIndexConfig(params.additionalParams);
|
|
237
|
+
const storageMode = cfg.storageMode === 'entityColumn' ? 'entityColumn' : 'sibling';
|
|
238
|
+
const dimension = params.dimension;
|
|
239
|
+
const metric = params.metric || 'cosine';
|
|
240
|
+
if (storageMode === 'entityColumn') {
|
|
241
|
+
return await this.RegisterEntityColumnIndex(params.id, dimension, metric, cfg);
|
|
242
|
+
}
|
|
243
|
+
return await this.CreateSiblingIndex(params.id, dimension, metric, cfg);
|
|
244
|
+
}
|
|
245
|
+
catch (ex) {
|
|
246
|
+
LogError('SQLServerVectorDatabase.CreateIndex error', undefined, ex);
|
|
247
|
+
return this.Failure(ex instanceof Error ? ex.message : 'Error creating index');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async CreateSiblingIndex(name, dimension, metric, cfg) {
|
|
251
|
+
await this.Run(`IF OBJECT_ID('${this.Schema}.${name}') IS NULL
|
|
252
|
+
CREATE TABLE ${this.Qualify(name)} (
|
|
253
|
+
id NVARCHAR(450) NOT NULL PRIMARY KEY,
|
|
254
|
+
embedding VECTOR(${dimension}) NULL,
|
|
255
|
+
metadata NVARCHAR(MAX) NULL,
|
|
256
|
+
content NVARCHAR(MAX) NULL
|
|
257
|
+
)`);
|
|
258
|
+
if (cfg.createVectorIndex !== false) {
|
|
259
|
+
await this.TryCreateVectorIndex(name, this.Qualify(name), 'embedding', metric);
|
|
260
|
+
}
|
|
261
|
+
await this.UpsertRegistry(name, {
|
|
262
|
+
dimension, metric, storageMode: 'sibling',
|
|
263
|
+
sourceTable: this.Qualify(name), vectorColumn: 'embedding', keyColumn: 'id',
|
|
264
|
+
entityName: null, selectColumns: ['metadata'], threshold: cfg.iterativeFilterThreshold ?? null,
|
|
265
|
+
});
|
|
266
|
+
LogStatus(`SQLServerVectorDatabase: Created sibling index "${name}" (dim=${dimension}, metric=${metric})`);
|
|
267
|
+
return this.Success({ name, dimension, metric, storageMode: 'sibling' });
|
|
268
|
+
}
|
|
269
|
+
async RegisterEntityColumnIndex(name, dimension, metric, cfg) {
|
|
270
|
+
if (!cfg.sourceTable) {
|
|
271
|
+
return this.Failure('entityColumn storage mode requires additionalParams.sourceTable');
|
|
272
|
+
}
|
|
273
|
+
const qualified = this.QualifyEntityTable(cfg);
|
|
274
|
+
const vectorColumn = ValidateSqlIdentifier(cfg.vectorColumn ?? 'Embedding', 'vectorColumn');
|
|
275
|
+
const keyColumn = ValidateSqlIdentifier(cfg.keyColumn ?? 'ID', 'keyColumn');
|
|
276
|
+
if (cfg.createVectorIndex !== false) {
|
|
277
|
+
await this.TryCreateVectorIndex(name, qualified, vectorColumn, metric);
|
|
278
|
+
}
|
|
279
|
+
await this.UpsertRegistry(name, {
|
|
280
|
+
dimension, metric, storageMode: 'entityColumn',
|
|
281
|
+
sourceTable: qualified, vectorColumn, keyColumn,
|
|
282
|
+
entityName: cfg.entityName ?? null, selectColumns: cfg.selectColumns ?? [],
|
|
283
|
+
threshold: cfg.iterativeFilterThreshold ?? null,
|
|
284
|
+
});
|
|
285
|
+
LogStatus(`SQLServerVectorDatabase: Registered entityColumn index "${name}" → ${qualified}.${vectorColumn} (key=${keyColumn})`);
|
|
286
|
+
return this.Success({ name, dimension, metric, storageMode: 'entityColumn', sourceTable: qualified });
|
|
287
|
+
}
|
|
288
|
+
/** Attempt to create a DiskANN vector index; tolerate failure (preview surface / pre-existing). */
|
|
289
|
+
async TryCreateVectorIndex(indexName, qualifiedTable, vectorColumn, metric) {
|
|
290
|
+
try {
|
|
291
|
+
await this.Run(`CREATE VECTOR INDEX [VIX_${indexName}_embedding] ON ${qualifiedTable}([${vectorColumn}])
|
|
292
|
+
WITH (METRIC = '${metric}', TYPE = 'DiskANN')`);
|
|
293
|
+
}
|
|
294
|
+
catch (ex) {
|
|
295
|
+
LogStatus(`SQLServerVectorDatabase: vector index not created for "${indexName}" (continuing): ${ex instanceof Error ? ex.message : String(ex)}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async UpsertRegistry(name, r) {
|
|
299
|
+
await this.Run(`MERGE ${this.Qualify(INDEX_REGISTRY_TABLE)} AS t
|
|
300
|
+
USING (SELECT @p0 AS name) AS s ON t.name = s.name
|
|
301
|
+
WHEN MATCHED THEN UPDATE SET dimension=@p1, metric=@p2, storage_mode=@p3, source_table=@p4,
|
|
302
|
+
vector_column=@p5, key_column=@p6, entity_name=@p7, select_columns=@p8, iterative_threshold=@p9
|
|
303
|
+
WHEN NOT MATCHED THEN INSERT (name, dimension, metric, storage_mode, source_table, vector_column, key_column, entity_name, select_columns, iterative_threshold)
|
|
304
|
+
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9);`, [name, r.dimension, r.metric, r.storageMode, r.sourceTable, r.vectorColumn, r.keyColumn,
|
|
305
|
+
r.entityName, JSON.stringify(r.selectColumns), r.threshold]);
|
|
306
|
+
}
|
|
307
|
+
async DeleteIndex(params) {
|
|
308
|
+
try {
|
|
309
|
+
await this.EnsureInfrastructure();
|
|
310
|
+
const resolved = await this.GetResolvedIndex(params.id);
|
|
311
|
+
// Only drop a table we own (sibling mode). entityColumn tables belong to the app.
|
|
312
|
+
if (resolved?.storageMode === 'sibling') {
|
|
313
|
+
await this.Run(`IF OBJECT_ID('${this.Schema}.${params.id}') IS NOT NULL DROP TABLE ${this.Qualify(params.id)}`);
|
|
314
|
+
}
|
|
315
|
+
await this.Run(`DELETE FROM ${this.Qualify(INDEX_REGISTRY_TABLE)} WHERE name = @p0`, [params.id]);
|
|
316
|
+
return this.Success(null);
|
|
317
|
+
}
|
|
318
|
+
catch (ex) {
|
|
319
|
+
LogError('SQLServerVectorDatabase.DeleteIndex error', undefined, ex);
|
|
320
|
+
return this.Failure('Error deleting index');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async EditIndex(_params) {
|
|
324
|
+
return this.Failure('EditIndex is not currently supported for colocated SQL Server vectors');
|
|
325
|
+
}
|
|
326
|
+
// ----------------------------------------------------------------
|
|
327
|
+
// Query — VECTOR_SEARCH TVF with cardinality-based exact/approximate dispatch
|
|
328
|
+
// ----------------------------------------------------------------
|
|
329
|
+
/**
|
|
330
|
+
* Whether a given host server supports the `VECTOR_SEARCH` DiskANN TVF
|
|
331
|
+
* (`SELECT TOP (N) WITH APPROXIMATE`). Verified live: boxed **SQL Server 2025 RTM does NOT** —
|
|
332
|
+
* only `VECTOR_DISTANCE` (exact) ships there; the approximate/DiskANN surface is currently Azure
|
|
333
|
+
* SQL Database (and likely a later boxed CU). Detected lazily on first query.
|
|
334
|
+
*
|
|
335
|
+
* Keyed **per host**, not process-wide: one MJ process can hold connections to multiple SQL
|
|
336
|
+
* Servers (parallel client-to-many-servers, or mixed 2022/2025 backends), and capability differs
|
|
337
|
+
* per server — a process-global flag would let one server's probe poison another's path.
|
|
338
|
+
*/
|
|
339
|
+
static { this.ApproximateSearchSupportedByHost = new WeakMap(); }
|
|
340
|
+
async ColocatedQuery(params, _contextUser) {
|
|
341
|
+
const resolved = await this.GetResolvedIndex(params.indexName);
|
|
342
|
+
if (!resolved) {
|
|
343
|
+
throw new Error(`SQLServerVectorDatabase: index "${params.indexName}" is not registered`);
|
|
344
|
+
}
|
|
345
|
+
if (!params.vector || params.vector.length === 0) {
|
|
346
|
+
// SQL Server colocated search is vector-only for now; no vector ⇒ nothing to rank.
|
|
347
|
+
return { matches: [] };
|
|
348
|
+
}
|
|
349
|
+
const build = {
|
|
350
|
+
target: resolved.target,
|
|
351
|
+
dimension: resolved.dimension || params.vector.length,
|
|
352
|
+
metric: resolved.metric,
|
|
353
|
+
vector: params.vector,
|
|
354
|
+
topK: SafeTopK(params.topK || 10),
|
|
355
|
+
filter: params.filter,
|
|
356
|
+
};
|
|
357
|
+
const rows = await this.RunVectorQuery(build, resolved, !!params.filter);
|
|
358
|
+
const includeMetadata = params.includeMetadata !== false;
|
|
359
|
+
return { matches: rows.map(row => this.RowToMatch(row, resolved, includeMetadata)) };
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Execute the vector search, choosing the exact vs. approximate path and transparently falling
|
|
363
|
+
* back to exact `VECTOR_DISTANCE` when this server lacks the `VECTOR_SEARCH` TVF.
|
|
364
|
+
*
|
|
365
|
+
* Path selection:
|
|
366
|
+
* - If the approximate surface is known-absent → always exact.
|
|
367
|
+
* - Else for filtered queries, count the filter cardinality and use exact below the threshold
|
|
368
|
+
* (DiskANN doesn't converge when the filter cluster is disjoint from the query neighborhood).
|
|
369
|
+
* - Else attempt approximate; if it errors with a "feature not supported" signature, cache that
|
|
370
|
+
* fact and retry exact.
|
|
371
|
+
*/
|
|
372
|
+
async RunVectorQuery(build, resolved, hasFilter) {
|
|
373
|
+
const cache = SQLServerVectorDatabase_1.ApproximateSearchSupportedByHost;
|
|
374
|
+
let useExact = cache.get(this.Host) === false;
|
|
375
|
+
if (!useExact && hasFilter) {
|
|
376
|
+
const countQ = BuildFilterCountQuery(resolved.target, build.filter);
|
|
377
|
+
const countRows = await this.Run(countQ.sql, countQ.params);
|
|
378
|
+
const n = countRows[0]?.n != null ? Number(countRows[0].n) : Number.MAX_SAFE_INTEGER;
|
|
379
|
+
useExact = n <= resolved.threshold;
|
|
380
|
+
}
|
|
381
|
+
if (useExact) {
|
|
382
|
+
const exact = BuildExactVectorQuery(build);
|
|
383
|
+
return this.Run(exact.sql, exact.params);
|
|
384
|
+
}
|
|
385
|
+
const approx = BuildApproximateVectorQuery(build);
|
|
386
|
+
try {
|
|
387
|
+
const rows = await this.Run(approx.sql, approx.params);
|
|
388
|
+
cache.set(this.Host, true);
|
|
389
|
+
return rows;
|
|
390
|
+
}
|
|
391
|
+
catch (ex) {
|
|
392
|
+
if (!this.IsApproximateUnsupportedError(ex)) {
|
|
393
|
+
throw ex;
|
|
394
|
+
}
|
|
395
|
+
cache.set(this.Host, false);
|
|
396
|
+
LogStatus('SQLServerVectorDatabase: VECTOR_SEARCH/WITH APPROXIMATE is unavailable on this server; falling back to exact VECTOR_DISTANCE search.');
|
|
397
|
+
const exact = BuildExactVectorQuery(build);
|
|
398
|
+
return this.Run(exact.sql, exact.params);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Best-effort heuristic: does this error indicate the VECTOR_SEARCH/DiskANN surface is absent on
|
|
402
|
+
* this server? Matches on message text (brittle across CUs/locales), but only gates a fallback to
|
|
403
|
+
* an equally-correct exact query, so a false negative just rethrows and a false positive is slow,
|
|
404
|
+
* not wrong. */
|
|
405
|
+
IsApproximateUnsupportedError(ex) {
|
|
406
|
+
const msg = ex instanceof Error ? ex.message : String(ex);
|
|
407
|
+
return /APPROXIMATE|VECTOR_SEARCH|Unknown object type 'VECTOR'/i.test(msg);
|
|
408
|
+
}
|
|
409
|
+
/** Map a query result row to a ColocatedMatch, synthesizing metadata per storage mode. */
|
|
410
|
+
RowToMatch(row, resolved, includeMetadata) {
|
|
411
|
+
const distance = Number(row['distance'] ?? 0);
|
|
412
|
+
const score = DistanceToScore(distance, resolved.metric);
|
|
413
|
+
if (resolved.storageMode === 'sibling') {
|
|
414
|
+
const match = { id: String(row['id']), score };
|
|
415
|
+
if (includeMetadata && row['metadata']) {
|
|
416
|
+
match.metadata = this.ParseMetadata(row['metadata']);
|
|
417
|
+
}
|
|
418
|
+
return match;
|
|
419
|
+
}
|
|
420
|
+
// entityColumn: synthesize MJ-shaped metadata (RecordID + Entity + projected fields) so the
|
|
421
|
+
// SearchEngine renders results the same way it does for external/sibling indexes.
|
|
422
|
+
const keyValue = String(row['id']);
|
|
423
|
+
const recordID = `${resolved.keyColumn}|${keyValue}`;
|
|
424
|
+
const match = { id: recordID, score };
|
|
425
|
+
if (includeMetadata) {
|
|
426
|
+
const metadata = {
|
|
427
|
+
RecordID: recordID,
|
|
428
|
+
};
|
|
429
|
+
if (resolved.entityName) {
|
|
430
|
+
metadata['Entity'] = resolved.entityName;
|
|
431
|
+
}
|
|
432
|
+
for (const col of resolved.target.selectColumns) {
|
|
433
|
+
const value = row[col];
|
|
434
|
+
if (value != null) {
|
|
435
|
+
metadata[col] = this.ToMetadataValue(value);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
match.metadata = metadata;
|
|
439
|
+
}
|
|
440
|
+
return match;
|
|
441
|
+
}
|
|
442
|
+
ToMetadataValue(value) {
|
|
443
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
446
|
+
if (Array.isArray(value)) {
|
|
447
|
+
return value.map(String);
|
|
448
|
+
}
|
|
449
|
+
return value instanceof Date ? value.toISOString() : String(value);
|
|
450
|
+
}
|
|
451
|
+
async QueryIndex(params, contextUser) {
|
|
452
|
+
try {
|
|
453
|
+
const vector = 'vector' in params ? params.vector : undefined;
|
|
454
|
+
const indexName = 'id' in params ? params.id : undefined;
|
|
455
|
+
if (!vector) {
|
|
456
|
+
return this.Failure('QueryIndex requires a vector in the params');
|
|
457
|
+
}
|
|
458
|
+
if (!indexName) {
|
|
459
|
+
return this.Failure('QueryIndex requires an index name (params.id)');
|
|
460
|
+
}
|
|
461
|
+
const result = await this.ColocatedQuery({
|
|
462
|
+
indexName,
|
|
463
|
+
vector,
|
|
464
|
+
topK: params.topK || 10,
|
|
465
|
+
filter: params.filter,
|
|
466
|
+
includeMetadata: params.includeMetadata !== false,
|
|
467
|
+
}, contextUser);
|
|
468
|
+
const matches = result.matches.map(m => ({
|
|
469
|
+
id: m.id,
|
|
470
|
+
values: m.values ?? [],
|
|
471
|
+
score: m.score,
|
|
472
|
+
...(m.metadata ? { metadata: m.metadata } : {}),
|
|
473
|
+
}));
|
|
474
|
+
const response = { matches, namespace: this.Schema };
|
|
475
|
+
return this.Success(response);
|
|
476
|
+
}
|
|
477
|
+
catch (ex) {
|
|
478
|
+
LogError('SQLServerVectorDatabase.QueryIndex error', undefined, ex);
|
|
479
|
+
return this.Failure('Error querying index');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// ----------------------------------------------------------------
|
|
483
|
+
// Record write
|
|
484
|
+
// ----------------------------------------------------------------
|
|
485
|
+
async CreateRecord(record, indexName) {
|
|
486
|
+
if (!indexName) {
|
|
487
|
+
return this.Failure('indexName is required for CreateRecord');
|
|
488
|
+
}
|
|
489
|
+
return this.UpsertRecords([record], indexName);
|
|
490
|
+
}
|
|
491
|
+
async CreateRecords(records, indexName) {
|
|
492
|
+
if (!indexName) {
|
|
493
|
+
return this.Failure('indexName is required for CreateRecords');
|
|
494
|
+
}
|
|
495
|
+
return this.UpsertRecords(records, indexName);
|
|
496
|
+
}
|
|
497
|
+
async UpsertRecords(records, indexName) {
|
|
498
|
+
try {
|
|
499
|
+
const resolved = await this.GetResolvedIndex(indexName);
|
|
500
|
+
if (!resolved) {
|
|
501
|
+
return this.Failure(`index "${indexName}" is not registered`);
|
|
502
|
+
}
|
|
503
|
+
for (const record of records) {
|
|
504
|
+
const dim = resolved.dimension || record.values.length;
|
|
505
|
+
if (resolved.storageMode === 'entityColumn') {
|
|
506
|
+
// The entity row already exists and is app-owned; only set its vector column.
|
|
507
|
+
const keyValue = this.ExtractKeyValue(record.id);
|
|
508
|
+
await this.Run(`UPDATE ${resolved.target.qualifiedTable}
|
|
509
|
+
SET [${resolved.target.vectorColumn}] = CAST(@p1 AS VECTOR(${dim}))
|
|
510
|
+
WHERE [${resolved.keyColumn}] = @p0`, [keyValue, VectorJson(record.values)]);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
const content = DeriveContent(record.metadata);
|
|
514
|
+
await this.Run(`MERGE ${this.Qualify(indexName)} AS t
|
|
515
|
+
USING (SELECT @p0 AS id) AS s ON t.id = s.id
|
|
516
|
+
WHEN MATCHED THEN UPDATE SET embedding = CAST(@p1 AS VECTOR(${dim})), metadata = @p2, content = @p3
|
|
517
|
+
WHEN NOT MATCHED THEN INSERT (id, embedding, metadata, content)
|
|
518
|
+
VALUES (@p0, CAST(@p1 AS VECTOR(${dim})), @p2, @p3);`, [record.id, VectorJson(record.values), JSON.stringify(record.metadata ?? {}), content]);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return this.Success({ upsertedCount: records.length });
|
|
522
|
+
}
|
|
523
|
+
catch (ex) {
|
|
524
|
+
LogError('SQLServerVectorDatabase.UpsertRecords error', undefined, ex);
|
|
525
|
+
return this.Failure('Error upserting records');
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/** Extract the scalar key value from a vector record id (CompositeKey URL `Field|Value`). */
|
|
529
|
+
ExtractKeyValue(recordId) {
|
|
530
|
+
const parts = recordId.split('|');
|
|
531
|
+
return parts.length >= 2 ? parts[1] : recordId;
|
|
532
|
+
}
|
|
533
|
+
async UpdateRecord(record) {
|
|
534
|
+
const indexName = record['indexName'];
|
|
535
|
+
if (!indexName) {
|
|
536
|
+
return this.Failure('indexName property is required on the UpdateOptions object');
|
|
537
|
+
}
|
|
538
|
+
if (!record.values) {
|
|
539
|
+
return this.Success(null);
|
|
540
|
+
}
|
|
541
|
+
return this.UpsertRecords([{ id: record.id, values: record.values, metadata: record.metadata }], indexName);
|
|
542
|
+
}
|
|
543
|
+
async UpdateRecords(records) {
|
|
544
|
+
return this.UpdateRecord(records);
|
|
545
|
+
}
|
|
546
|
+
// ----------------------------------------------------------------
|
|
547
|
+
// Record delete
|
|
548
|
+
// ----------------------------------------------------------------
|
|
549
|
+
async DeleteRecord(record, indexName) {
|
|
550
|
+
if (!indexName) {
|
|
551
|
+
return this.Failure('indexName is required for DeleteRecord');
|
|
552
|
+
}
|
|
553
|
+
return this.DeleteByKeys([record.id], indexName);
|
|
554
|
+
}
|
|
555
|
+
async DeleteRecords(records, indexName) {
|
|
556
|
+
if (!indexName) {
|
|
557
|
+
return this.Failure('indexName is required for DeleteRecords');
|
|
558
|
+
}
|
|
559
|
+
return this.DeleteByKeys(records.map(r => r.id), indexName);
|
|
560
|
+
}
|
|
561
|
+
async DeleteByKeys(ids, indexName) {
|
|
562
|
+
try {
|
|
563
|
+
const resolved = await this.GetResolvedIndex(indexName);
|
|
564
|
+
if (!resolved) {
|
|
565
|
+
return this.Failure(`index "${indexName}" is not registered`);
|
|
566
|
+
}
|
|
567
|
+
for (const id of ids) {
|
|
568
|
+
if (resolved.storageMode === 'entityColumn') {
|
|
569
|
+
// Don't delete app-owned rows — null out the vector instead.
|
|
570
|
+
await this.Run(`UPDATE ${resolved.target.qualifiedTable} SET [${resolved.target.vectorColumn}] = NULL WHERE [${resolved.keyColumn}] = @p0`, [this.ExtractKeyValue(id)]);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
await this.Run(`DELETE FROM ${this.Qualify(indexName)} WHERE id = @p0`, [id]);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return this.Success(null);
|
|
577
|
+
}
|
|
578
|
+
catch (ex) {
|
|
579
|
+
LogError('SQLServerVectorDatabase.DeleteByKeys error', undefined, ex);
|
|
580
|
+
return this.Failure('Error deleting records');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async DeleteAllRecords(indexName, _namespace) {
|
|
584
|
+
try {
|
|
585
|
+
const resolved = await this.GetResolvedIndex(indexName);
|
|
586
|
+
if (resolved?.storageMode === 'entityColumn') {
|
|
587
|
+
await this.Run(`UPDATE ${resolved.target.qualifiedTable} SET [${resolved.target.vectorColumn}] = NULL`);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
await this.Run(`DELETE FROM ${this.Qualify(indexName)}`);
|
|
591
|
+
}
|
|
592
|
+
return this.Success(null);
|
|
593
|
+
}
|
|
594
|
+
catch (ex) {
|
|
595
|
+
LogError('SQLServerVectorDatabase.DeleteAllRecords error', undefined, ex);
|
|
596
|
+
return this.Failure('Error deleting all records');
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// ----------------------------------------------------------------
|
|
600
|
+
// Record read
|
|
601
|
+
// ----------------------------------------------------------------
|
|
602
|
+
async GetRecord(params) {
|
|
603
|
+
try {
|
|
604
|
+
const indexName = typeof params.data?.indexName === 'string' ? params.data.indexName : undefined;
|
|
605
|
+
if (!indexName) {
|
|
606
|
+
return this.Failure('params.data.indexName is required');
|
|
607
|
+
}
|
|
608
|
+
const resolved = await this.GetResolvedIndex(indexName);
|
|
609
|
+
if (!resolved) {
|
|
610
|
+
return this.Failure(`index "${indexName}" is not registered`);
|
|
611
|
+
}
|
|
612
|
+
const t = resolved.target;
|
|
613
|
+
const rows = await this.Run(`SELECT c.[${t.keyColumn}] AS id, CAST(c.[${t.vectorColumn}] AS NVARCHAR(MAX)) AS embedding_text
|
|
614
|
+
FROM ${t.qualifiedTable} c WHERE c.[${t.keyColumn}] = @p0`, [this.ExtractKeyValue(params.id)]);
|
|
615
|
+
if (rows.length === 0) {
|
|
616
|
+
return this.Failure(`Record "${params.id}" not found`);
|
|
617
|
+
}
|
|
618
|
+
return this.Success({ id: String(rows[0].id), values: ParseVectorJson(rows[0].embedding_text) });
|
|
619
|
+
}
|
|
620
|
+
catch (ex) {
|
|
621
|
+
LogError('SQLServerVectorDatabase.GetRecord error', undefined, ex);
|
|
622
|
+
return this.Failure('Error getting record');
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async GetRecords(params) {
|
|
626
|
+
try {
|
|
627
|
+
const indexName = typeof params.data?.indexName === 'string' ? params.data.indexName : undefined;
|
|
628
|
+
const ids = Array.isArray(params.data?.ids) ? params.data.ids : undefined;
|
|
629
|
+
if (!indexName) {
|
|
630
|
+
return this.Failure('params.data.indexName is required');
|
|
631
|
+
}
|
|
632
|
+
if (!ids || ids.length === 0) {
|
|
633
|
+
return this.Failure('params.data.ids is required');
|
|
634
|
+
}
|
|
635
|
+
const resolved = await this.GetResolvedIndex(indexName);
|
|
636
|
+
if (!resolved) {
|
|
637
|
+
return this.Failure(`index "${indexName}" is not registered`);
|
|
638
|
+
}
|
|
639
|
+
const t = resolved.target;
|
|
640
|
+
const keyValues = ids.map(id => this.ExtractKeyValue(id));
|
|
641
|
+
const placeholders = keyValues.map((_v, i) => `@p${i}`).join(', ');
|
|
642
|
+
const rows = await this.Run(`SELECT c.[${t.keyColumn}] AS id, CAST(c.[${t.vectorColumn}] AS NVARCHAR(MAX)) AS embedding_text
|
|
643
|
+
FROM ${t.qualifiedTable} c WHERE c.[${t.keyColumn}] IN (${placeholders})`, keyValues);
|
|
644
|
+
return this.Success(rows.map(r => ({ id: String(r.id), values: ParseVectorJson(r.embedding_text) })));
|
|
645
|
+
}
|
|
646
|
+
catch (ex) {
|
|
647
|
+
LogError('SQLServerVectorDatabase.GetRecords error', undefined, ex);
|
|
648
|
+
return this.Failure('Error getting records');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async ListVectorIDs(params) {
|
|
652
|
+
try {
|
|
653
|
+
const resolved = await this.GetResolvedIndex(params.IndexName);
|
|
654
|
+
if (!resolved) {
|
|
655
|
+
return { IDs: [] };
|
|
656
|
+
}
|
|
657
|
+
const t = resolved.target;
|
|
658
|
+
const limit = params.Limit ?? 100;
|
|
659
|
+
const offset = params.PaginationToken ? parseInt(params.PaginationToken, 10) : 0;
|
|
660
|
+
const rows = await this.Run(`SELECT c.[${t.keyColumn}] AS id FROM ${t.qualifiedTable} c
|
|
661
|
+
ORDER BY c.[${t.keyColumn}] OFFSET @p0 ROWS FETCH NEXT @p1 ROWS ONLY`, [offset, limit]);
|
|
662
|
+
const ids = rows.map(r => String(r.id));
|
|
663
|
+
const nextToken = ids.length < limit ? undefined : String(offset + ids.length);
|
|
664
|
+
return { IDs: ids, NextPaginationToken: nextToken };
|
|
665
|
+
}
|
|
666
|
+
catch (ex) {
|
|
667
|
+
LogError('SQLServerVectorDatabase.ListVectorIDs error', undefined, ex);
|
|
668
|
+
return { IDs: [] };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
BuildMetadataFilter(options) {
|
|
672
|
+
const conditions = VectorMetadataFilter.BuildConditions(options);
|
|
673
|
+
return conditions.length === 0 ? undefined : { _pgvectorConditions: conditions };
|
|
674
|
+
}
|
|
675
|
+
// ----------------------------------------------------------------
|
|
676
|
+
// Helpers
|
|
677
|
+
// ----------------------------------------------------------------
|
|
678
|
+
ParseMetadata(raw) {
|
|
679
|
+
if (raw == null) {
|
|
680
|
+
return {};
|
|
681
|
+
}
|
|
682
|
+
if (typeof raw === 'object') {
|
|
683
|
+
return raw;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
return JSON.parse(String(raw));
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return {};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
Success(data) {
|
|
693
|
+
return { success: true, message: '', data };
|
|
694
|
+
}
|
|
695
|
+
Failure(message) {
|
|
696
|
+
return { success: false, message: message || 'An error occurred', data: null };
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
SQLServerVectorDatabase = SQLServerVectorDatabase_1 = __decorate([
|
|
700
|
+
RegisterClass(VectorDBBase, 'SQLServerVectorDatabase'),
|
|
701
|
+
__metadata("design:paramtypes", [String])
|
|
702
|
+
], SQLServerVectorDatabase);
|
|
703
|
+
export { SQLServerVectorDatabase };
|
|
704
|
+
//# sourceMappingURL=SQLServerVectorDatabase.js.map
|