@dudousxd/nestjs-agent-rag 0.10.3 → 0.12.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/index.js CHANGED
@@ -356,6 +356,181 @@ function cosineSimilarity(a, b) {
356
356
  }
357
357
  __name(cosineSimilarity, "cosineSimilarity");
358
358
 
359
+ // src/lexical-query.ts
360
+ var words = /* @__PURE__ */ __name((s) => new Set(s.split(/\s+/).filter(Boolean)), "words");
361
+ var DEFAULT_STOP_WORDS = {
362
+ english: words(`
363
+ me my myself we our ours ourselves you your yours yourself yourselves he him his himself she
364
+ her hers herself it its itself they them their theirs themselves what which who whom this that
365
+ these those am is are was were be been being have has had having do does did doing an the and
366
+ but if or because as until while of at by for with about against between into through during
367
+ before after above below to from up down in out on off over under again further then once here
368
+ there when where why how all any both each few more most other some such no nor not only own
369
+ same so than too very can will just don should now`),
370
+ portuguese: words(`
371
+ de que do da em um para com n\xE3o uma os no se na por mais as dos como mas ao ele das seu sua ou
372
+ quando muito nos j\xE1 eu tamb\xE9m s\xF3 pelo pela at\xE9 isso ela entre depois sem mesmo aos seus quem
373
+ nas me esse eles voc\xEA essa num nem suas meu \xE0s minha numa pelos elas qual n\xF3s lhe deles essas
374
+ esses pelas este dele tu te voc\xEAs vos lhes meus minhas teu tua teus tuas nosso nossa nossos
375
+ nossas dela delas esta estes estas aquele aquela aqueles aquelas isto aquilo estou est\xE1 estamos
376
+ est\xE3o estive esteve estivemos estiveram estava est\xE1vamos estavam estivera estiv\xE9ramos esteja
377
+ estejamos estejam estivesse estiv\xE9ssemos estivessem estiver estivermos estiverem hei h\xE1 havemos
378
+ h\xE3o houve houvemos houveram houvera houv\xE9ramos haja hajamos hajam houvesse houv\xE9ssemos
379
+ houvessem houver houvermos houverem houverei houver\xE1 houveremos houver\xE3o houveria houver\xEDamos
380
+ houveriam sou somos s\xE3o era \xE9ramos eram fui foi fomos foram fora f\xF4ramos seja sejamos sejam
381
+ fosse f\xF4ssemos fossem for formos forem serei ser\xE1 seremos ser\xE3o seria ser\xEDamos seriam tenho tem
382
+ temos t\xE9m tinha t\xEDnhamos tinham tive teve tivemos tiveram tivera tiv\xE9ramos tenha tenhamos
383
+ tenham tivesse tiv\xE9ssemos tivessem tiver tivermos tiverem terei ter\xE1 teremos ter\xE3o teria
384
+ ter\xEDamos teriam`),
385
+ spanish: words(`
386
+ de la que el en los del se las por un para con no una su al lo como m\xE1s pero sus le ya este s\xED
387
+ porque esta entre cuando muy sin sobre tambi\xE9n me hasta hay donde quien desde todo nos durante
388
+ todos uno les ni contra otros ese eso ante ellos esto m\xED antes algunos qu\xE9 unos yo otro otras
389
+ otra \xE9l tanto esa estos mucho quienes nada muchos cual poco ella estar estas algunas algo
390
+ nosotros mi mis t\xFA te ti tu tus ellas nosotras vosotros vosotras os m\xEDo m\xEDa m\xEDos m\xEDas tuyo
391
+ tuya tuyos tuyas suyo suya suyos suyas nuestro nuestra nuestros nuestras vuestro vuestra
392
+ vuestros vuestras esos esas estoy est\xE1s est\xE1 estamos est\xE1is est\xE1n est\xE9 est\xE9s estemos est\xE9is
393
+ est\xE9n estar\xE9 estar\xE1s estar\xE1 estaremos estar\xE9is estar\xE1n estar\xEDa estar\xEDas estar\xEDamos estar\xEDais
394
+ estar\xEDan estaba estabas est\xE1bamos estabais estaban estuve estuviste estuvo estuvimos
395
+ estuvisteis estuvieron estuviera estuvieras estuvi\xE9ramos estuvierais estuvieran estuviese
396
+ estuvieses estuvi\xE9semos estuvieseis estuviesen estando estad he has ha hemos hab\xE9is han haya
397
+ hayas hayamos hay\xE1is hayan habr\xE9 habr\xE1s habr\xE1 habremos habr\xE9is habr\xE1n habr\xEDa habr\xEDas habr\xEDamos
398
+ habr\xEDais habr\xEDan hab\xEDa hab\xEDas hab\xEDamos hab\xEDais hab\xEDan hube hubiste hubo hubimos hubisteis
399
+ hubieron hubiera hubieras hubi\xE9ramos hubierais hubieran hubiese hubieses hubi\xE9semos hubieseis
400
+ hubiesen habiendo habido habida habidos habidas soy eres es somos sois son sea seas seamos
401
+ se\xE1is sean ser\xE9 ser\xE1s ser\xE1 seremos ser\xE9is ser\xE1n ser\xEDa ser\xEDas ser\xEDamos ser\xEDais ser\xEDan era eras
402
+ \xE9ramos erais eran fui fuiste fue fuimos fuisteis fueron fuera fueras fu\xE9ramos fuerais fueran
403
+ fuese fueses fu\xE9semos fueseis fuesen tengo tienes tiene tenemos ten\xE9is tienen tenga tengas
404
+ tengamos teng\xE1is tengan tendr\xE9 tendr\xE1s tendr\xE1 tendremos tendr\xE9is tendr\xE1n tendr\xEDa tendr\xEDas
405
+ tendr\xEDamos tendr\xEDais tendr\xEDan ten\xEDa ten\xEDas ten\xEDamos ten\xEDais ten\xEDan tuve tuviste tuvo tuvimos
406
+ tuvisteis tuvieron tuviera tuvieras tuvi\xE9ramos tuvierais tuvieran tuviese tuvieses tuvi\xE9semos
407
+ tuvieseis tuviesen teniendo tenido tenida tenidos tenidas tened`)
408
+ };
409
+ var TOKEN = /[\p{L}\p{N}]{2,}/gu;
410
+ function keywordTerms(query, max = 24, stopWords = DEFAULT_STOP_WORDS) {
411
+ const tokens = [
412
+ ...new Set(query.toLowerCase().normalize("NFC").match(TOKEN) ?? [])
413
+ ];
414
+ const counts = Object.entries(stopWords).map(([, stop2]) => [
415
+ stop2,
416
+ tokens.filter((t) => stop2.has(t)).length
417
+ ]);
418
+ const best = Math.max(0, ...counts.map(([, n]) => n));
419
+ if (best === 0) return tokens.slice(0, max);
420
+ const stop = counts.filter(([, n]) => n === best).map(([s]) => s);
421
+ const terms = tokens.filter((t) => !stop.some((s) => s.has(t)));
422
+ return (terms.length > 0 ? terms : tokens).slice(0, max);
423
+ }
424
+ __name(keywordTerms, "keywordTerms");
425
+ function hasSearchSyntax(query) {
426
+ return /"[^"]+"/.test(query) || /(^|\s)-[\p{L}\p{N}]/u.test(query);
427
+ }
428
+ __name(hasSearchSyntax, "hasSearchSyntax");
429
+ function anyTermTsquery(terms) {
430
+ return terms.map((t) => `'${t.replace(/'/g, "")}'`).join(" | ");
431
+ }
432
+ __name(anyTermTsquery, "anyTermTsquery");
433
+
434
+ // src/pg-client.ts
435
+ var ADAPTED = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-rag/pg-client");
436
+ function toPgClient(source) {
437
+ if (source[ADAPTED]) {
438
+ return source;
439
+ }
440
+ if (isDrizzle(source)) {
441
+ return toPgClient(source.$client);
442
+ }
443
+ if (typeof source.unsafe === "function") {
444
+ return fromPostgresJs(source);
445
+ }
446
+ return fromQueryable(source);
447
+ }
448
+ __name(toPgClient, "toPgClient");
449
+ function isDrizzle(source) {
450
+ const client = source.$client;
451
+ return client !== null && client !== void 0 && typeof client === "object";
452
+ }
453
+ __name(isDrizzle, "isDrizzle");
454
+ function mark(client) {
455
+ Object.defineProperty(client, ADAPTED, {
456
+ value: true
457
+ });
458
+ return client;
459
+ }
460
+ __name(mark, "mark");
461
+ function rowsOf(result) {
462
+ if (Array.isArray(result)) {
463
+ return result;
464
+ }
465
+ const rows = result?.rows;
466
+ return Array.isArray(rows) ? rows : [];
467
+ }
468
+ __name(rowsOf, "rowsOf");
469
+ function fromPostgresJs(sql) {
470
+ const client = {
471
+ query: /* @__PURE__ */ __name(async (text, params = []) => (
472
+ // postgres.js resolves a RowList (an Array subclass); copy it to a plain array.
473
+ [
474
+ ...rowsOf(await sql.unsafe(text, params))
475
+ ]
476
+ ), "query")
477
+ };
478
+ if (typeof sql.begin === "function") {
479
+ const begin = sql.begin.bind(sql);
480
+ client.transaction = (work) => Promise.resolve(begin((tx) => work(fromPostgresJs(tx))));
481
+ }
482
+ return mark(client);
483
+ }
484
+ __name(fromPostgresJs, "fromPostgresJs");
485
+ function fromQueryable(source) {
486
+ const client = {
487
+ query: /* @__PURE__ */ __name(async (text, params) => rowsOf(await source.query(text, params)), "query")
488
+ };
489
+ if (typeof source.transaction === "function") {
490
+ const transaction = source.transaction.bind(source);
491
+ client.transaction = (work) => transaction((tx) => work(toPgClient(tx)));
492
+ } else if (isNodePgPool(source)) {
493
+ client.transaction = async (work) => {
494
+ const connection = await source.connect();
495
+ let broken = false;
496
+ try {
497
+ return await runInTransaction(connection, work, () => {
498
+ broken = true;
499
+ });
500
+ } finally {
501
+ connection.release(broken ? new Error("ROLLBACK failed") : void 0);
502
+ }
503
+ };
504
+ } else if (isNodePgClient(source)) {
505
+ client.transaction = (work) => runInTransaction(source, work);
506
+ }
507
+ return mark(client);
508
+ }
509
+ __name(fromQueryable, "fromQueryable");
510
+ async function runInTransaction(connection, work, onRollbackFailed) {
511
+ const tx = mark({
512
+ query: /* @__PURE__ */ __name(async (text, params) => rowsOf(await connection.query(text, params)), "query")
513
+ });
514
+ await connection.query("BEGIN");
515
+ try {
516
+ const result = await work(tx);
517
+ await connection.query("COMMIT");
518
+ return result;
519
+ } catch (error) {
520
+ await connection.query("ROLLBACK").catch(() => onRollbackFailed?.());
521
+ throw error;
522
+ }
523
+ }
524
+ __name(runInTransaction, "runInTransaction");
525
+ function isNodePgPool(source) {
526
+ return typeof source.connect === "function" && typeof source.totalCount === "number";
527
+ }
528
+ __name(isNodePgPool, "isNodePgPool");
529
+ function isNodePgClient(source) {
530
+ return typeof source.release === "function" || typeof source.processID === "number" || source.connectionParameters !== void 0;
531
+ }
532
+ __name(isNodePgClient, "isNodePgClient");
533
+
359
534
  // src/pg-vector-store.ts
