@dudousxd/nestjs-agent-rag 0.6.0 → 0.7.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
@@ -595,6 +595,29 @@ declare class RedisVectorStore implements LexicalVectorStore {
595
595
  searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
596
596
  }
597
597
 
598
+ interface EmbeddingRetrieverOptions {
599
+ /**
600
+ * Drop passages scoring below this, so a query the corpus cannot answer returns nothing.
601
+ *
602
+ * A vector search always returns its `topK` nearest neighbours, and "nearest" is not "relevant".
603
+ * Ask a corpus of maintenance policy about the dining hall menu and it hands back the five least
604
+ * unrelated paragraphs it has, each with a score, in confident rank order. Nothing in that result
605
+ * says the answer is absent, so without a floor the consumer either feeds a model five irrelevant
606
+ * passages or invents a threshold of its own.
607
+ *
608
+ * **This is the only place in the package where a threshold is meaningful, which is why it lives
609
+ * here and not on `HybridRetriever`.** The score here comes straight from the store's vector
610
+ * search: a calibrated similarity, comparable across queries and monotonic in how well the passage
611
+ * matches. A fused score is neither — see the score section on `HybridRetriever`. Threshold each
612
+ * leg, then fuse; never the other way round.
613
+ *
614
+ * Pick the number by measuring, not by taste: score questions you know the corpus answers against
615
+ * questions you know it does not, and put the floor in the gap between the two populations. It is
616
+ * specific to the embedding model and the distance metric, so it does not survive changing either.
617
+ * Left unset nothing is dropped, which is the previous behaviour exactly.
618
+ */
619
+ minScore?: number;
620
+ }
598
621
  /**
599
622
  * Bridges an {@link EmbeddingProvider} + {@link VectorStore} into the core {@link Retriever} SPI:
600
623
  * embed the query, then vector-search. This is what you wire as the agent's retriever (as a tool
@@ -603,7 +626,8 @@ declare class RedisVectorStore implements LexicalVectorStore {
603
626
  declare class EmbeddingRetriever implements Retriever {
604
627
  private readonly embedder;
605
628
  private readonly store;
606
- constructor(embedder: EmbeddingProvider, store: VectorStore);
629
+ private readonly options;
630
+ constructor(embedder: EmbeddingProvider, store: VectorStore, options?: EmbeddingRetrieverOptions);
607
631
  retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
608
632
  }
609
633
 
@@ -724,6 +748,36 @@ interface HybridRetrieverOptions {
724
748
  * vector search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the
725
749
  * lists it appears in, so it needs no score normalization between incompatible scales (cosine vs
726
750
  * BM25). Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce.
751
+ *
752
+ * ## The score on a fused passage is not a similarity. Do not threshold it.
753
+ *
754
+ * Read this before using `passage.score` for anything other than the order it already produced.
755
+ *
756
+ * Not normalizing between scales is what makes RRF robust, and the price is that the number it
757
+ * emits is a function of **rank alone** — it never sees a similarity at all. Two consequences that
758
+ * bite in practice:
759
+ *
760
+ * - **It is not comparable to the score on a passage from {@link EmbeddingRetriever}**, even though
761
+ * it arrives in the same field, of the same type, from the same `Retriever` interface. There is no
762
+ * discriminator; a consumer holding a `Passage[]` cannot tell which kind of number it has. If you
763
+ * surface scores to a user or a model, know which retriever produced them.
764
+ * - **Its range is narrow, fixed, and says nothing about quality.** Every possible score lies in
765
+ * `[minWeight / (k + fetchTopK), Σ weights / (k + 1)]` — with the defaults (`k = 60`,
766
+ * `fetchTopK = 20`, two unweighted legs) that is `[0.0125, 0.0328]`. A perfect match and the
767
+ * least-bad member of a set of terrible matches both come back around `0.016`.
768
+ *
769
+ * The second point is why a relevance floor cannot live here. What the fused score actually measures
770
+ * is how strongly the legs *agree* — and agreement tracks nothing about correctness. When both legs
771
+ * return the same nearest nothing for an unanswerable query, they reinforce it to the ceiling, the
772
+ * same ceiling a unanimous correct answer gets. Measured on one corpus, the dense leg alone
773
+ * separated cleanly (weakest real answer 0.244, loudest absent-topic answer 0.143) while those same
774
+ * queries after fusion scored 0.032 and 0.033 — inverted. On another corpus the inversion may not
775
+ * appear; what does not change is that both numbers live in a band roughly one part in fifty wide,
776
+ * with no room for a threshold to mean anything.
777
+ *
778
+ * So: put the floor on the legs, with {@link EmbeddingRetrieverOptions.minScore}, and fuse what
779
+ * survives. `HybridRetriever` deliberately has no `minScore` of its own; a threshold option here
780
+ * would be an invitation to apply it to a number that cannot support one.
727
781
  */
