@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/dist/index.d.cts CHANGED
@@ -432,6 +432,34 @@ declare class MemoryVectorStore implements VectorStore {
432
432
  search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
433
433
  }
434
434
 
435
+ /**
436
+ * Turning a natural-language question into full-text search terms, for the lexical leg of
437
+ * `PgLexicalVectorStore`.
438
+ *
439
+ * With the `simple` text-search configuration (no stemming, no stop words; any language) a question
440
+ * searched as is either matches nothing (every word required) or, as "any word", ranks chunks full
441
+ * of "the / of / which / de / que" first. So the query side drops the stop words of the question's
442
+ * language(s) and keeps the meaningful terms, which the store then ranks by how many of them a chunk
443
+ * holds, rare terms weighing more (an IDF over the matching rows, BM25-style).
444
+ */
445
+ /**
446
+ * Postgres' Snowball stop word lists (`tsearch_data/{english,portuguese,spanish}.stop`), minus
447
+ * one-letter words (never terms here) and words that carry meaning in a sibling language
448
+ * ("estado", "sentido").
449
+ */
450
+ declare const DEFAULT_STOP_WORDS: Readonly<Record<string, ReadonlySet<string>>>;
451
+ /**
452
+ * The meaningful terms of a question, in order, deduplicated, at most `max`: the stop words of the
453
+ * language(s) the question is written in are dropped (the language(s) with the most stop words in
454
+ * it, so "sea"/"son" stay terms in an English question although they are Spanish stop words). A
455
+ * question made only of stop words keeps them all (there is nothing else to search for).
456
+ */
457
+ declare function keywordTerms(query: string, max?: number, stopWords?: Readonly<Record<string, ReadonlySet<string>>>): string[];
458
+ /** Whether the user wrote search syntax (a quoted phrase, `-exclusion`) worth honoring as such. */
459
+ declare function hasSearchSyntax(query: string): boolean;
460
+ /** `'a' | 'b' | …` for `to_tsquery` (terms are letter/digit runs, so quoting is enough). */
461
+ declare function anyTermTsquery(terms: string[]): string;
462
+
435
463
  /**
436
464
  * The minimal Postgres surface {@link import('./pg-vector-store.js').PgVectorStore} needs — adapt your
437
465
  * own `pg` / `postgres.js` client to it, so this package pulls in NO driver (bring your own, like the
@@ -502,11 +530,22 @@ interface PgFullTextOptions {
502
530
  */
503
531
  column?: string;
504
532
  /**
505
- * When the question matches nothing as a whole (`websearch_to_tsquery`: every word must appear),
506
- * retry with any word (`w1 | w2 | …`, at most 24 distinct words) so a long natural-language
507
- * question still finds the chunks sharing its rare terms. Default `true`.
533
+ * Search natural-language questions by their meaningful terms (default `true`): the question's
534
+ * stop words are dropped (see {@link PgFullTextOptions.stopWords}) and rows holding any remaining
535
+ * term (at most 24) are ranked by the sum of the matched terms' IDF among those rows (BM25
536
+ * without term frequencies), `ts_rank_cd` breaking ties; a question with explicit search syntax
537
+ * (a quoted phrase, `-word`) is first tried as written. `false`: only `websearch_to_tsquery` on
538
+ * the query as written (every word must appear).
508
539
  */
509
540
  anyTermFallback?: boolean;
541
+ /**
542
+ * Stop word lists dropped from questions, by language; only the list(s) with the most words in
543
+ * the question apply, so a word that is a stop word in another language stays a term. Default
544
+ * {@link DEFAULT_STOP_WORDS} (English, Portuguese, Spanish: Postgres' Snowball lists). `false`
545
+ * keeps every word. With a language `config` (`english`) Postgres drops that language's stop
546
+ * words as well.
547
+ */
548
+ stopWords?: Readonly<Record<string, ReadonlySet<string>>> | false;
510
549
  }