360
535
  var NUL_BYTE = String.fromCharCode(0);
361
536
  function stripNulBytes(value) {
@@ -384,11 +559,37 @@ var PgVectorStore = class {
384
559
  }
385
560
  client;
386
561
  table;
562
+ /** The single width (fixed-dimension table), or `undefined` for a mixed-dimension table. */
387
563
  dimensions;
564
+ /** The widths with a partial HNSW index, for a mixed-dimension table. */
565
+ indexedDimensions;
566
+ nullableEmbeddings;
567
+ iterativeScan;
568
+ efSearch;
569
+ upsertBatchSize;
570
+ iterativeScanSupport;
388
571
  constructor(client, options = {}) {
389
- this.client = client;
572
+ this.client = toPgClient(client);
390
573
  this.table = options.table ?? "agent_rag_chunks";
391
- this.dimensions = options.dimensions ?? 1536;
574
+ const dimensions = options.dimensions ?? 1536;
575
+ if (typeof dimensions === "number") {
576
+ this.dimensions = dimensions;
577
+ this.indexedDimensions = [];
578
+ } else {
579
+ for (const width of dimensions) {
580
+ if (!Number.isInteger(width) || width <= 0) {
581
+ throw new Error(`PgVectorStore: invalid embedding width ${String(width)}`);
582
+ }
583
+ }
584
+ this.dimensions = void 0;
585
+ this.indexedDimensions = [
586
+ ...new Set(dimensions)
587
+ ];
588
+ }
589
+ this.nullableEmbeddings = options.nullableEmbeddings ?? false;
590
+ this.iterativeScan = options.iterativeScan;
591
+ this.efSearch = options.efSearch;
592
+ this.upsertBatchSize = Math.max(1, Math.floor(options.upsertBatchSize ?? 100));
392
593
  }
393
594
  /** Telemetry self-description — the table is the namespace a retrieval was served from. */
394
595
  describeRetrieval() {
@@ -397,38 +598,77 @@ var PgVectorStore = class {
397
598
  collection: this.table
398
599
  };
399
600
  }
400
- /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index. */
401
- async ensureSchema() {
402
- await this.client.query("CREATE EXTENSION IF NOT EXISTS vector");
403
- await this.client.query(`CREATE TABLE IF NOT EXISTS ${this.table} (
601
+ /**
602
+ * The idempotent DDL {@link PgVectorStore.ensureSchema} runs, in order — for a migration instead
603
+ * of a boot-time side effect. On a populated table, run the index statements `CONCURRENTLY` there.
604
+ */
605
+ schemaStatements() {
606
+ const mixed = this.dimensions === void 0;
607
+ const column = mixed ? "vector" : `vector(${this.dimensions})`;
608
+ const statements = [
609
+ "CREATE EXTENSION IF NOT EXISTS vector",
610
+ `CREATE TABLE IF NOT EXISTS ${this.table} (
404
611
  id TEXT PRIMARY KEY,
405
612
  text TEXT NOT NULL,
406
613
  source TEXT,
407
614
  metadata JSONB,
408
- embedding vector(${this.dimensions}) NOT NULL
409
- )`);
410
- await this.client.query(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
615
+ embedding ${column}${this.nullableEmbeddings ? "" : " NOT NULL"}
616
+ )`
617
+ ];
618
+ if (mixed) {
619
+ for (const width of this.indexedDimensions) {
620
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_${width}_idx
621
+ ON ${this.table} USING hnsw ((embedding::vector(${width})) vector_cosine_ops)
622
+ WHERE vector_dims(embedding) = ${width}`);
623
+ }
624
+ } else {
625
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_embedding_idx
411
626
  ON ${this.table} USING hnsw (embedding vector_cosine_ops)`);
627
+ }
628
+ return statements;
629
+ }
630
+ /** Idempotent DDL: the `vector` extension, the chunks table, and the cosine HNSW index(es). */
631
+ async ensureSchema() {
632
+ for (const statement of this.schemaStatements()) {
633
+ await this.client.query(statement);
634
+ }
412
635
  }
636
+ /**
637
+ * Multi-row `INSERT … ON CONFLICT (id) DO UPDATE`, {@link PgVectorStoreOptions.upsertBatchSize}
638
+ * rows per statement. An empty `embedding` is written as `NULL` (see
639
+ * {@link PgVectorStoreOptions.nullableEmbeddings}).
640
+ */
413
641
  async upsert(records) {
642
+ const byId = /* @__PURE__ */ new Map();
414
643
  for (const record of records) {
415
644
  const id = stripNulBytes(record.id);
416
- const text = stripNulBytes(record.text);
417
- const source = record.source !== void 0 ? stripNulBytes(record.source) : void 0;
418
- const metadata = record.metadata !== void 0 ? stripNulBytes(record.metadata) : void 0;
645
+ byId.delete(id);
646
+ byId.set(id, {
647
+ ...record,
648
+ id
649
+ });
650
+ }
651
+ const unique = [
652
+ ...byId.values()
653
+ ];
654
+ for (let start = 0; start < unique.length; start += this.upsertBatchSize) {
655
+ const batch = unique.slice(start, start + this.upsertBatchSize);
656
+ const params = [];
657
+ const rows = batch.map((record) => {
658
+ const text = stripNulBytes(record.text);
659
+ const source = record.source !== void 0 ? stripNulBytes(record.source) : void 0;
660
+ const metadata = record.metadata !== void 0 ? stripNulBytes(record.metadata) : void 0;
661
+ const base = params.length;
662
+ params.push(record.id, text, source ?? null, metadata !== void 0 ? JSON.stringify(metadata) : null, record.embedding.length > 0 ? toVectorLiteral(record.embedding) : null);
663
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}::jsonb, $${base + 5}::vector)`;
664
+ });
419
665
  await this.client.query(`INSERT INTO ${this.table} (id, text, source, metadata, embedding)
420
- VALUES ($1, $2, $3, $4, $5)
666
+ VALUES ${rows.join(", ")}
421
667
  ON CONFLICT (id) DO UPDATE SET
422
668
  text = EXCLUDED.text,
423
669
  source = EXCLUDED.source,
424
670
  metadata = EXCLUDED.metadata,
425
- embedding = EXCLUDED.embedding`, [
426
- id,
427
- text,
428
- source ?? null,
429
- metadata !== void 0 ? JSON.stringify(metadata) : null,
430
- toVectorLiteral(record.embedding)
431
- ]);
671
+ embedding = EXCLUDED.embedding`, params);
432
672
  }
433
673
  }