728
782
  declare class HybridRetriever implements Retriever {
729
783
  private readonly retrievers;
@@ -789,4 +843,4 @@ interface RetrievalTool {
789
843
  */
790
844
  declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
791
845
 
792
- export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
846
+ export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
package/dist/index.d.ts CHANGED
@@ -595,6 +595,29 @@ declare class RedisVectorStore implements LexicalVectorStore {
595
595
  searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
596
596
  }
597
597
 
598
+ interface EmbeddingRetrieverOptions {
599
+ /**
600
+ * Drop passages scoring below this, so a query the corpus cannot answer returns nothing.
601
+ *
602
+ * A vector search always returns its `topK` nearest neighbours, and "nearest" is not "relevant".
603
+ * Ask a corpus of maintenance policy about the dining hall menu and it hands back the five least
604
+ * unrelated paragraphs it has, each with a score, in confident rank order. Nothing in that result
605
+ * says the answer is absent, so without a floor the consumer either feeds a model five irrelevant
606
+ * passages or invents a threshold of its own.
607
+ *
608
+ * **This is the only place in the package where a threshold is meaningful, which is why it lives
609
+ * here and not on `HybridRetriever`.** The score here comes straight from the store's vector
610
+ * search: a calibrated similarity, comparable across queries and monotonic in how well the passage
611
+ * matches. A fused score is neither — see the score section on `HybridRetriever`. Threshold each
612
+ * leg, then fuse; never the other way round.
613
+ *
614
+ * Pick the number by measuring, not by taste: score questions you know the corpus answers against
615
+ * questions you know it does not, and put the floor in the gap between the two populations. It is
616
+ * specific to the embedding model and the distance metric, so it does not survive changing either.
617
+ * Left unset nothing is dropped, which is the previous behaviour exactly.
618
+ */
619
+ minScore?: number;
620
+ }
598
621
  /**
599
622
  * Bridges an {@link EmbeddingProvider} + {@link VectorStore} into the core {@link Retriever} SPI:
600
623
  * embed the query, then vector-search. This is what you wire as the agent's retriever (as a tool
@@ -603,7 +626,8 @@ declare class RedisVectorStore implements LexicalVectorStore {
603
626
  declare class EmbeddingRetriever implements Retriever {
604
627
  private readonly embedder;
605
628
  private readonly store;
606
- constructor(embedder: EmbeddingProvider, store: VectorStore);
629
+ private readonly options;
630
+ constructor(embedder: EmbeddingProvider, store: VectorStore, options?: EmbeddingRetrieverOptions);
607
631
  retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
608
632
  }
609
633
 
@@ -724,6 +748,36 @@ interface HybridRetrieverOptions {
724
748
  * vector search with lexical BM25. RRF scores each passage by `Σ weight / (k + rank)` across the
725
749
  * lists it appears in, so it needs no score normalization between incompatible scales (cosine vs
726
750
  * BM25). Deduplicates by passage id, so aligned chunk ids from the two retrievers reinforce.
751
+ *
752
+ * ## The score on a fused passage is not a similarity. Do not threshold it.
753
+ *
754
+ * Read this before using `passage.score` for anything other than the order it already produced.
755
+ *
756
+ * Not normalizing between scales is what makes RRF robust, and the price is that the number it
757
+ * emits is a function of **rank alone** — it never sees a similarity at all. Two consequences that
758
+ * bite in practice:
759
+ *
760
+ * - **It is not comparable to the score on a passage from {@link EmbeddingRetriever}**, even though
761
+ * it arrives in the same field, of the same type, from the same `Retriever` interface. There is no
762
+ * discriminator; a consumer holding a `Passage[]` cannot tell which kind of number it has. If you
763
+ * surface scores to a user or a model, know which retriever produced them.
764
+ * - **Its range is narrow, fixed, and says nothing about quality.** Every possible score lies in
765
+ * `[minWeight / (k + fetchTopK), Σ weights / (k + 1)]` — with the defaults (`k = 60`,
766
+ * `fetchTopK = 20`, two unweighted legs) that is `[0.0125, 0.0328]`. A perfect match and the
767
+ * least-bad member of a set of terrible matches both come back around `0.016`.
768
+ *
769
+ * The second point is why a relevance floor cannot live here. What the fused score actually measures
770
+ * is how strongly the legs *agree* — and agreement tracks nothing about correctness. When both legs
771
+ * return the same nearest nothing for an unanswerable query, they reinforce it to the ceiling, the
772
+ * same ceiling a unanimous correct answer gets. Measured on one corpus, the dense leg alone
773
+ * separated cleanly (weakest real answer 0.244, loudest absent-topic answer 0.143) while those same
774
+ * queries after fusion scored 0.032 and 0.033 — inverted. On another corpus the inversion may not
775
+ * appear; what does not change is that both numbers live in a band roughly one part in fifty wide,
776
+ * with no room for a threshold to mean anything.
777
+ *
778
+ * So: put the floor on the legs, with {@link EmbeddingRetrieverOptions.minScore}, and fuse what
779
+ * survives. `HybridRetriever` deliberately has no `minScore` of its own; a threshold option here
780
+ * would be an invitation to apply it to a number that cannot support one.
727
781
  */
728
782
  declare class HybridRetriever implements Retriever {
729
783
  private readonly retrievers;
@@ -789,4 +843,4 @@ interface RetrievalTool {
789
843
  */
790
844
  declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
791
845
 
792
- export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
846
+ export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
package/dist/index.js CHANGED
@@ -1295,9 +1295,11 @@ var EmbeddingRetriever = class {
1295
1295
  }
1296
1296
  embedder;
1297
1297
  store;
1298
- constructor(embedder, store) {
1298
+ options;
1299
+ constructor(embedder, store, options = {}) {
1299
1300
  this.embedder = embedder;
1300
1301
  this.store = store;
1302
+ this.options = options;
1301
1303
  }
1302
1304
  async retrieve(query, options = {}) {
1303
1305
  const [embedding] = await this.embedder.embed([
@@ -1306,12 +1308,14 @@ var EmbeddingRetriever = class {
1306
1308
  if (embedding === void 0) {
1307
1309
  return [];
1308
1310
  }
1309
- return this.store.search(embedding, {
1311
+ const passages = await this.store.search(embedding, {
1310
1312
  topK: options.topK ?? 5,
1311
1313
  ...options.filter !== void 0 ? {
1312
1314
  filter: options.filter
1313
1315
  } : {}
1314
1316
  });
1317
+ const { minScore } = this.options;
1318
+ return minScore === void 0 ? passages : passages.filter((passage) => passage.score >= minScore);
1315
1319
  }
1316
1320
  };
1317
1321