511
550
  interface PgVectorStoreOptions {
512
551
  /** Table name. Default `agent_rag_chunks`. */
@@ -669,9 +708,10 @@ declare class PgVectorStore implements VectorStore {
669
708
  * `LexicalRetriever` and `HybridRetriever` work on Postgres exactly as on RediSearch — the dense leg
670
709
  * over pgvector, the lexical leg over a `tsvector`, both on the same rows and ids.
671
710
  *
672
- * Ranking is `ts_rank_cd` (cover density). The query is parsed with `websearch_to_tsquery` (quoted
673
- * phrases, `-exclusions`, `or`), which requires every word; when that matches nothing it retries
674
- * with any word (see {@link PgFullTextOptions.anyTermFallback}).
711
+ * A question is searched by its meaningful terms: its stop words dropped, rows holding any remaining
712
+ * term ranked by IDF-weighted coverage, `ts_rank_cd` (cover density) breaking ties (see
713
+ * {@link PgFullTextOptions.anyTermFallback}); explicit syntax (quoted phrases, `-exclusions`) goes
714
+ * through `websearch_to_tsquery` first.
675
715
  *
676
716
  * The `tsvector` is either a column you own ({@link PgFullTextOptions.column}) or the expression
677
717
  * `to_tsvector(config, text)`, for which {@link PgVectorStore.schemaStatements} adds a GIN
@@ -682,12 +722,21 @@ declare class PgLexicalVectorStore extends PgVectorStore implements LexicalVecto
682
722
  private readonly tsConfig;
683
723
  private readonly tsColumn;
684
724
  private readonly anyTermFallback;
725
+ private readonly stopWords;
685
726
  constructor(client: PgClientSource, options?: PgVectorStoreOptions & {
686
727
  fullText?: PgFullTextOptions;
687
728
  });
688
729
  schemaStatements(): string[];
689
730
  searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
731
+ /** `websearch_to_tsquery` on the query as written: every word, quoted phrases, `-exclusions`. */
690
732
  private runTextSearch;
733
+ /**
734
+ * Rows holding any of `terms`, ranked by the sum of the matched terms' IDF among those rows,
735
+ * `ln(1 + (N - df + 0.5) / (df + 0.5))`, then `ts_rank_cd`. Each (row, term) match is evaluated
736
+ * once, and `ts_rank_cd` only for the rows ranked within `topK` (ties included): a question full
737
+ * of common words can match most of the table.
738
+ */
739
+ private runRankedTermSearch;
691
740
  /** Must be character-for-character the index expression, or the planner won't use the index. */
692
741
  private tsvector;
693
742
  }
@@ -1476,4 +1525,4 @@ declare class HttpReranker implements Reranker {
1476
1525
  rerank(query: string, passages: Passage[], options?: RerankOptions): Promise<Passage[]>;
1477
1526
  }
1478
1527
 
1479
- export { type ChunkOptions, type ChunkRecord, type DescribesRetrieval, type DrizzleLike, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryVectorStore, type MetadataPatch, type NodePgQueryable, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_CHANNEL, RAG_RETRIEVAL_EVENT, type RagRetrievalEvent, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalDescriptor, type RetrievalTool, type RetrievalToolOptions, type RetrieverKind, type StoredChunk, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, applyMetadataPatch, chunkDocuments, chunkIndexOf, chunkText, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, documentIdOf, emitRagRetrieval, ingestChunks, ingestDocuments, instrumentRetriever, isLexicalVectorStore, openAiEmbeddings, toPgClient };
1528
+ export { type ChunkOptions, type ChunkRecord, DEFAULT_STOP_WORDS, type DescribesRetrieval, type DrizzleLike, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryVectorStore, type MetadataPatch, type NodePgQueryable, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_CHANNEL, RAG_RETRIEVAL_EVENT, type RagRetrievalEvent, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalDescriptor, type RetrievalTool, type RetrievalToolOptions, type RetrieverKind, type StoredChunk, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, anyTermTsquery, applyMetadataPatch, chunkDocuments, chunkIndexOf, chunkText, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, documentIdOf, emitRagRetrieval, hasSearchSyntax, ingestChunks, ingestDocuments, instrumentRetriever, isLexicalVectorStore, keywordTerms, openAiEmbeddings, toPgClient };
package/dist/index.d.ts CHANGED
@@ -432,6 +432,34 @@ declare class MemoryVectorStore implements VectorStore {
432
432
  search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
433
433
  }
434
434
 
435
+ /**
436
+ * Turning a natural-language question into full-text search terms, for the lexical leg of
437
+ * `PgLexicalVectorStore`.
438
+ *
439
+ * With the `simple` text-search configuration (no stemming, no stop words; any language) a question
440
+ * searched as is either matches nothing (every word required) or, as "any word", ranks chunks full
441
+ * of "the / of / which / de / que" first. So the query side drops the stop words of the question's
442
+ * language(s) and keeps the meaningful terms, which the store then ranks by how many of them a chunk
443
+ * holds, rare terms weighing more (an IDF over the matching rows, BM25-style).
444
+ */
445
+ /**
446
+ * Postgres' Snowball stop word lists (`tsearch_data/{english,portuguese,spanish}.stop`), minus
447
+ * one-letter words (never terms here) and words that carry meaning in a sibling language
448
+ * ("estado", "sentido").
449
+ */
450
+ declare const DEFAULT_STOP_WORDS: Readonly<Record<string, ReadonlySet<string>>>;
451
+ /**
452
+ * The meaningful terms of a question, in order, deduplicated, at most `max`: the stop words of the
453
+ * language(s) the question is written in are dropped (the language(s) with the most stop words in
454
+ * it, so "sea"/"son" stay terms in an English question although they are Spanish stop words). A
455
+ * question made only of stop words keeps them all (there is nothing else to search for).
456
+ */
457
+ declare function keywordTerms(query: string, max?: number, stopWords?: Readonly<Record<string, ReadonlySet<string>>>): string[];
458
+ /** Whether the user wrote search syntax (a quoted phrase, `-exclusion`) worth honoring as such. */
459
+ declare function hasSearchSyntax(query: string): boolean;
460
+ /** `'a' | 'b' | …` for `to_tsquery` (terms are letter/digit runs, so quoting is enough). */
461
+ declare function anyTermTsquery(terms: string[]): string;
462
+
435
463
  /**
436
464
  * The minimal Postgres surface {@link import('./pg-vector-store.js').PgVectorStore} needs — adapt your
437
465
  * own `pg` / `postgres.js` client to it, so this package pulls in NO driver (bring your own, like the
@@ -502,11 +530,22 @@ interface PgFullTextOptions {
502
530
  */
503
531
  column?: string;
504
532
  /**
505
- * When the question matches nothing as a whole (`websearch_to_tsquery`: every word must appear),
506
- * retry with any word (`w1 | w2 | …`, at most 24 distinct words) so a long natural-language
507
- * question still finds the chunks sharing its rare terms. Default `true`.
533
+ * Search natural-language questions by their meaningful terms (default `true`): the question's
534
+ * stop words are dropped (see {@link PgFullTextOptions.stopWords}) and rows holding any remaining
535
+ * term (at most 24) are ranked by the sum of the matched terms' IDF among those rows (BM25
536
+ * without term frequencies), `ts_rank_cd` breaking ties; a question with explicit search syntax
537
+ * (a quoted phrase, `-word`) is first tried as written. `false`: only `websearch_to_tsquery` on
538
+ * the query as written (every word must appear).
508
539
  */
509
540
  anyTermFallback?: boolean;
541
+ /**
542
+ * Stop word lists dropped from questions, by language; only the list(s) with the most words in
543
+ * the question apply, so a word that is a stop word in another language stays a term. Default
544
+ * {@link DEFAULT_STOP_WORDS} (English, Portuguese, Spanish: Postgres' Snowball lists). `false`
545
+ * keeps every word. With a language `config` (`english`) Postgres drops that language's stop
546
+ * words as well.
547
+ */
548
+ stopWords?: Readonly<Record<string, ReadonlySet<string>>> | false;
510
549
  }