434
674
  async remove(documentId) {
@@ -464,11 +704,12 @@ var PgVectorStore = class {
464
704
  return rows.length;
465
705
  }
466
706
  async listDocuments(filter) {
467
- const where = buildWhere(filter, 1);
707
+ const params = [];
708
+ const where = this.where(filter, params);
468
709
  const rows = await this.client.query(`SELECT DISTINCT ON (${DOCUMENT_ID_FROM_CHUNK}) ${DOCUMENT_ID_FROM_CHUNK} AS doc_id, metadata
469
710
  FROM ${this.table}
470
- ${where.sql}
471
- ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`, where.params);
711
+ ${where}
712
+ ORDER BY ${DOCUMENT_ID_FROM_CHUNK}`, params);
472
713
  return rows.map((row) => ({
473
714
  id: row.doc_id,
474
715
  ...row.metadata !== null ? {
@@ -519,11 +760,12 @@ var PgVectorStore = class {
519
760
  if (filterMatchesNothing(filter)) {
520
761
  return [];
521
762
  }
522
- const where = buildWhere(filter, 1);
763
+ const params = [];
764
+ const where = this.where(filter, params);
523
765
  const rows = await this.client.query(`SELECT DISTINCT ${DOCUMENT_ID_FROM_CHUNK} AS doc_id
524
766
  FROM ${this.table}
525
- ${where.sql}
526
- ORDER BY doc_id`, where.params);
767
+ ${where}
768
+ ORDER BY doc_id`, params);
527
769
  return rows.map((row) => row.doc_id);
528
770
  }
