@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.
@@ -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
- export async function createTable(connection, tableName, embeddingDim) {
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
- embedding: `vector(${embeddingDim}, FLOAT32)`,
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 ddl = `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(tableName)}
228
+ const createTableSql = `CREATE TABLE IF NOT EXISTS ${tableIdentifier}
150
229
  (
151
230
  ${ddlBody}
152
231
  )`;
153
- await connection.execute(ddl);
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 || "HNSW";
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 defaults = {
175
- idxName: "HNSW",
176
- idxType: "HNSW",
177
- neighbors: 32,
178
- efConstruction: 200,
179
- accuracy: 90,
180
- parallel: 8,
181
- };
182
- // if params then copy params to config
183
- const config = params
184
- ? { ...params }
185
- : { ...defaults };
186
- // Ensure compulsory parts are included
187
- const compulsoryKeys = ["idxName", "parallel"];
188
- for (const key of compulsoryKeys) {
189
- if (!(key in config)) {
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 defaults = {
241
- idxName: "IVF",
242
- idxType: "IVF",
243
- neighborPart: 32,
244
- accuracy: 90,
245
- parallel: 8,
246
- };
247
- // Combine defaults with any provided params. Note: params could contain keys not explicitly declared in IndexConfig
248
- const config = params
249
- ? { ...params }
250
- : { ...defaults };
251
- // Ensure compulsory parts are included
252
- const compulsoryKeys = ["idxName", "parallel"];
253
- for (const key of compulsoryKeys) {
254
- if (!(key in config)) {
255
- if (key === "idxName") {
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 ids = options?.ids;
629
+ const inputIds = options?.ids;
412
630
  let connection = null;
413
631
  try {
414
632
  // Ensure there are IDs for all documents
415
- if (ids !== undefined && ids.length !== vectors.length) {
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
- let processedId;
423
- if (ids) {
424
- processedId = createHash("sha256")
425
- .update(ids[index])
426
- .digest("hex")
427
- .substring(0, 16)
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: new Float32Array(vectors[index]),
649
+ embedding: preparedEmbedding,
444
650
  };
445
- binds.push(bind);
651
+ binds.push(row);
446
652
  }
447
- const sql = `INSERT INTO ${this.tableName} (id, embedding, text, metadata )
448
- VALUES (:id, :embedding, :text, :metadata)`;
449
- const options = {
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
- id: { type: oracledb.BUFFER, maxSize: 24 },
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, options);
682
+ await connection.executeMany(sql, binds, executeOptions);
459
683
  // Commit once all inserts are queued up
460
684
  await connection.commit();
461
- console.log("All documents have been inserted and committed.");
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 convertedEmbedding = new Float32Array(query);
489
- const bindValues = [convertedEmbedding];
711
+ const bindValues = [this.prepareQueryVector(query)];
490
712
  let sqlQuery = `
491
- SELECT id,
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].toString("hex"),
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 embeddings = docsScoresEmbeddings.map(([, , embedding]) => new Float32Array(embedding));
580
- // Convert all embeddings to Float32Array for consistency
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) {