@dudousxd/nestjs-agent-rag 0.11.0 → 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/README.md CHANGED
@@ -230,9 +230,13 @@ const store = new PgLexicalVectorStore(db, {
230
230
  });
231
231
  ```
232
232
 
233
- Ranking is `ts_rank_cd`; the query goes through `websearch_to_tsquery` (every word must appear), and
234
- when that finds nothing it retries with any word (`anyTermFallback`, default on) so a long question
235
- still matches. Without `column`, `ensureSchema`/`schemaStatements` add a GIN index over
233
+ A question is searched by its meaningful terms: its stop words are dropped (English, Portuguese and
234
+ Spanish lists by default, only the language(s) the question is written in; `stopWords` to change or
235
+ `false` to keep every word), and rows holding any remaining term are ranked by the sum of the
236
+ matched terms' IDF (BM25 without term frequencies), `ts_rank_cd` breaking ties, so three rare terms
237
+ beat a row full of "the/of/which". Quoted phrases and `-word` go through `websearch_to_tsquery` as
238
+ written first; `anyTermFallback: false` keeps only that (every word must appear). `keywordTerms()`
239
+ is exported for other stores. Without `column`, `ensureSchema`/`schemaStatements` add a GIN index over
236
240
  `to_tsvector(config, text)` — the exact expression the query uses. On a populated table create it
237
241
  `CONCURRENTLY` in a migration rather than at boot (see below).
238
242
 
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ DEFAULT_STOP_WORDS: () => DEFAULT_STOP_WORDS,
24
25
  EmbeddingRetriever: () => EmbeddingRetriever,
25
26
  FilteredRetriever: () => FilteredRetriever,
26
27
  HttpModelError: () => HttpModelError,
@@ -38,6 +39,7 @@ __export(index_exports, {
38
39
  RedisVectorStore: () => RedisVectorStore,
39
40
  RerankingRetriever: () => RerankingRetriever,
40
41
  UnsafeRemovalError: () => UnsafeRemovalError,
42
+ anyTermTsquery: () => anyTermTsquery,
41
43
  applyMetadataPatch: () => applyMetadataPatch,
42
44
  chunkDocuments: () => chunkDocuments,
43
45
  chunkIndexOf: () => chunkIndexOf,
@@ -49,10 +51,12 @@ __export(index_exports, {
49
51
  describesRetrieval: () => describesRetrieval,
50
52
  documentIdOf: () => documentIdOf,
51
53
  emitRagRetrieval: () => emitRagRetrieval,
54
+ hasSearchSyntax: () => hasSearchSyntax,
52
55
  ingestChunks: () => ingestChunks,
53
56
  ingestDocuments: () => ingestDocuments,
54
57
  instrumentRetriever: () => instrumentRetriever,
55
58
  isLexicalVectorStore: () => isLexicalVectorStore,
59
+ keywordTerms: () => keywordTerms,
56
60
  openAiEmbeddings: () => openAiEmbeddings,
57
61
  toPgClient: () => toPgClient
58
62
  });
@@ -413,6 +417,81 @@ function cosineSimilarity(a, b) {
413
417
  }
414
418
  __name(cosineSimilarity, "cosineSimilarity");
415
419
 
420
+ // src/lexical-query.ts
421
+ var words = /* @__PURE__ */ __name((s) => new Set(s.split(/\s+/).filter(Boolean)), "words");
422
+ var DEFAULT_STOP_WORDS = {
423
+ english: words(`
424
+ me my myself we our ours ourselves you your yours yourself yourselves he him his himself she
425
+ her hers herself it its itself they them their theirs themselves what which who whom this that
426
+ these those am is are was were be been being have has had having do does did doing an the and
427
+ but if or because as until while of at by for with about against between into through during
428
+ before after above below to from up down in out on off over under again further then once here
429
+ there when where why how all any both each few more most other some such no nor not only own
430
+ same so than too very can will just don should now`),
431
+ portuguese: words(`
432
+ 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
433
+ quando muito nos j\xE1 eu tamb\xE9m s\xF3 pelo pela at\xE9 isso ela entre depois sem mesmo aos seus quem
434
+ nas me esse eles voc\xEA essa num nem suas meu \xE0s minha numa pelos elas qual n\xF3s lhe deles essas
435
+ esses pelas este dele tu te voc\xEAs vos lhes meus minhas teu tua teus tuas nosso nossa nossos
436
+ nossas dela delas esta estes estas aquele aquela aqueles aquelas isto aquilo estou est\xE1 estamos
437
+ est\xE3o estive esteve estivemos estiveram estava est\xE1vamos estavam estivera estiv\xE9ramos esteja
438
+ estejamos estejam estivesse estiv\xE9ssemos estivessem estiver estivermos estiverem hei h\xE1 havemos
439
+ h\xE3o houve houvemos houveram houvera houv\xE9ramos haja hajamos hajam houvesse houv\xE9ssemos
440
+ houvessem houver houvermos houverem houverei houver\xE1 houveremos houver\xE3o houveria houver\xEDamos
441
+ houveriam sou somos s\xE3o era \xE9ramos eram fui foi fomos foram fora f\xF4ramos seja sejamos sejam
442
+ fosse f\xF4ssemos fossem for formos forem serei ser\xE1 seremos ser\xE3o seria ser\xEDamos seriam tenho tem
443
+ temos t\xE9m tinha t\xEDnhamos tinham tive teve tivemos tiveram tivera tiv\xE9ramos tenha tenhamos
444
+ tenham tivesse tiv\xE9ssemos tivessem tiver tivermos tiverem terei ter\xE1 teremos ter\xE3o teria
445
+ ter\xEDamos teriam`),
446
+ spanish: words(`
447
+ 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
448
+ porque esta entre cuando muy sin sobre tambi\xE9n me hasta hay donde quien desde todo nos durante
449
+ todos uno les ni contra otros ese eso ante ellos esto m\xED antes algunos qu\xE9 unos yo otro otras
450
+ otra \xE9l tanto esa estos mucho quienes nada muchos cual poco ella estar estas algunas algo
451
+ nosotros mi mis t\xFA te ti tu tus ellas nosotras vosotros vosotras os m\xEDo m\xEDa m\xEDos m\xEDas tuyo
452
+ tuya tuyos tuyas suyo suya suyos suyas nuestro nuestra nuestros nuestras vuestro vuestra
453
+ vuestros vuestras esos esas estoy est\xE1s est\xE1 estamos est\xE1is est\xE1n est\xE9 est\xE9s estemos est\xE9is
454
+ est\xE9n estar\xE9 estar\xE1s estar\xE1 estaremos estar\xE9is estar\xE1n estar\xEDa estar\xEDas estar\xEDamos estar\xEDais
455
+ estar\xEDan estaba estabas est\xE1bamos estabais estaban estuve estuviste estuvo estuvimos
456
+ estuvisteis estuvieron estuviera estuvieras estuvi\xE9ramos estuvierais estuvieran estuviese
457
+ estuvieses estuvi\xE9semos estuvieseis estuviesen estando estad he has ha hemos hab\xE9is han haya
458
+ hayas hayamos hay\xE1is hayan habr\xE9 habr\xE1s habr\xE1 habremos habr\xE9is habr\xE1n habr\xEDa habr\xEDas habr\xEDamos
459
+ habr\xEDais habr\xEDan hab\xEDa hab\xEDas hab\xEDamos hab\xEDais hab\xEDan hube hubiste hubo hubimos hubisteis
460
+ hubieron hubiera hubieras hubi\xE9ramos hubierais hubieran hubiese hubieses hubi\xE9semos hubieseis
461
+ hubiesen habiendo habido habida habidos habidas soy eres es somos sois son sea seas seamos
462
+ 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
463
+ \xE9ramos erais eran fui fuiste fue fuimos fuisteis fueron fuera fueras fu\xE9ramos fuerais fueran
464
+ fuese fueses fu\xE9semos fueseis fuesen tengo tienes tiene tenemos ten\xE9is tienen tenga tengas
465
+ tengamos teng\xE1is tengan tendr\xE9 tendr\xE1s tendr\xE1 tendremos tendr\xE9is tendr\xE1n tendr\xEDa tendr\xEDas
466
+ tendr\xEDamos tendr\xEDais tendr\xEDan ten\xEDa ten\xEDas ten\xEDamos ten\xEDais ten\xEDan tuve tuviste tuvo tuvimos
467
+ tuvisteis tuvieron tuviera tuvieras tuvi\xE9ramos tuvierais tuvieran tuviese tuvieses tuvi\xE9semos
468
+ tuvieseis tuviesen teniendo tenido tenida tenidos tenidas tened`)
469
+ };
470
+ var TOKEN = /[\p{L}\p{N}]{2,}/gu;
471
+ function keywordTerms(query, max = 24, stopWords = DEFAULT_STOP_WORDS) {
472
+ const tokens = [
473
+ ...new Set(query.toLowerCase().normalize("NFC").match(TOKEN) ?? [])
474
+ ];
475
+ const counts = Object.entries(stopWords).map(([, stop2]) => [
476
+ stop2,
477
+ tokens.filter((t) => stop2.has(t)).length
478
+ ]);
479
+ const best = Math.max(0, ...counts.map(([, n]) => n));
480
+ if (best === 0) return tokens.slice(0, max);
481
+ const stop = counts.filter(([, n]) => n === best).map(([s]) => s);
482
+ const terms = tokens.filter((t) => !stop.some((s) => s.has(t)));
483
+ return (terms.length > 0 ? terms : tokens).slice(0, max);
484
+ }
485
+ __name(keywordTerms, "keywordTerms");
486
+ function hasSearchSyntax(query) {
487
+ return /"[^"]+"/.test(query) || /(^|\s)-[\p{L}\p{N}]/u.test(query);
488
+ }
489
+ __name(hasSearchSyntax, "hasSearchSyntax");
490
+ function anyTermTsquery(terms) {
491
+ return terms.map((t) => `'${t.replace(/'/g, "")}'`).join(" | ");
492
+ }
493
+ __name(anyTermTsquery, "anyTermTsquery");
494
+
416
495
  // src/pg-client.ts