529
771
  /** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */
@@ -547,81 +789,250 @@ var PgVectorStore = class {
547
789
  if (filterMatchesNothing(filter)) {
548
790
  return 0;
549
791
  }
550
- const where = buildWhere(filter, 1);
551
- const rows = await this.client.query(`DELETE FROM ${this.table} ${where.sql} RETURNING id`, where.params);
792
+ const params = [];
793
+ const where = this.where(filter, params);
794
+ const rows = await this.client.query(`DELETE FROM ${this.table} ${where} RETURNING id`, params);
552
795
  return rows.length;
553
796
  }
554
797
  async countChunks(filter) {
555
798
  if (filterMatchesNothing(filter)) {
556
799
  return 0;
557
800
  }
558
- const where = buildWhere(filter, 1);
559
- const rows = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table} ${where.sql}`, where.params);
801
+ const params = [];
802
+ const where = this.where(filter, params);
803
+ const rows = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table} ${where}`, params);
560
804
  return Number(rows[0]?.count ?? 0);
561
805
  }
806
+ /**
807
+ * Cosine similarity, nearest first. On a mixed-dimension table only vectors as wide as the query
808
+ * are compared (through that width's partial index when it has one); chunks without an embedding
809
+ * are never returned.
810
+ */
562
811
  async search(embedding, options) {
563
- const vector = toVectorLiteral(embedding);
564
- const where = buildWhere(options.filter, 3);
565
- const rows = await this.client.query(`SELECT id, text, source, metadata, 1 - (embedding <=> $1::vector) AS score
812
+ if (embedding.length === 0) {
813
+ return [];
814
+ }
815
+ const params = [
816
+ toVectorLiteral(embedding),
817
+ options.topK
818
+ ];
819
+ const conditions = this.whereConditions(options.filter, params);
820
+ let vectorType = "vector";
821
+ if (this.dimensions === void 0) {
822
+ const width = embedding.length;
823
+ if (this.indexedDimensions.includes(width)) {
824
+ vectorType = `vector(${width})`;
825
+ }
826
+ conditions.push("embedding IS NOT NULL", `vector_dims(embedding) = ${width}`);
827
+ } else if (this.nullableEmbeddings) {
828
+ conditions.push("embedding IS NOT NULL");
829
+ }
830
+ const column = vectorType === "vector" ? "embedding" : `(embedding::${vectorType})`;
831
+ const sql = `SELECT id, text, source, metadata, 1 - (${column} <=> $1::${vectorType}) AS score
566
832
  FROM ${this.table}
567
- ${where.sql}
568
- ORDER BY embedding <=> $1::vector
569
- LIMIT $2`, [
570
- vector,
833
+ ${conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""}
834
+ ORDER BY ${column} <=> $1::${vectorType}
835
+ LIMIT $2`;
836
+ const rows = await this.withSearchSettings((client) => client.query(sql, params));
837
+ return rows.map(toPassage);
838
+ }
839
+ /**
840
+ * The SQL conditions (ANDed) a metadata `filter` compiles to, appending their bindings to `params`
841
+ * (placeholders are numbered after what `params` already holds). Every statement that takes a
842
+ * filter goes through here, so a subclass that maps the filter onto its own columns — a tenant id,
843
+ * an access list — overrides this one method. Return `['false']` to deny.
844
+ */
845
+ whereConditions(filter, params) {
846
+ return buildWhere(filter, params);
847
+ }
848
+ where(filter, params) {
849
+ const conditions = this.whereConditions(filter, params);
850
+ return conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
851
+ }
852
+ /** Run a read with the configured HNSW settings `SET LOCAL` in one transaction, when possible. */
853
+ async withSearchSettings(read) {
854
+ const transaction = this.client.transaction?.bind(this.client);
855
+ if (transaction === void 0) {
856
+ return read(this.client);
857
+ }
858
+ const iterative = this.iterativeScan !== void 0 && await this.supportsIterativeScan();
859
+ if (!iterative && this.efSearch === void 0) {
860
+ return read(this.client);
861
+ }
862
+ return transaction(async (tx) => {
863
+ if (iterative) {
864
+ await tx.query(`SELECT set_config('hnsw.iterative_scan', $1, true)`, [
865
+ this.iterativeScan
866
+ ]);
867
+ }
868
+ if (this.efSearch !== void 0) {
869
+ await tx.query(`SELECT set_config('hnsw.ef_search', $1, true)`, [
870
+ String(this.efSearch)
871
+ ]);
872
+ }
873
+ return read(tx);
874
+ });
875
+ }
876
+ /** `hnsw.iterative_scan` exists from pgvector 0.8.0; setting it on an older one is an error. */
877
+ supportsIterativeScan() {
878
+ this.iterativeScanSupport ??= this.client.query(`SELECT extversion FROM pg_extension WHERE extname = 'vector'`).then((rows) => {
879
+ const [major = 0, minor = 0] = (rows[0]?.extversion ?? "0.0").split(".").map(Number);
880
+ return major > 0 || minor >= 8;
881
+ }).catch(() => false);
882
+ return this.iterativeScanSupport;
883
+ }
884
+ };
885
+ var PgLexicalVectorStore = class extends PgVectorStore {
886
+ static {
887
+ __name(this, "PgLexicalVectorStore");
888
+ }
889
+ tsConfig;
890
+ tsColumn;
891
+ anyTermFallback;
892
+ stopWords;
893
+ constructor(client, options = {}) {
894
+ super(client, options);
895
+ const config = options.fullText?.config ?? "simple";
896
+ if (!SQL_IDENTIFIER.test(config)) {
897
+ throw new Error(`PgLexicalVectorStore: invalid text search configuration "${config}"`);
898
+ }
899
+ const column = options.fullText?.column;
900
+ if (column !== void 0 && !SQL_IDENTIFIER.test(column)) {
901
+ throw new Error(`PgLexicalVectorStore: invalid tsvector column "${column}"`);
902
+ }
903
+ this.tsConfig = config;
904
+ this.tsColumn = column;
905
+ this.anyTermFallback = options.fullText?.anyTermFallback ?? true;
906
+ const stopWords = options.fullText?.stopWords;
907
+ this.stopWords = stopWords === false ? {} : stopWords ?? DEFAULT_STOP_WORDS;
908
+ }
909
+ schemaStatements() {
910
+ const statements = super.schemaStatements();
911
+ if (this.tsColumn === void 0) {
912
+ statements.push(`CREATE INDEX IF NOT EXISTS ${this.table}_text_tsv_idx
913
+ ON ${this.table} USING gin (${this.tsvector()})`);
914
+ }
915
+ return statements;
916
+ }
917
+ async searchText(query, options) {
918
+ if (filterMatchesNothing(options.filter) || query.trim() === "") {
919
+ return [];
920
+ }
921
+ if (!this.anyTermFallback || hasSearchSyntax(query)) {
922
+ const strict = await this.runTextSearch(query, options);
923
+ if (strict.length > 0 || !this.anyTermFallback) {
924
+ return strict;
925
+ }
926
+ }
927
+ const terms = keywordTerms(query, 24, this.stopWords);
928
+ return terms.length === 0 ? [] : this.runRankedTermSearch(terms, options);
929
+ }
930
+ /** `websearch_to_tsquery` on the query as written: every word, quoted phrases, `-exclusions`. */
931
+ async runTextSearch(query, options) {
932
+ const tsquery = `websearch_to_tsquery('${this.tsConfig}', $1)`;
933
+ const params = [
934
+ query,
935
+ options.topK
936
+ ];
937
+ const conditions = [
938
+ `${this.tsvector()} @@ ${tsquery}`,
939
+ ...this.whereConditions(options.filter, params)
940
+ ];
941
+ const rows = await this.client.query(`SELECT id, text, source, metadata, ts_rank_cd(${this.tsvector()}, ${tsquery}) AS score
942
+ FROM ${this.table}
943
+ WHERE ${conditions.join(" AND ")}
944
+ ORDER BY score DESC, id
945
+ LIMIT $2`, params);
946
+ return rows.map(toPassage);
947
+ }
948
+ /**
949
+ * Rows holding any of `terms`, ranked by the sum of the matched terms' IDF among those rows,
950
+ * `ln(1 + (N - df + 0.5) / (df + 0.5))`, then `ts_rank_cd`. Each (row, term) match is evaluated
951
+ * once, and `ts_rank_cd` only for the rows ranked within `topK` (ties included): a question full
952
+ * of common words can match most of the table.
953
+ */
954
+ async runRankedTermSearch(terms, options) {
955
+ const any = `to_tsquery('${this.tsConfig}', $1)`;
956
+ const params = [
957
+ anyTermTsquery(terms),
571
958
  options.topK,
572
- ...where.params
573
- ]);
574
- return rows.map((row) => ({
575
- id: row.id,
576
- text: row.text,
577
- score: Number(row.score),
578
- ...row.source !== null ? {
579
- source: row.source
580
- } : {},
581
- ...row.metadata !== null ? {
582
- metadata: row.metadata
583
- } : {}
584
- }));
959
+ terms
960
+ ];
961
+ const conditions = [
962
+ `${this.tsvector()} @@ ${any}`,
963
+ ...this.whereConditions(options.filter, params)
964
+ ];
965
+ const rows = await this.client.query(`WITH __terms AS (
966
+ SELECT DISTINCT to_tsquery('${this.tsConfig}', quote_literal(t)) AS q
967
+ FROM unnest($3::text[]) AS t
968
+ ), __cand AS MATERIALIZED (
969
+ SELECT id, text, source, metadata, ${this.tsvector()} AS __tsv
970
+ FROM ${this.table}
971
+ WHERE ${conditions.join(" AND ")}
972
+ ), __hits AS MATERIALIZED (
973
+ SELECT __cand.id AS __id, __terms.q FROM __cand JOIN __terms ON __cand.__tsv @@ __terms.q
974
+ ), __idf AS (
975
+ SELECT q, ln(1 + ((SELECT count(*) FROM __cand) - count(*) + 0.5) / (count(*) + 0.5)) AS w
976
+ FROM __hits GROUP BY q
977
+ ), __scored AS (
978
+ SELECT __hits.__id, sum(__idf.w) AS s, rank() OVER (ORDER BY sum(__idf.w) DESC) AS r
979
+ FROM __hits JOIN __idf USING (q) GROUP BY __hits.__id
980
+ )
981
+ SELECT __cand.id, __cand.text, __cand.source, __cand.metadata,
982
+ __scored.s + ts_rank_cd(__cand.__tsv, ${any}, 32) AS score
983
+ FROM __scored JOIN __cand ON __cand.id = __scored.__id
984
+ WHERE __scored.r <= $2
985
+ ORDER BY __scored.s DESC, ts_rank_cd(__cand.__tsv, ${any}, 32) DESC, __cand.id
986
+ LIMIT $2`, params);
987
+ return rows.map(toPassage);
988
+ }
989
+ /** Must be character-for-character the index expression, or the planner won't use the index. */
990
+ tsvector() {
991
+ return this.tsColumn ?? `to_tsvector('${this.tsConfig}'::regconfig, text)`;
585
992
  }
