@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/README.md CHANGED
@@ -171,6 +171,38 @@ const retriever = new HybridRetriever([
171
171
  ]);
172
172
  ```
173
173
 
174
+ ### The fused score is not a similarity — do not threshold it
175
+
176
+ Rank-based fusion is what makes RRF robust across incompatible scales, and the price is that its
177
+ output is a function of **rank alone**. It never sees a similarity. Two things follow:
178
+
179
+ - The score on a fused passage is **not comparable** to the score on one from `EmbeddingRetriever`,
180
+ even though it arrives in the same field, of the same type, through the same `Retriever` interface.
181
+ There is no discriminator — a consumer holding a `Passage[]` cannot tell which kind of number it
182
+ has. If you show scores to a user or a model, know which retriever produced them.
183
+ - Its range is narrow, fixed and quality-blind: every possible value lies in
184
+ `[minWeight / (k + fetchTopK), Σ weights / (k + 1)]`, which with the defaults is
185
+ `[0.0125, 0.0328]`. A perfect match and the least-bad member of a set of terrible matches both land
186
+ around `0.016`.
187
+
188
+ What a fused score measures is how strongly the legs *agree*, and agreement tracks nothing about
189
+ correctness: two legs returning the same nearest nothing reinforce it to the same ceiling a unanimous
190
+ correct answer gets. Measured on one corpus, the dense leg alone separated cleanly (weakest real
191
+ answer `0.244`, loudest absent-topic answer `0.143`) while those same queries after fusion scored
192
+ `0.032` and `0.033` — inverted.
193
+
194
+ So a relevance floor goes on the legs, never on the fusion:
195
+
196
+ ```ts
197
+ const retriever = new HybridRetriever([
198
+ new EmbeddingRetriever(embedder, store, { minScore: 0.2 }), // ← floor here
199
+ new LexicalRetriever(store),
200
+ ]);
201
+ ```
202
+
203
+ `HybridRetriever` has no `minScore` of its own on purpose: the option would be an invitation to
204
+ apply one to a number that cannot support it.
205
+
174
206
  Nothing else changes: no chunks to hand the retriever, no state in the process, no deletes to mirror.
175
207
  This is the only option that works when **ingestion and search run in different processes** — a
176
208
  worker that ingests and an API pod that searches. The API pod never sees the chunks, so an in-process
@@ -238,7 +270,9 @@ library upgrade. The interface is open, so it can be added later without changin
238
270
 
239
271
  - `chunkText(text, { chunkSize?, overlap? })` — overlapping, boundary-aware chunks.
240
272
  - `ingestDocuments(docs, { embedder, store, chunkSize?, overlap? })` — chunk → embed → upsert.
241
- - `EmbeddingRetriever(embedder, store)` — the core `Retriever` from an embedder + vector store.
273
+ - `EmbeddingRetriever(embedder, store, { minScore? })` — the core `Retriever` from an embedder +
274
+ vector store. `minScore` drops passages below a similarity floor, so an unanswerable query can
275
+ return nothing.
242
276
  - `LexicalRetriever(store)` — the core `Retriever` from a store's own full-text index (BM25).
243
277
  - `KeywordRetriever` — in-process BM25 over chunks you feed it; the single-process lexical half:
244
278
  `add(chunks)`, `remove(documentId)`, `clear()`, `size`.
package/dist/index.cjs CHANGED
@@ -1337,9 +1337,11 @@ var EmbeddingRetriever = class {
1337
1337
  }
1338
1338
  embedder;
1339
1339
  store;
1340
- constructor(embedder, store) {
1340
+ options;
1341
+ constructor(embedder, store, options = {}) {
1341
1342
  this.embedder = embedder;
1342
1343
  this.store = store;
1344
+ this.options = options;
1343
1345
  }
1344
1346
  async retrieve(query, options = {}) {
1345
1347
  const [embedding] = await this.embedder.embed([
@@ -1348,12 +1350,14 @@ var EmbeddingRetriever = class {
1348
1350
  if (embedding === void 0) {
1349
1351
  return [];
1350
1352
  }
1351
- return this.store.search(embedding, {
1353
+ const passages = await this.store.search(embedding, {
1352
1354
  topK: options.topK ?? 5,
1353
1355
  ...options.filter !== void 0 ? {
1354
1356
  filter: options.filter
1355
1357
  } : {}
1356
1358
  });
1359
+ const { minScore } = this.options;
1360
+ return minScore === void 0 ? passages : passages.filter((passage) => passage.score >= minScore);
1357
1361
  }
1358
1362
  };
1359
1363