417
496
  var ADAPTED = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-rag/pg-client");
418
497
  function toPgClient(source) {
@@ -871,6 +950,7 @@ var PgLexicalVectorStore = class extends PgVectorStore {
871
950
  tsConfig;
872
951
  tsColumn;
873
952
  anyTermFallback;
953
+ stopWords;
874
954
  constructor(client, options = {}) {
875
955
  super(client, options);
876
956
  const config = options.fullText?.config ?? "simple";
@@ -884,6 +964,8 @@ var PgLexicalVectorStore = class extends PgVectorStore {
884
964
  this.tsConfig = config;
885
965
  this.tsColumn = column;
886
966
  this.anyTermFallback = options.fullText?.anyTermFallback ?? true;
967
+ const stopWords = options.fullText?.stopWords;
968
+ this.stopWords = stopWords === false ? {} : stopWords ?? DEFAULT_STOP_WORDS;
887
969
  }
888
970
  schemaStatements() {
889
971
  const statements = super.schemaStatements();
@@ -897,19 +979,18 @@ var PgLexicalVectorStore = class extends PgVectorStore {
897
979
  if (filterMatchesNothing(options.filter) || query.trim() === "") {
898
980
  return [];
899
981
  }
900
- const all = await this.runTextSearch(`websearch_to_tsquery('${this.tsConfig}', $1)`, query, options);
901
- if (all.length > 0 || !this.anyTermFallback) {
902
- return all;
903
- }
904
- const terms = [
905
- ...new Set(query.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [])
906
- ].slice(0, 24);
907
- if (terms.length < 2) {
908
- return all;
982
+ if (!this.anyTermFallback || hasSearchSyntax(query)) {
983
+ const strict = await this.runTextSearch(query, options);
984
+ if (strict.length > 0 || !this.anyTermFallback) {
985
+ return strict;
986
+ }
909
987
  }
910
- return this.runTextSearch(`to_tsquery('${this.tsConfig}', $1)`, terms.map((term) => `'${term}'`).join(" | "), options);
988
+ const terms = keywordTerms(query, 24, this.stopWords);
989
+ return terms.length === 0 ? [] : this.runRankedTermSearch(terms, options);
911
990
  }
912
- async runTextSearch(tsquery, query, options) {
991
+ /** `websearch_to_tsquery` on the query as written: every word, quoted phrases, `-exclusions`. */
992
+ async runTextSearch(query, options) {
993
+ const tsquery = `websearch_to_tsquery('${this.tsConfig}', $1)`;
913
994
  const params = [
914
995
  query,
915
996
  options.topK
@@ -925,6 +1006,47 @@ var PgLexicalVectorStore = class extends PgVectorStore {
925
1006
  LIMIT $2`, params);
926
1007
  return rows.map(toPassage);
927
1008
  }
1009
+ /**
1010
+ * Rows holding any of `terms`, ranked by the sum of the matched terms' IDF among those rows,
1011
+ * `ln(1 + (N - df + 0.5) / (df + 0.5))`, then `ts_rank_cd`. Each (row, term) match is evaluated
1012
+ * once, and `ts_rank_cd` only for the rows ranked within `topK` (ties included): a question full
1013
+ * of common words can match most of the table.
1014
+ */
1015
+ async runRankedTermSearch(terms, options) {
1016
+ const any = `to_tsquery('${this.tsConfig}', $1)`;
1017
+ const params = [
1018
+ anyTermTsquery(terms),
1019
+ options.topK,
1020
+ terms
1021
+ ];
1022
+ const conditions = [
1023
+ `${this.tsvector()} @@ ${any}`,
1024
+ ...this.whereConditions(options.filter, params)
1025
+ ];
1026
+ const rows = await this.client.query(`WITH __terms AS (
1027
+ SELECT DISTINCT to_tsquery('${this.tsConfig}', quote_literal(t)) AS q
1028
+ FROM unnest($3::text[]) AS t
1029
+ ), __cand AS MATERIALIZED (
1030
+ SELECT id, text, source, metadata, ${this.tsvector()} AS __tsv
1031
+ FROM ${this.table}
1032
+ WHERE ${conditions.join(" AND ")}
1033
+ ), __hits AS MATERIALIZED (
1034
+ SELECT __cand.id AS __id, __terms.q FROM __cand JOIN __terms ON __cand.__tsv @@ __terms.q
1035
+ ), __idf AS (
1036
+ SELECT q, ln(1 + ((SELECT count(*) FROM __cand) - count(*) + 0.5) / (count(*) + 0.5)) AS w
1037
+ FROM __hits GROUP BY q
1038
+ ), __scored AS (
1039
+ SELECT __hits.__id, sum(__idf.w) AS s, rank() OVER (ORDER BY sum(__idf.w) DESC) AS r
1040
+ FROM __hits JOIN __idf USING (q) GROUP BY __hits.__id
1041
+ )
1042
+ SELECT __cand.id, __cand.text, __cand.source, __cand.metadata,
1043
+ __scored.s + ts_rank_cd(__cand.__tsv, ${any}, 32) AS score
1044
+ FROM __scored JOIN __cand ON __cand.id = __scored.__id
1045
+ WHERE __scored.r <= $2
1046
+ ORDER BY __scored.s DESC, ts_rank_cd(__cand.__tsv, ${any}, 32) DESC, __cand.id
1047
+ LIMIT $2`, params);
1048
+ return rows.map(toPassage);
1049
+ }
928
1050
  /** Must be character-for-character the index expression, or the planner won't use the index. */
929
1051
  tsvector() {
930
1052
  return this.tsColumn ?? `to_tsvector('${this.tsConfig}'::regconfig, text)`;
@@ -2546,6 +2668,7 @@ function itemsOf(body) {
2546
2668
  __name(itemsOf, "itemsOf");
2547
2669
  // Annotate the CommonJS export names for ESM import in node:
2548
2670
  0 && (module.exports = {
2671
+ DEFAULT_STOP_WORDS,
2549
2672
  EmbeddingRetriever,
2550
2673
  FilteredRetriever,
2551
2674
  HttpModelError,
@@ -2563,6 +2686,7 @@ __name(itemsOf, "itemsOf");
2563
2686
  RedisVectorStore,
2564
2687
  RerankingRetriever,
2565
2688
  UnsafeRemovalError,
2689
+ anyTermTsquery,
2566
2690
  applyMetadataPatch,
2567
2691
  chunkDocuments,
2568
2692
  chunkIndexOf,
@@ -2574,10 +2698,12 @@ __name(itemsOf, "itemsOf");
2574
2698
  describesRetrieval,
2575
2699
  documentIdOf,
2576
2700
  emitRagRetrieval,
2701
+ hasSearchSyntax,
2577
2702
  ingestChunks,
2578
2703
  ingestDocuments,
2579
2704
  instrumentRetriever,
2580
2705
  isLexicalVectorStore,
2706
+ keywordTerms,
2581
2707
  openAiEmbeddings,
2582
2708
  toPgClient
2583
2709
  });