586
993
  };
587
- function buildWhere(filter, startIndex) {
994
+ var SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
995
+ function toPassage(row) {
996
+ return {
997
+ id: row.id,
998
+ text: row.text,
999
+ score: Number(row.score),
1000
+ ...row.source !== null ? {
1001
+ source: row.source
1002
+ } : {},
1003
+ ...row.metadata !== null ? {
1004
+ metadata: row.metadata
1005
+ } : {}
1006
+ };
1007
+ }
1008
+ __name(toPassage, "toPassage");
1009
+ function buildWhere(filter, params) {
588
1010
  if (filter === void 0 || Object.keys(filter).length === 0) {
589
- return {
590
- sql: "",
591
- params: []
592
- };
1011
+ return [];
593
1012
  }
594
1013
  const stripped = stripNulBytes(filter);
595
1014
  const clauses = [];
596
- const params = [];
597
1015
  const scalar = {};
598
- let index = startIndex;
599
1016
  for (const [key, value] of Object.entries(stripped)) {
600
1017
  if (Array.isArray(value)) {
601
1018
  if (value.length === 0) {
602
1019
  clauses.push("false");
603
1020
  continue;
604
1021
  }
605
- const keyParam = `$${index}`;
606
1022
  params.push(key);
607
- index += 1;
608
- const arrParam = `$${index}`;
1023
+ const keyParam = `$${params.length}`;
609
1024
  params.push(value.map(String));
610
- index += 1;
1025
+ const arrParam = `$${params.length}`;
611
1026
  clauses.push(`(CASE WHEN jsonb_typeof(metadata->${keyParam}) = 'array' THEN metadata->${keyParam} ELSE jsonb_build_array(metadata->${keyParam}) END) ?| ${arrParam}::text[]`);
612
1027
  } else {
613
1028
  scalar[key] = value;
614
1029
  }
615
1030
  }
616
1031
  if (Object.keys(scalar).length > 0) {
617
- clauses.push(`metadata @> $${index}::jsonb`);
618
1032
  params.push(JSON.stringify(scalar));
619
- index += 1;
1033
+ clauses.push(`metadata @> $${params.length}::jsonb`);
620
1034
  }
621
- return {
622
- sql: `WHERE ${clauses.join(" AND ")}`,
623
- params
624
- };
1035
+ return clauses;
625
1036
  }
626
1037
  __name(buildWhere, "buildWhere");
627
1038
  var DOCUMENT_ID_FROM_CHUNK = "regexp_replace(id, '#[0-9]+$', '')";
@@ -1431,7 +1842,7 @@ function parseSearchReply(reply, prefix, scoreSource = "vector") {
1431
1842
  attrs[toStr(rawFields[field])] = toStr(rawFields[field + 1]);
1432
1843
  }
1433
1844
  const inlineScore = scoreSource === "lexical" ? toStr(reply[index + 1]) : void 0;
1434
- passages.push(toPassage(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));
1845
+ passages.push(toPassage2(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));
1435
1846
  }
