@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 +35 -1
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -2
- package/dist/index.d.ts +56 -2
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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 +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|