511
550
  interface PgVectorStoreOptions {
512
551
  /** Table name. Default `agent_rag_chunks`. */
@@ -669,9 +708,10 @@ declare class PgVectorStore implements VectorStore {
669
708
  * `LexicalRetriever` and `HybridRetriever` work on Postgres exactly as on RediSearch — the dense leg
670
709
  * over pgvector, the lexical leg over a `tsvector`, both on the same rows and ids.
671
710
  *
672
- * Ranking is `ts_rank_cd` (cover density). The query is parsed with `websearch_to_tsquery` (quoted
673
- * phrases, `-exclusions`, `or`), which requires every word; when that matches nothing it retries
674
- * with any word (see {@link PgFullTextOptions.anyTermFallback}).
711
+ * A question is searched by its meaningful terms: its stop words dropped, rows holding any remaining
712
+ * term ranked by IDF-weighted coverage, `ts_rank_cd` (cover density) breaking ties (see
713
+ * {@link PgFullTextOptions.anyTermFallback}); explicit syntax (quoted phrases, `-exclusions`) goes
714
+ * through `websearch_to_tsquery` first.
675
715
  *
676
716
  * The `tsvector` is either a column you own ({@link PgFullTextOptions.column}) or the expression
677
717
  * `to_tsvector(config, text)`, for which {@link PgVectorStore.schemaStatements} adds a GIN
@@ -682,12 +722,21 @@ declare class PgLexicalVectorStore extends PgVectorStore implements LexicalVecto
682
722
  private readonly tsConfig;
683
723
  private readonly tsColumn;
684
724
  private readonly anyTermFallback;
725
+ private readonly stopWords;
685
726
  constructor(client: PgClientSource, options?: PgVectorStoreOptions & {
686
727
  fullText?: PgFullTextOptions;
687
728
  });
688
729
  schemaStatements(): string[];
689
730
  searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
731
+ /** `websearch_to_tsquery` on the query as written: every word, quoted phrases, `-exclusions`. */
690
732
  private runTextSearch;
733
+ /**
734
+ * Rows holding any of `terms`, ranked by the sum of the matched terms' IDF among those rows,
735
+ * `ln(1 + (N - df + 0.5) / (df + 0.5))`, then `ts_rank_cd`. Each (row, term) match is evaluated
736
+ * once, and `ts_rank_cd` only for the rows ranked within `topK` (ties included): a question full
737
+ * of common words can match most of the table.
738
+ */
739
+ private runRankedTermSearch;
691
740
  /** Must be character-for-character the index expression, or the planner won't use the index. */
692
741
  private tsvector;
693
742
  }
@@ -1476,4 +1525,4 @@ declare class HttpReranker implements Reranker {
1476
1525
  rerank(query: string, passages: Passage[], options?: RerankOptions): Promise<Passage[]>;
1477
1526
  }
1478
1527
 
1479
- export { type ChunkOptions, type ChunkRecord, type DescribesRetrieval, type DrizzleLike, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryVectorStore, type MetadataPatch, type NodePgQueryable, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_CHANNEL, RAG_RETRIEVAL_EVENT, type RagRetrievalEvent, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalDescriptor, type RetrievalTool, type RetrievalToolOptions, type RetrieverKind, type StoredChunk, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, applyMetadataPatch, chunkDocuments, chunkIndexOf, chunkText, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, documentIdOf, emitRagRetrieval, ingestChunks, ingestDocuments, instrumentRetriever, isLexicalVectorStore, openAiEmbeddings, toPgClient };
1528
+ export { type ChunkOptions, type ChunkRecord, DEFAULT_STOP_WORDS, type DescribesRetrieval, type DrizzleLike, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HttpModelError, HttpReranker, type HttpRerankerOptions, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, type InstrumentRetrieverOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryVectorStore, type MetadataPatch, type NodePgQueryable, type OpenAiEmbeddingsOptions, type PgClient, type PgClientSource, type PgFullTextOptions, PgLexicalVectorStore, PgVectorStore, type PgVectorStoreOptions, type PostgresJsSql, RAG_DIAGNOSTIC_LIB, RAG_RETRIEVAL_CHANNEL, RAG_RETRIEVAL_EVENT, type RagRetrievalEvent, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalDescriptor, type RetrievalTool, type RetrievalToolOptions, type RetrieverKind, type StoredChunk, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, type VectorStoreKind, anyTermTsquery, applyMetadataPatch, chunkDocuments, chunkIndexOf, chunkText, createRetrievalTool, describeRetrieval, describeSharedSource, describeSource, describesRetrieval, documentIdOf, emitRagRetrieval, hasSearchSyntax, ingestChunks, ingestDocuments, instrumentRetriever, isLexicalVectorStore, keywordTerms, openAiEmbeddings, toPgClient };
package/dist/index.js CHANGED
@@ -356,6 +356,81 @@ 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
+
359
434
  // src/pg-client.ts
360
435
  var ADAPTED = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-rag/pg-client");
361
436
  function toPgClient(source) {
@@ -814,6 +889,7 @@ var PgLexicalVectorStore = class extends PgVectorStore {
814
889
  tsConfig;
815
890
  tsColumn;
816
891
  anyTermFallback;
892
+ stopWords;
817
893
  constructor(client, options = {}) {
818
894
  super(client, options);
819
895
  const config = options.fullText?.config ?? "simple";
@@ -827,6 +903,8 @@ var PgLexicalVectorStore = class extends PgVectorStore {
827
903
  this.tsConfig = config;
828
904
  this.tsColumn = column;
829
905
  this.anyTermFallback = options.fullText?.anyTermFallback ?? true;
906
+ const stopWords = options.fullText?.stopWords;
907
+ this.stopWords = stopWords === false ? {} : stopWords ?? DEFAULT_STOP_WORDS;
830
908
  }
831
909
  schemaStatements() {
832
910
  const statements = super.schemaStatements();
@@ -840,19 +918,18 @@ var PgLexicalVectorStore = class extends PgVectorStore {
840
918
  if (filterMatchesNothing(options.filter) || query.trim() === "") {
841
919
  return [];
842
920
  }
843
- const all = await this.runTextSearch(`websearch_to_tsquery('${this.tsConfig}', $1)`, query, options);
844
- if (all.length > 0 || !this.anyTermFallback) {
845
- return all;
846
- }
847
- const terms = [
848
- ...new Set(query.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [])
849
- ].slice(0, 24);
850
- if (terms.length < 2) {
851
- return all;
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
+ }
852
926
  }
853
- return this.runTextSearch(`to_tsquery('${this.tsConfig}', $1)`, terms.map((term) => `'${term}'`).join(" | "), options);
927
+ const terms = keywordTerms(query, 24, this.stopWords);
928
+ return terms.length === 0 ? [] : this.runRankedTermSearch(terms, options);
854
929
  }
855
- async runTextSearch(tsquery, query, options) {
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)`;
856
933
  const params = [
857
934
  query,
858
935
  options.topK
@@ -868,6 +945,47 @@ var PgLexicalVectorStore = class extends PgVectorStore {
868
945
  LIMIT $2`, params);
869
946
  return rows.map(toPassage);
870
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),
958
+ options.topK,
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
+ }
871
989
  /** Must be character-for-character the index expression, or the planner won't use the index. */
872
990
  tsvector() {
873
991
  return this.tsColumn ?? `to_tsvector('${this.tsConfig}'::regconfig, text)`;
@@ -2488,6 +2606,7 @@ function itemsOf(body) {
2488
2606
  }
2489
2607
  __name(itemsOf, "itemsOf");
2490
2608
  export {
2609
+ DEFAULT_STOP_WORDS,
2491
2610
  EmbeddingRetriever,
2492
2611
  FilteredRetriever,
2493
2612
  HttpModelError,
@@ -2505,6 +2624,7 @@ export {
2505
2624
  RedisVectorStore,
2506
2625
  RerankingRetriever,
2507
2626
  UnsafeRemovalError,
2627
+ anyTermTsquery,
2508
2628
  applyMetadataPatch,
2509
2629
  chunkDocuments,
2510
2630
  chunkIndexOf,
@@ -2516,10 +2636,12 @@ export {
2516
2636
  describesRetrieval,
2517
2637
  documentIdOf,
2518
2638
  emitRagRetrieval,
2639
+ hasSearchSyntax,
2519
2640
  ingestChunks,
2520
2641
  ingestDocuments,
2521
2642
  instrumentRetriever,
2522
2643
  isLexicalVectorStore,
2644
+ keywordTerms,
2523
2645
  openAiEmbeddings,
2524
2646
  toPgClient
2525
2647
  };