1436
1847
  }
1437
1848
  return passages;
@@ -1439,7 +1850,7 @@ function parseSearchReply(reply, prefix, scoreSource = "vector") {
1439
1850
  if (typeof reply === "object" && reply !== null && "results" in reply) {
1440
1851
  const results = reply.results;
1441
1852
  if (Array.isArray(results)) {
1442
- return results.map((result) => toPassage(readId(result), readAttributes(result), prefix, scoreSource, scoreSource === "lexical" ? readScore(result) : void 0));
1853
+ return results.map((result) => toPassage2(readId(result), readAttributes(result), prefix, scoreSource, scoreSource === "lexical" ? readScore(result) : void 0));
1443
1854
  }
1444
1855
  }
1445
1856
  return [];
@@ -1472,7 +1883,7 @@ function readAttributes(result) {
1472
1883
  return attrs;
1473
1884
  }
1474
1885
  __name(readAttributes, "readAttributes");
1475
- function toPassage(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1886
+ function toPassage2(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1476
1887
  const score = scoreSource === "lexical" ? Number(inlineScore ?? "0") : 1 - Number(attrs.vector_score ?? "1");
1477
1888
  const source = attrs.source;
1478
1889
  return {
@@ -1485,7 +1896,7 @@ function toPassage(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
1485
1896
  ...parseMetadata(attrs.metadata_json)
1486
1897
  };
1487
1898
  }
1488
- __name(toPassage, "toPassage");
1899
+ __name(toPassage2, "toPassage");
1489
1900
  function parseMetadata(raw) {
1490
1901
  if (raw === void 0) {
1491
1902
  return {};
@@ -1878,7 +2289,7 @@ async function ingestDocuments(documents, options) {
1878
2289
  __name(ingestDocuments, "ingestDocuments");
1879
2290
 
1880
2291
  // src/retrieval-telemetry.ts
1881
- import { AsyncLocalStorage } from "node:async_hooks";
2292
+ import { AsyncLocalStorage } from "async_hooks";
1882
2293
  import { channelName, emit, getChannel } from "@dudousxd/nestjs-diagnostics";
1883
2294
  var RAG_DIAGNOSTIC_LIB = "rag";
1884
2295
  var RAG_RETRIEVAL_EVENT = "retrieval";
@@ -2030,13 +2441,181 @@ function createRetrievalTool(retriever, options = {}) {
2030
2441
  };
2031
2442
  }
2032
2443
  __name(createRetrievalTool, "createRetrievalTool");
2444
+
2445
+ // src/openai-embeddings.ts
2446
+ var HttpModelError = class extends Error {
2447
+ static {
2448
+ __name(this, "HttpModelError");
2449
+ }
2450
+ status;
2451
+ constructor(status, message) {
2452
+ super(message), this.status = status;
2453
+ this.name = "HttpModelError";
2454
+ }
2455
+ };
2456
+ function openAiEmbeddings(options) {
2457
+ const url = `${(options.baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "")}/embeddings`;
2458
+ const batchSize = Math.max(1, Math.floor(options.batchSize ?? 64));
2459
+ const maxInputChars = options.maxInputChars ?? 24e3;
2460
+ const timeoutMs = options.timeoutMs ?? 12e4;
2461
+ const doFetch = options.fetch ?? fetch;
2462
+ return {
2463
+ async embed(texts) {
2464
+ const vectors = [];
2465
+ for (let start = 0; start < texts.length; start += batchSize) {
2466
+ const input = texts.slice(start, start + batchSize).map((text) => (text.length > maxInputChars ? text.slice(0, maxInputChars) : text) || " ");
2467
+ const response = await doFetch(url, {
2468
+ method: "POST",
2469
+ headers: {
2470
+ "content-type": "application/json",
2471
+ ...options.apiKey ? {
2472
+ authorization: `Bearer ${options.apiKey}`
2473
+ } : {},
2474
+ ...options.headers
2475
+ },
2476
+ body: JSON.stringify({
2477
+ model: options.model,
2478
+ input,
2479
+ ...options.dimensions !== void 0 ? {
2480
+ dimensions: options.dimensions
2481
+ } : {}
2482
+ }),
2483
+ signal: AbortSignal.timeout(timeoutMs)
2484
+ });
2485
+ const body = await response.text();
2486
+ if (!response.ok) {
2487
+ throw new HttpModelError(response.status, `Embeddings request failed (${response.status}): ${errorMessageOf(body)}`);
2488
+ }
2489
+ const json = parseJson(body);
2490
+ const items = [
2491
+ ...json?.data ?? []
2492
+ ].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
2493
+ if (items.length !== input.length || !items.every((item) => Array.isArray(item.embedding))) {
2494
+ throw new HttpModelError(502, `Embeddings returned ${items.length} vectors for ${input.length} inputs`);
2495
+ }
2496
+ for (const item of items) {
2497
+ vectors.push(item.embedding);
2498
+ }
2499
+ const tokens = json?.usage?.total_tokens ?? json?.usage?.prompt_tokens;
2500
+ if (typeof tokens === "number" && Number.isFinite(tokens)) {
2501
+ options.onUsage?.(tokens);
2502
+ }
2503
+ }
2504
+ return vectors;
2505
+ }
2506
+ };
2507
+ }
2508
+ __name(openAiEmbeddings, "openAiEmbeddings");
2509
+ function parseJson(body) {
2510
+ try {
2511
+ return JSON.parse(body);
2512
+ } catch {
2513
+ return null;
2514
+ }
2515
+ }
2516
+ __name(parseJson, "parseJson");
2517
+ function errorMessageOf(body) {
2518
+ const json = parseJson(body);
2519
+ const message = typeof json?.error === "string" ? json.error : json?.error?.message ?? json?.message ?? void 0;
2520
+ return typeof message === "string" ? message : body.slice(0, 300);
2521
+ }
2522
+ __name(errorMessageOf, "errorMessageOf");
2523
+
2524
+ // src/http-reranker.ts
2525
+ var HttpReranker = class {
2526
+ static {
2527
+ __name(this, "HttpReranker");
2528
+ }
2529
+ options;
2530
+ format;
2531
+ doFetch;
2532
+ constructor(options) {
2533
+ this.options = options;
2534
+ this.format = options.format ?? "auto";
2535
+ this.doFetch = options.fetch ?? fetch;
2536
+ }
2537
+ async rerank(query, passages, options = {}) {
2538
+ if (passages.length === 0) {
2539
+ return [];
2540
+ }
2541
+ const topK = options.topK ?? passages.length;
2542
+ const documents = passages.map((passage) => passage.text);
2543
+ const response = await this.doFetch(this.options.url, {
2544
+ method: "POST",
2545
+ headers: {
2546
+ "content-type": "application/json",
2547
+ ...this.options.apiKey ? {
2548
+ authorization: `Bearer ${this.options.apiKey}`
2549
+ } : {},
2550
+ ...this.options.headers
2551
+ },
2552
+ body: JSON.stringify({
2553
+ ...this.options.model !== void 0 ? {
2554
+ model: this.options.model
2555
+ } : {},
2556
+ query,
2557
+ ...this.format !== "tei" ? {
2558
+ documents,
2559
+ top_n: topK
2560
+ } : {},
2561
+ ...this.format !== "cohere" ? {
2562
+ texts: documents
2563
+ } : {}
2564
+ }),
2565
+ signal: AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
2566
+ });
2567
+ const body = await response.text();
2568
+ if (!response.ok) {
2569
+ throw new HttpModelError(response.status, `Rerank request failed (${response.status}): ${errorMessageOf(body)}`);
2570
+ }
2571
+ const items = itemsOf(body);
2572
+ if (items === void 0) {
2573
+ throw new HttpModelError(502, "Rerank response has no results");
2574
+ }
2575
+ const seen = /* @__PURE__ */ new Set();
2576
+ const reranked = [];
2577
+ for (const item of items) {
2578
+ const index = item.index;
2579
+ const passage = typeof index === "number" ? passages[index] : void 0;
2580
+ if (passage === void 0 || seen.has(index)) {
2581
+ continue;
2582
+ }
2583
+ seen.add(index);
2584
+ reranked.push({
2585
+ ...passage,
2586
+ score: Number(item.relevance_score ?? item.score ?? 0)
2587
+ });
2588
+ }
2589
+ reranked.sort((a, b) => b.score - a.score);
2590
+ return reranked.slice(0, topK);
2591
+ }
2592
+ };
2593
+ function itemsOf(body) {
2594
+ let json;
2595
+ try {
2596
+ json = JSON.parse(body);
2597
+ } catch {
2598
+ return void 0;
2599
+ }
2600
+ if (Array.isArray(json)) {
2601
+ return json;
2602
+ }
2603
+ const wrapped = json;
2604
+ const items = wrapped?.results ?? wrapped?.data;
2605
+ return Array.isArray(items) ? items : void 0;
2606
+ }
2607
+ __name(itemsOf, "itemsOf");
2033
2608
  export {
2609
+ DEFAULT_STOP_WORDS,
2034
2610
  EmbeddingRetriever,
2035
2611
  FilteredRetriever,
2612
+ HttpModelError,
2613
+ HttpReranker,
2036
2614
  HybridRetriever,
2037
2615
  KeywordRetriever,
2038
2616
  LexicalRetriever,
2039
2617
  MemoryVectorStore,
2618
+ PgLexicalVectorStore,
2040
2619
  PgVectorStore,
2041
2620
  RAG_DIAGNOSTIC_LIB,
2042
2621
  RAG_RETRIEVAL_CHANNEL,
@@ -2045,6 +2624,7 @@ export {
2045
2624
  RedisVectorStore,
2046
2625
  RerankingRetriever,
2047
2626
  UnsafeRemovalError,
2627
+ anyTermTsquery,
2048
2628
  applyMetadataPatch,
2049
2629
  chunkDocuments,
2050
2630
  chunkIndexOf,
@@ -2056,9 +2636,13 @@ export {
2056
2636
  describesRetrieval,
2057
2637
  documentIdOf,
2058
2638
  emitRagRetrieval,
2639
+ hasSearchSyntax,
2059
2640
  ingestChunks,
2060
2641
  ingestDocuments,
2061
2642
  instrumentRetriever,
2062
- isLexicalVectorStore
2643
+ isLexicalVectorStore,
2644
+ keywordTerms,
2645
+ openAiEmbeddings,
2646
+ toPgClient
2063
2647
  };
2064
2648
  //# sourceMappingURL=index.js.map