@oracle/langchain-oracledb 0.1.1 → 0.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.
- package/dist/vectorstores.cjs +379 -159
- package/dist/vectorstores.cjs.map +1 -1
- package/dist/vectorstores.d.cts +58 -7
- package/dist/vectorstores.d.ts +58 -7
- package/dist/vectorstores.js +377 -157
- package/dist/vectorstores.js.map +1 -1
- package/package.json +1 -1
package/dist/vectorstores.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import oracledb from "oracledb";
|
|
2
|
-
import { createHash } from "crypto";
|
|
3
3
|
import { VectorStore, } from "@langchain/core/vectorstores";
|
|
4
4
|
import { Document } from "@langchain/core/documents";
|
|
5
5
|
import { maximalMarginalRelevance } from "@langchain/core/utils/math";
|
|
@@ -90,6 +90,17 @@ function jsonCompare(column, op, value, bindValues) {
|
|
|
90
90
|
const operator = op === "=" ? "==" : op; // Oracle requires '==' for equality
|
|
91
91
|
return `JSON_EXISTS(metadata, '$.${column}?(@ ${operator} $${alias})' PASSING :${pos} AS "${alias}")`;
|
|
92
92
|
}
|
|
93
|
+
export const VectorType = {
|
|
94
|
+
DENSE: "DENSE",
|
|
95
|
+
SPARSE: "SPARSE",
|
|
96
|
+
};
|
|
97
|
+
export const VectorElementFormat = {
|
|
98
|
+
INT8: "INT8",
|
|
99
|
+
FLOAT32: "FLOAT32",
|
|
100
|
+
FLOAT64: "FLOAT64",
|
|
101
|
+
BINARY: "BINARY",
|
|
102
|
+
FLEX: "*",
|
|
103
|
+
};
|
|
93
104
|
export const DistanceStrategy = {
|
|
94
105
|
COSINE: "COSINE",
|
|
95
106
|
DOT_PRODUCT: "DOT",
|
|
@@ -135,10 +146,78 @@ function quoteIdentifier(identifier) {
|
|
|
135
146
|
const quotedParts = groups.map((g) => `"${g}"`);
|
|
136
147
|
return quotedParts.join(".");
|
|
137
148
|
}
|
|
138
|
-
|
|
149
|
+
const isFloat32Array = (value) =>
|
|
150
|
+
// eslint-disable-next-line no-instanceof/no-instanceof
|
|
151
|
+
value instanceof Float32Array;
|
|
152
|
+
const isFloat64Array = (value) =>
|
|
153
|
+
// eslint-disable-next-line no-instanceof/no-instanceof
|
|
154
|
+
value instanceof Float64Array;
|
|
155
|
+
const isInt8Array = (value) =>
|
|
156
|
+
// eslint-disable-next-line no-instanceof/no-instanceof
|
|
157
|
+
value instanceof Int8Array;
|
|
158
|
+
const isUint8Array = (value) =>
|
|
159
|
+
// eslint-disable-next-line no-instanceof/no-instanceof
|
|
160
|
+
value instanceof Uint8Array;
|
|
161
|
+
const isSparseVector = (value) =>
|
|
162
|
+
// eslint-disable-next-line no-instanceof/no-instanceof
|
|
163
|
+
value instanceof oracledb.SparseVector;
|
|
164
|
+
function normalizeVectorTypeValue(value) {
|
|
165
|
+
const normalized = value?.toUpperCase();
|
|
166
|
+
if (normalized === VectorType.DENSE || normalized === VectorType.SPARSE) {
|
|
167
|
+
return normalized;
|
|
168
|
+
}
|
|
169
|
+
return VectorType.DENSE;
|
|
170
|
+
}
|
|
171
|
+
function normalizeVectorFormat(value, vectorType) {
|
|
172
|
+
if (!value) {
|
|
173
|
+
return VectorElementFormat.FLOAT32;
|
|
174
|
+
}
|
|
175
|
+
const normalizedValue = value === VectorElementFormat.FLEX
|
|
176
|
+
? VectorElementFormat.FLEX
|
|
177
|
+
: value.toUpperCase();
|
|
178
|
+
const validFormats = Object.values(VectorElementFormat);
|
|
179
|
+
const format = validFormats.includes(normalizedValue)
|
|
180
|
+
? normalizedValue
|
|
181
|
+
: VectorElementFormat.FLOAT32;
|
|
182
|
+
if (vectorType === VectorType.SPARSE && format === VectorElementFormat.BINARY) {
|
|
183
|
+
throw new Error("BINARY format is not supported for SPARSE vectors.");
|
|
184
|
+
}
|
|
185
|
+
return format;
|
|
186
|
+
}
|
|
187
|
+
function buildVectorColumnDefinition(embeddingDim, customization) {
|
|
188
|
+
if (embeddingDim === undefined || embeddingDim === null) {
|
|
189
|
+
throw new Error("Embedding dimension is required to create the vector column.");
|
|
190
|
+
}
|
|
191
|
+
if (!Number.isInteger(embeddingDim) || embeddingDim <= 0) {
|
|
192
|
+
throw new Error("Embedding dimension must be a positive integer.");
|
|
193
|
+
}
|
|
194
|
+
const vectorType = normalizeVectorTypeValue(customization?.vectorType);
|
|
195
|
+
if (vectorType === VectorType.SPARSE && customization?.format === undefined) {
|
|
196
|
+
throw new Error("Sparse vector type requires a vector format to be specified.");
|
|
197
|
+
}
|
|
198
|
+
const format = normalizeVectorFormat(customization?.format, vectorType);
|
|
199
|
+
if (format === VectorElementFormat.BINARY && embeddingDim % 8 !== 0) {
|
|
200
|
+
throw new Error("BINARY vector format requires dimensions to be a multiple of 8.");
|
|
201
|
+
}
|
|
202
|
+
const dimensionSegment = String(embeddingDim);
|
|
203
|
+
const formatSegment = format ?? VectorElementFormat.FLOAT32;
|
|
204
|
+
if (vectorType === VectorType.SPARSE) {
|
|
205
|
+
return `VECTOR(${dimensionSegment}, ${formatSegment}, SPARSE)`;
|
|
206
|
+
}
|
|
207
|
+
return `VECTOR(${dimensionSegment}, ${formatSegment})`;
|
|
208
|
+
}
|
|
209
|
+
function escapeCommentText(comment) {
|
|
210
|
+
return comment.replace(/'/g, "''");
|
|
211
|
+
}
|
|
212
|
+
function escapePlSqlLiteral(sql) {
|
|
213
|
+
return sql.replace(/'/g, "''");
|
|
214
|
+
}
|
|
215
|
+
export async function createTable(connection, tableName, embeddingDim, customization) {
|
|
216
|
+
const tableIdentifier = quoteIdentifier(tableName);
|
|
139
217
|
const colsDict = {
|
|
140
218
|
id: "RAW(16) DEFAULT SYS_GUID() PRIMARY KEY",
|
|
141
|
-
|
|
219
|
+
external_id: `VARCHAR2(36) UNIQUE`,
|
|
220
|
+
embedding: buildVectorColumnDefinition(embeddingDim, customization),
|
|
142
221
|
text: "CLOB",
|
|
143
222
|
metadata: "JSON",
|
|
144
223
|
};
|
|
@@ -146,11 +225,33 @@ export async function createTable(connection, tableName, embeddingDim) {
|
|
|
146
225
|
const ddlBody = Object.entries(colsDict)
|
|
147
226
|
.map(([colName, colType]) => `${colName} ${colType}`)
|
|
148
227
|
.join(", ");
|
|
149
|
-
const
|
|
228
|
+
const createTableSql = `CREATE TABLE IF NOT EXISTS ${tableIdentifier}
|
|
150
229
|
(
|
|
151
230
|
${ddlBody}
|
|
152
231
|
)`;
|
|
153
|
-
|
|
232
|
+
const statements = [
|
|
233
|
+
`EXECUTE IMMEDIATE '${escapePlSqlLiteral(createTableSql)}';`,
|
|
234
|
+
];
|
|
235
|
+
const tableDescription = customization?.description?.trim();
|
|
236
|
+
if (tableDescription) {
|
|
237
|
+
const escapedDescription = escapeCommentText(tableDescription);
|
|
238
|
+
statements.push(`EXECUTE IMMEDIATE 'COMMENT ON TABLE ${tableIdentifier} IS ''${escapedDescription}''';`);
|
|
239
|
+
}
|
|
240
|
+
if (customization?.annotations) {
|
|
241
|
+
for (const [columnName, note] of Object.entries(customization.annotations)) {
|
|
242
|
+
const trimmedNote = note?.trim();
|
|
243
|
+
if (!trimmedNote)
|
|
244
|
+
continue;
|
|
245
|
+
const columnKey = columnName.trim().toLowerCase();
|
|
246
|
+
if (!(columnKey in colsDict))
|
|
247
|
+
continue;
|
|
248
|
+
const normalizedColumnIdentifier = `${tableIdentifier}.${quoteIdentifier(columnKey.toUpperCase())}`;
|
|
249
|
+
const escapedNote = escapeCommentText(trimmedNote);
|
|
250
|
+
statements.push(`EXECUTE IMMEDIATE 'COMMENT ON COLUMN ${normalizedColumnIdentifier} IS ''${escapedNote}''';`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const block = `BEGIN\n ${statements.join("\n ")}\nEND;`;
|
|
254
|
+
await connection.execute(block);
|
|
154
255
|
}
|
|
155
256
|
catch (error) {
|
|
156
257
|
handleError(error);
|
|
@@ -160,8 +261,60 @@ function _getIndexName(baseName) {
|
|
|
160
261
|
const uniqueId = crypto.randomUUID().replace(/-/g, "");
|
|
161
262
|
return `${baseName}_${uniqueId}`;
|
|
162
263
|
}
|
|
264
|
+
// Packs an array of 0/1 numbers into a big-endian Uint8Array for BINARY VECTOR storage.
|
|
265
|
+
function packBinaryVector(bits) {
|
|
266
|
+
const byteLength = Math.ceil(bits.length / 8);
|
|
267
|
+
const buffer = new Uint8Array(byteLength);
|
|
268
|
+
for (let i = 0; i < bits.length; i += 1) {
|
|
269
|
+
const bit = bits[i] > 0 ? 1 : 0;
|
|
270
|
+
if (bit === 1) {
|
|
271
|
+
const byteIndex = Math.floor(i / 8);
|
|
272
|
+
const bitOffset = 7 - (i % 8);
|
|
273
|
+
buffer[byteIndex] |= 1 << bitOffset;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return buffer;
|
|
277
|
+
}
|
|
278
|
+
// Converts a dense numeric vector into the typed array that matches the configured element format.
|
|
279
|
+
function convertDenseVectorForFormat(values, format) {
|
|
280
|
+
switch (format) {
|
|
281
|
+
case VectorElementFormat.FLOAT32:
|
|
282
|
+
case undefined:
|
|
283
|
+
return new Float32Array(values);
|
|
284
|
+
case VectorElementFormat.FLOAT64:
|
|
285
|
+
return new Float64Array(values);
|
|
286
|
+
case VectorElementFormat.INT8: {
|
|
287
|
+
const clamped = new Int8Array(values.length);
|
|
288
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
289
|
+
const rounded = Math.round(values[i]);
|
|
290
|
+
if (rounded < -128 || rounded > 127) {
|
|
291
|
+
throw new Error("INT8 vector values must be within [-128, 127].");
|
|
292
|
+
}
|
|
293
|
+
clamped[i] = rounded;
|
|
294
|
+
}
|
|
295
|
+
return clamped;
|
|
296
|
+
}
|
|
297
|
+
case VectorElementFormat.BINARY:
|
|
298
|
+
return packBinaryVector(values);
|
|
299
|
+
default:
|
|
300
|
+
return new Float64Array(values);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function unpackBinaryVector(bytesLike, dimension) {
|
|
304
|
+
const bytes = isUint8Array(bytesLike)
|
|
305
|
+
? bytesLike
|
|
306
|
+
: Uint8Array.from(bytesLike);
|
|
307
|
+
const result = new Float32Array(dimension);
|
|
308
|
+
for (let i = 0; i < dimension; i += 1) {
|
|
309
|
+
const byteIndex = Math.floor(i / 8);
|
|
310
|
+
const bitOffset = 7 - (i % 8);
|
|
311
|
+
const byte = bytes[byteIndex] ?? 0;
|
|
312
|
+
result[i] = (byte >> bitOffset) & 1;
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
163
316
|
export async function createIndex(client, vectorStore, params) {
|
|
164
|
-
const idxType = params?.idxType
|
|
317
|
+
const idxType = (params?.idxType ?? "HNSW").toUpperCase();
|
|
165
318
|
if (idxType === "IVF") {
|
|
166
319
|
await createIVFIndex(client, vectorStore, params);
|
|
167
320
|
}
|
|
@@ -169,122 +322,48 @@ export async function createIndex(client, vectorStore, params) {
|
|
|
169
322
|
await createHNSWIndex(client, vectorStore, params);
|
|
170
323
|
}
|
|
171
324
|
}
|
|
172
|
-
async function createHNSWIndex(connection, oraclevs, params) {
|
|
325
|
+
async function createHNSWIndex(connection, oraclevs, params = {}) {
|
|
173
326
|
try {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (key === "idxName") {
|
|
191
|
-
config[key] = _getIndexName(defaults[key]);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
config[key] = defaults[key];
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// Validate keys in config against defaults
|
|
199
|
-
for (const key of Object.keys(config)) {
|
|
200
|
-
if (!(key in defaults)) {
|
|
201
|
-
throw new Error(`Invalid parameter: ${key}`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const { idxName } = config;
|
|
205
|
-
const baseSql = `CREATE VECTOR INDEX IF NOT EXISTS ${quoteIdentifier(idxName)}
|
|
206
|
-
ON ${oraclevs.tableName}(embedding)
|
|
207
|
-
ORGANIZATION INMEMORY NEIGHBOR GRAPH`;
|
|
208
|
-
const accuracyPart = config.accuracy
|
|
209
|
-
? ` WITH TARGET ACCURACY ${config.accuracy}`
|
|
210
|
-
: "";
|
|
211
|
-
const distancePart = ` DISTANCE ${oraclevs.distanceStrategy}`;
|
|
212
|
-
let parametersPart = "";
|
|
213
|
-
if ("neighbors" in config && "efConstruction" in config) {
|
|
214
|
-
parametersPart = ` parameters (type ${config.idxType},
|
|
215
|
-
neighbors ${config.neighbors},
|
|
216
|
-
efConstruction ${config.efConstruction})`;
|
|
217
|
-
}
|
|
218
|
-
else if ("neighbors" in config && !("efConstruction" in config)) {
|
|
219
|
-
config.efConstruction = defaults.efConstruction;
|
|
220
|
-
parametersPart = ` parameters (type ${config.idxType},
|
|
221
|
-
neighbors ${config.neighbors},
|
|
222
|
-
efConstruction ${config.efConstruction})`;
|
|
223
|
-
}
|
|
224
|
-
else if (!("neighbors" in config) && "efConstruction" in config) {
|
|
225
|
-
config.neighbors = defaults.neighbors;
|
|
226
|
-
parametersPart = ` parameters (type ${config.idxType},
|
|
227
|
-
neighbors ${config.neighbors},
|
|
228
|
-
efConstruction ${config.efConstruction})`;
|
|
229
|
-
}
|
|
230
|
-
const parallelPart = ` PARALLEL ${config.parallel}`;
|
|
231
|
-
const ddl = baseSql + accuracyPart + distancePart + parametersPart + parallelPart;
|
|
327
|
+
const { idxName = _getIndexName("HNSW"), idxType = "HNSW", neighbors = 32, efConstruction = 200, accuracy = 90, parallel = 8, ...extra } = params;
|
|
328
|
+
const invalidKeys = Object.keys(extra);
|
|
329
|
+
if (invalidKeys.length > 0) {
|
|
330
|
+
throw new Error(`Invalid parameter(s): ${invalidKeys.join(", ")}`);
|
|
331
|
+
}
|
|
332
|
+
const ddl = `
|
|
333
|
+
CREATE VECTOR INDEX IF NOT EXISTS ${quoteIdentifier(idxName)}
|
|
334
|
+
ON ${oraclevs.tableName}(embedding)
|
|
335
|
+
ORGANIZATION INMEMORY NEIGHBOR GRAPH
|
|
336
|
+
WITH TARGET ACCURACY ${accuracy}
|
|
337
|
+
DISTANCE ${oraclevs.distanceStrategy}
|
|
338
|
+
PARAMETERS (type ${idxType}, neighbors ${neighbors}, efConstruction ${efConstruction})
|
|
339
|
+
PARALLEL ${parallel}
|
|
340
|
+
`
|
|
341
|
+
.trim()
|
|
342
|
+
.replace(/\s+/g, " ");
|
|
232
343
|
await connection.execute(ddl);
|
|
233
344
|
}
|
|
234
345
|
catch (error) {
|
|
235
346
|
handleError(error);
|
|
236
347
|
}
|
|
237
348
|
}
|
|
238
|
-
async function createIVFIndex(connection, oraclevs, params) {
|
|
349
|
+
async function createIVFIndex(connection, oraclevs, params = {}) {
|
|
239
350
|
try {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
config[key] = _getIndexName(defaults[key]);
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
config[key] = defaults[key];
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
// Validate keys in config against defaults
|
|
264
|
-
for (const key of Object.keys(config)) {
|
|
265
|
-
if (!(key in defaults)) {
|
|
266
|
-
throw new Error(`Invalid parameter: ${key}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
// Base SQL statement
|
|
270
|
-
const { idxName } = config;
|
|
271
|
-
const baseSql = `CREATE VECTOR INDEX IF NOT EXISTS ${quoteIdentifier(idxName)}
|
|
272
|
-
ON ${oraclevs.tableName}(embedding)
|
|
273
|
-
ORGANIZATION NEIGHBOR PARTITIONS`;
|
|
274
|
-
// Optional parts depending on parameters
|
|
275
|
-
const accuracyPart = config.accuracy
|
|
276
|
-
? ` WITH TARGET ACCURACY ${config.accuracy}`
|
|
277
|
-
: "";
|
|
278
|
-
const distancePart = ` DISTANCE ${oraclevs.distanceStrategy}`;
|
|
279
|
-
let parametersPart = "";
|
|
280
|
-
if ("idxType" in config && "neighborPart" in config) {
|
|
281
|
-
parametersPart = ` PARAMETERS (type ${config.idxType},
|
|
282
|
-
neighbor partitions ${config.neighborPart})`;
|
|
283
|
-
}
|
|
284
|
-
// Always included part for parallel - assuming parallel is compulsory and always included
|
|
285
|
-
const parallelPart = ` PARALLEL ${config.parallel}`;
|
|
286
|
-
// Combine all parts
|
|
287
|
-
const ddl = baseSql + accuracyPart + distancePart + parametersPart + parallelPart;
|
|
351
|
+
const { idxName = _getIndexName("IVF"), idxType = "IVF", neighborPart = 32, accuracy = 90, parallel = 8, ...extra } = params;
|
|
352
|
+
const invalidKeys = Object.keys(extra);
|
|
353
|
+
if (invalidKeys.length > 0) {
|
|
354
|
+
throw new Error(`Invalid parameter(s): ${invalidKeys.join(", ")}`);
|
|
355
|
+
}
|
|
356
|
+
const ddl = `
|
|
357
|
+
CREATE VECTOR INDEX IF NOT EXISTS ${quoteIdentifier(idxName)}
|
|
358
|
+
ON ${oraclevs.tableName}(embedding)
|
|
359
|
+
ORGANIZATION NEIGHBOR PARTITIONS
|
|
360
|
+
WITH TARGET ACCURACY ${accuracy}
|
|
361
|
+
DISTANCE ${oraclevs.distanceStrategy}
|
|
362
|
+
PARAMETERS (type ${idxType}, neighbor partitions ${neighborPart})
|
|
363
|
+
PARALLEL ${parallel}
|
|
364
|
+
`
|
|
365
|
+
.trim()
|
|
366
|
+
.replace(/\s+/g, " ");
|
|
288
367
|
await connection.execute(ddl);
|
|
289
368
|
}
|
|
290
369
|
catch (error) {
|
|
@@ -336,6 +415,30 @@ export class OracleVS extends VectorStore {
|
|
|
336
415
|
writable: true,
|
|
337
416
|
value: void 0
|
|
338
417
|
});
|
|
418
|
+
Object.defineProperty(this, "description", {
|
|
419
|
+
enumerable: true,
|
|
420
|
+
configurable: true,
|
|
421
|
+
writable: true,
|
|
422
|
+
value: void 0
|
|
423
|
+
});
|
|
424
|
+
Object.defineProperty(this, "annotations", {
|
|
425
|
+
enumerable: true,
|
|
426
|
+
configurable: true,
|
|
427
|
+
writable: true,
|
|
428
|
+
value: void 0
|
|
429
|
+
});
|
|
430
|
+
Object.defineProperty(this, "vectorType", {
|
|
431
|
+
enumerable: true,
|
|
432
|
+
configurable: true,
|
|
433
|
+
writable: true,
|
|
434
|
+
value: void 0
|
|
435
|
+
});
|
|
436
|
+
Object.defineProperty(this, "vectorFormat", {
|
|
437
|
+
enumerable: true,
|
|
438
|
+
configurable: true,
|
|
439
|
+
writable: true,
|
|
440
|
+
value: void 0
|
|
441
|
+
});
|
|
339
442
|
Object.defineProperty(this, "query", {
|
|
340
443
|
enumerable: true,
|
|
341
444
|
configurable: true,
|
|
@@ -349,11 +452,120 @@ export class OracleVS extends VectorStore {
|
|
|
349
452
|
dbConfig.distanceStrategy ?? this.distanceStrategy;
|
|
350
453
|
this.query = dbConfig.query;
|
|
351
454
|
this.filter = dbConfig.filter;
|
|
455
|
+
this.description = dbConfig.description;
|
|
456
|
+
this.annotations = dbConfig.annotations
|
|
457
|
+
? { ...dbConfig.annotations }
|
|
458
|
+
: undefined;
|
|
459
|
+
this.vectorType = dbConfig.vectorType;
|
|
460
|
+
this.vectorFormat = dbConfig.format;
|
|
352
461
|
}
|
|
353
462
|
catch (error) {
|
|
354
463
|
handleError(error);
|
|
355
464
|
}
|
|
356
465
|
}
|
|
466
|
+
getActiveVectorType() {
|
|
467
|
+
return this.vectorType ? normalizeVectorTypeValue(this.vectorType) : VectorType.DENSE;
|
|
468
|
+
}
|
|
469
|
+
ensureEmbeddingDimension() {
|
|
470
|
+
if (this.embeddingDimension === undefined || this.embeddingDimension === null) {
|
|
471
|
+
throw new Error("Embedding dimension is not initialized for this vector store.");
|
|
472
|
+
}
|
|
473
|
+
return this.embeddingDimension;
|
|
474
|
+
}
|
|
475
|
+
prepareVectorForStorage(vector) {
|
|
476
|
+
if (this.embeddingDimension === undefined || this.embeddingDimension === null) {
|
|
477
|
+
this.embeddingDimension = vector.length;
|
|
478
|
+
}
|
|
479
|
+
const dimension = this.ensureEmbeddingDimension();
|
|
480
|
+
const vectorType = this.getActiveVectorType();
|
|
481
|
+
const format = this.vectorFormat
|
|
482
|
+
? normalizeVectorFormat(this.vectorFormat, vectorType)
|
|
483
|
+
: VectorElementFormat.FLOAT32;
|
|
484
|
+
if (vectorType === VectorType.SPARSE) {
|
|
485
|
+
if (vector.length !== dimension) {
|
|
486
|
+
throw new Error("Sparse vectors must supply full-dimension arrays for conversion.");
|
|
487
|
+
}
|
|
488
|
+
let sparseInput = vector;
|
|
489
|
+
if (format === VectorElementFormat.INT8) {
|
|
490
|
+
const clamped = new Int8Array(vector.length);
|
|
491
|
+
for (let i = 0; i < vector.length; i += 1) {
|
|
492
|
+
const rounded = Math.round(vector[i]);
|
|
493
|
+
if (rounded < -128 || rounded > 127) {
|
|
494
|
+
throw new Error("INT8 sparse vector values must be within [-128, 127].");
|
|
495
|
+
}
|
|
496
|
+
clamped[i] = rounded;
|
|
497
|
+
}
|
|
498
|
+
sparseInput = clamped;
|
|
499
|
+
}
|
|
500
|
+
else if (format === VectorElementFormat.FLOAT64) {
|
|
501
|
+
sparseInput = new Float64Array(vector);
|
|
502
|
+
}
|
|
503
|
+
else if (format === VectorElementFormat.FLOAT32) {
|
|
504
|
+
sparseInput = new Float32Array(vector);
|
|
505
|
+
}
|
|
506
|
+
return new oracledb.SparseVector(sparseInput);
|
|
507
|
+
}
|
|
508
|
+
if (vector.length !== dimension) {
|
|
509
|
+
throw new Error("Vector length does not match the embedding dimension.");
|
|
510
|
+
}
|
|
511
|
+
return convertDenseVectorForFormat(vector, format);
|
|
512
|
+
}
|
|
513
|
+
prepareQueryVector(vector) {
|
|
514
|
+
return this.prepareVectorForStorage(vector);
|
|
515
|
+
}
|
|
516
|
+
// Preserves the native driver representation, only expanding sparse vectors to dense form.
|
|
517
|
+
// Preserves the native driver representation, only expanding sparse vectors to dense form.
|
|
518
|
+
normalizeReturnedEmbedding(value) {
|
|
519
|
+
if (isSparseVector(value)) {
|
|
520
|
+
const dense = value.dense?.();
|
|
521
|
+
if (!dense) {
|
|
522
|
+
throw new Error("Unable to expand sparse vector to dense representation.");
|
|
523
|
+
}
|
|
524
|
+
if (isFloat32Array(dense)) {
|
|
525
|
+
return dense;
|
|
526
|
+
}
|
|
527
|
+
if (isFloat64Array(dense)) {
|
|
528
|
+
return new Float32Array(dense);
|
|
529
|
+
}
|
|
530
|
+
if (isInt8Array(dense)) {
|
|
531
|
+
return Float32Array.from(dense);
|
|
532
|
+
}
|
|
533
|
+
if (Array.isArray(dense)) {
|
|
534
|
+
return Float32Array.from(dense);
|
|
535
|
+
}
|
|
536
|
+
return Float32Array.from(dense);
|
|
537
|
+
}
|
|
538
|
+
if (isFloat32Array(value) || isFloat64Array(value) || isInt8Array(value) || isUint8Array(value)) {
|
|
539
|
+
return value;
|
|
540
|
+
}
|
|
541
|
+
if (Array.isArray(value)) {
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
throw new Error("Received unsupported vector representation from the database.");
|
|
545
|
+
}
|
|
546
|
+
// Normalizes any supported vector representation into Float32Array so downstream math
|
|
547
|
+
// (MMR, distance calculations) can assume a consistent element type without forcing
|
|
548
|
+
// callers to give up the native shape returned by node-oracledb.
|
|
549
|
+
coerceEmbeddingToFloat32(value) {
|
|
550
|
+
if (isFloat32Array(value)) {
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
if (isFloat64Array(value)) {
|
|
554
|
+
return new Float32Array(value);
|
|
555
|
+
}
|
|
556
|
+
if (isInt8Array(value)) {
|
|
557
|
+
return Float32Array.from(value);
|
|
558
|
+
}
|
|
559
|
+
if (isUint8Array(value)) {
|
|
560
|
+
const byteLength = value.length;
|
|
561
|
+
const dimension = this.embeddingDimension ?? byteLength * 8;
|
|
562
|
+
return unpackBinaryVector(value, dimension);
|
|
563
|
+
}
|
|
564
|
+
if (Array.isArray(value)) {
|
|
565
|
+
return Float32Array.from(value);
|
|
566
|
+
}
|
|
567
|
+
return this.coerceEmbeddingToFloat32(this.normalizeReturnedEmbedding(value));
|
|
568
|
+
}
|
|
357
569
|
async getEmbeddingDimension(query) {
|
|
358
570
|
const embeddingVector = await this.embeddings.embedQuery(query);
|
|
359
571
|
return embeddingVector.length;
|
|
@@ -363,7 +575,12 @@ export class OracleVS extends VectorStore {
|
|
|
363
575
|
try {
|
|
364
576
|
this.embeddingDimension = await this.getEmbeddingDimension(this.query);
|
|
365
577
|
connection = await this.getConnection();
|
|
366
|
-
await createTable(connection, this.tableName, this.embeddingDimension
|
|
578
|
+
await createTable(connection, this.tableName, this.embeddingDimension, {
|
|
579
|
+
description: this.description,
|
|
580
|
+
annotations: this.annotations,
|
|
581
|
+
vectorType: this.vectorType,
|
|
582
|
+
format: this.vectorFormat
|
|
583
|
+
});
|
|
367
584
|
}
|
|
368
585
|
catch (error) {
|
|
369
586
|
handleError(error);
|
|
@@ -402,73 +619,79 @@ export class OracleVS extends VectorStore {
|
|
|
402
619
|
* @param vectors The vectors to add.
|
|
403
620
|
* @param documents The documents associated with the vectors.
|
|
404
621
|
* @param options
|
|
622
|
+
* ** Add { mutateOnDuplicate?: boolean } to do upsert
|
|
405
623
|
* @returns Promise that resolves when the vectors have been added.
|
|
406
624
|
*/
|
|
407
625
|
async addVectors(vectors, documents, options) {
|
|
408
626
|
if (vectors.length === 0) {
|
|
409
627
|
throw new Error("Vectors input null. Nothing to add...");
|
|
410
628
|
}
|
|
411
|
-
const
|
|
629
|
+
const inputIds = options?.ids;
|
|
412
630
|
let connection = null;
|
|
413
631
|
try {
|
|
414
632
|
// Ensure there are IDs for all documents
|
|
415
|
-
if (
|
|
633
|
+
if (inputIds !== undefined && inputIds.length !== vectors.length) {
|
|
416
634
|
throw new Error("The number of ids must match the number of vectors provided.");
|
|
417
635
|
}
|
|
418
636
|
connection = await this.getConnection();
|
|
637
|
+
const finalIds = [];
|
|
419
638
|
const binds = [];
|
|
420
639
|
for (let index = 0; index < documents.length; index += 1) {
|
|
421
640
|
const doc = documents[index];
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
.toUpperCase();
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
const sourceId = doc.metadata.id ?? crypto.randomUUID();
|
|
432
|
-
processedId = createHash("sha256")
|
|
433
|
-
.update(sourceId)
|
|
434
|
-
.digest("hex")
|
|
435
|
-
.substring(0, 16)
|
|
436
|
-
.toUpperCase();
|
|
437
|
-
}
|
|
438
|
-
const idBuffer = Buffer.from(processedId, "hex");
|
|
439
|
-
const bind = {
|
|
440
|
-
id: idBuffer,
|
|
641
|
+
// Generate ID if not provided: Priority (Options -> Metadata -> Random)
|
|
642
|
+
const externalId = inputIds?.[index] ?? doc.metadata.id ?? crypto.randomUUID();
|
|
643
|
+
finalIds.push(externalId);
|
|
644
|
+
const preparedEmbedding = this.prepareVectorForStorage(vectors[index]);
|
|
645
|
+
const row = {
|
|
646
|
+
ext_id: externalId,
|
|
441
647
|
text: doc.pageContent,
|
|
442
648
|
metadata: doc.metadata,
|
|
443
|
-
embedding:
|
|
649
|
+
embedding: preparedEmbedding,
|
|
444
650
|
};
|
|
445
|
-
binds.push(
|
|
651
|
+
binds.push(row);
|
|
446
652
|
}
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
653
|
+
const isMutateOnDuplicate = options?.mutateOnDuplicate ?? false; // Default to false for better performance
|
|
654
|
+
const insertSql = `
|
|
655
|
+
INSERT INTO ${this.tableName} (external_id, embedding, text, metadata)
|
|
656
|
+
VALUES (:ext_id, :embedding, :text, :metadata)`;
|
|
657
|
+
const mergeSql = `
|
|
658
|
+
MERGE INTO ${this.tableName} t
|
|
659
|
+
USING (
|
|
660
|
+
SELECT :ext_id as external_id, :embedding as embedding,
|
|
661
|
+
:metadata as metadata, :text as text FROM DUAL
|
|
662
|
+
) s
|
|
663
|
+
ON (t.external_id = s.external_id)
|
|
664
|
+
WHEN MATCHED THEN
|
|
665
|
+
UPDATE SET
|
|
666
|
+
t.embedding = s.embedding,
|
|
667
|
+
t.metadata = s.metadata,
|
|
668
|
+
t.text = s.text
|
|
669
|
+
WHEN NOT MATCHED THEN
|
|
670
|
+
INSERT (external_id, embedding, metadata, text)
|
|
671
|
+
VALUES (s.external_id, s.embedding, s.metadata, s.text)`;
|
|
672
|
+
const sql = isMutateOnDuplicate ? mergeSql : insertSql;
|
|
673
|
+
const executeOptions = {
|
|
450
674
|
bindDefs: {
|
|
451
|
-
|
|
675
|
+
ext_id: { type: oracledb.STRING, maxSize: 255 },
|
|
452
676
|
text: { type: oracledb.STRING, maxSize: 10000000 },
|
|
453
677
|
metadata: { type: oracledb.DB_TYPE_JSON },
|
|
454
678
|
embedding: { type: oracledb.DB_TYPE_VECTOR },
|
|
455
679
|
},
|
|
456
680
|
autoCommit: false,
|
|
457
681
|
};
|
|
458
|
-
await connection.executeMany(sql, binds,
|
|
682
|
+
await connection.executeMany(sql, binds, executeOptions);
|
|
459
683
|
// Commit once all inserts are queued up
|
|
460
684
|
await connection.commit();
|
|
461
|
-
|
|
685
|
+
return finalIds;
|
|
462
686
|
}
|
|
463
687
|
catch (error) {
|
|
464
|
-
handleError(error);
|
|
688
|
+
return handleError(error);
|
|
465
689
|
}
|
|
466
690
|
finally {
|
|
467
691
|
if (connection) {
|
|
468
692
|
await this.retConnection(connection);
|
|
469
693
|
}
|
|
470
694
|
}
|
|
471
|
-
return ids;
|
|
472
695
|
}
|
|
473
696
|
async addDocuments(documents, options) {
|
|
474
697
|
const texts = documents.map(({ pageContent }) => pageContent);
|
|
@@ -485,10 +708,9 @@ export class OracleVS extends VectorStore {
|
|
|
485
708
|
const docsScoresAndEmbeddings = [];
|
|
486
709
|
let connection = null;
|
|
487
710
|
try {
|
|
488
|
-
const
|
|
489
|
-
const bindValues = [convertedEmbedding];
|
|
711
|
+
const bindValues = [this.prepareQueryVector(query)];
|
|
490
712
|
let sqlQuery = `
|
|
491
|
-
SELECT
|
|
713
|
+
SELECT external_id,
|
|
492
714
|
text,
|
|
493
715
|
metadata,
|
|
494
716
|
vector_distance(embedding, :1, ${this.distanceStrategy}) as distance,
|
|
@@ -513,11 +735,11 @@ export class OracleVS extends VectorStore {
|
|
|
513
735
|
const text = row[1];
|
|
514
736
|
const metadata = row[2];
|
|
515
737
|
const distance = row[3];
|
|
516
|
-
const embedding = row[4];
|
|
738
|
+
const embedding = this.normalizeReturnedEmbedding(row[4]);
|
|
517
739
|
const document = new Document({
|
|
518
740
|
pageContent: text || "",
|
|
519
741
|
metadata: metadata || {},
|
|
520
|
-
id: row[0]
|
|
742
|
+
id: row[0],
|
|
521
743
|
});
|
|
522
744
|
docsScoresAndEmbeddings.push([document, distance, embedding]);
|
|
523
745
|
}
|
|
@@ -576,10 +798,8 @@ export class OracleVS extends VectorStore {
|
|
|
576
798
|
// Split documents, scores, and embeddings
|
|
577
799
|
const documents = docsScoresEmbeddings.map(([document]) => document);
|
|
578
800
|
const scores = docsScoresEmbeddings.map(([, score]) => score);
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
const consistentEmbeddings = embeddings.map((embedding) => Array.from(embedding));
|
|
582
|
-
const queryEmbedding = Array.from(embedding);
|
|
801
|
+
const consistentEmbeddings = docsScoresEmbeddings.map(([, , emb]) => Array.from(this.coerceEmbeddingToFloat32(emb)));
|
|
802
|
+
const queryEmbedding = Array.from(this.coerceEmbeddingToFloat32(embedding));
|
|
583
803
|
// Ensure lambdaMult has a default value if not provided
|
|
584
804
|
const lambdaMult = options.lambda ?? 0.5;
|
|
585
805
|
const mmrSelectedIndices = maximalMarginalRelevance(queryEmbedding, consistentEmbeddings, lambdaMult, options.k);
|
|
@@ -616,7 +836,7 @@ export class OracleVS extends VectorStore {
|
|
|
616
836
|
await connection.close();
|
|
617
837
|
}
|
|
618
838
|
}
|
|
619
|
-
static async fromDocuments(documents, embeddings, dbConfig) {
|
|
839
|
+
static async fromDocuments(documents, embeddings, dbConfig, options) {
|
|
620
840
|
const { client } = dbConfig;
|
|
621
841
|
if (!client)
|
|
622
842
|
throw new Error("client parameter is required...");
|
|
@@ -626,7 +846,7 @@ export class OracleVS extends VectorStore {
|
|
|
626
846
|
const texts = documents.map(({ pageContent }) => pageContent);
|
|
627
847
|
const vectors = await embeddings.embedDocuments(texts);
|
|
628
848
|
// Assuming a method exists to handle adding texts and metadata appropriately
|
|
629
|
-
await vss.addVectors(vectors, documents);
|
|
849
|
+
await vss.addVectors(vectors, documents, options);
|
|
630
850
|
return vss;
|
|
631
851
|
}
|
|
632
852
|
catch (error) {
|