@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 +7 -3
- package/dist/index.cjs +137 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -7
- package/dist/index.d.ts +56 -7
- package/dist/index.js +133 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,9 +230,13 @@ const store = new PgLexicalVectorStore(db, {
|
|
|
230
230
|
});
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
988
|
+
const terms = keywordTerms(query, 24, this.stopWords);
|
|
989
|
+
return terms.length === 0 ? [] : this.runRankedTermSearch(terms, options);
|
|
911
990
|
}
|
|
912
|
-
|
|
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
|
});
|