@dudousxd/nestjs-agent-rag 0.7.0 → 0.8.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 +46 -2
- package/dist/index.cjs +37 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -29,6 +29,48 @@ await ingestDocuments(
|
|
|
29
29
|
const retriever = new EmbeddingRetriever(embedder, store);
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## Chunking record-shaped text
|
|
33
|
+
|
|
34
|
+
The default chunker is structure-blind: it fills a `chunkSize` window, breaks at the latest
|
|
35
|
+
paragraph → sentence → word boundary inside it, and carries `overlap` characters across the seam.
|
|
36
|
+
That is the right rule for prose — the two halves are still sentences, and the overlap rescues the
|
|
37
|
+
one that got cut.
|
|
38
|
+
|
|
39
|
+
It is the wrong rule for text whose boundaries mean something. A spreadsheet flattened to one
|
|
40
|
+
field-labelled record per row has real boundaries the chunker does not know exist, so a cut lands
|
|
41
|
+
mid-record routinely — and the half holding the row identifier and the half holding the value end up
|
|
42
|
+
in different chunks, where **neither can answer a question about that row**. Tell it where the
|
|
43
|
+
records are:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
await ingestDocuments([{ id: 'mvr-2026', text: rows.join('\n') }], {
|
|
47
|
+
embedder,
|
|
48
|
+
store,
|
|
49
|
+
chunkSize: 800,
|
|
50
|
+
separator: '\n', // one record per line; a record is never split across chunks
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Records are packed greedily up to `chunkSize` and never split — except one longer than `chunkSize`,
|
|
55
|
+
which has nowhere to go and is character-split as usual. That fallback is confined to the single
|
|
56
|
+
record that could not fit, rather than applied to the whole document.
|
|
57
|
+
|
|
58
|
+
**`overlap` is ignored when `separator` is set**, deliberately. Overlap exists to rescue a sentence a
|
|
59
|
+
boundary cut in half; a boundary that never falls inside a record has nothing to rescue, and carrying
|
|
60
|
+
the previous chunk's tail in would duplicate whole records into their neighbours — paying for the
|
|
61
|
+
same rows twice at embedding time and letting one row match from two places.
|
|
62
|
+
|
|
63
|
+
Measured on a 200-row, 15-column sheet ingested twice — same bytes, same embedder, same questions,
|
|
64
|
+
only the cut positions differing — blind chunking left 27% of records split, and on questions
|
|
65
|
+
targeting a field on the far side of such a cut, BM25 answered 54/66 (MRR 0.818) against 66/66
|
|
66
|
+
(MRR 1.000) with records kept whole. The dense leg scored 7/66 either way: 200 near-identical rows
|
|
67
|
+
produce near-identical vectors, so that leg cannot do row lookup at all and chunking does not change
|
|
68
|
+
it. The win is the lexical leg's, and it only shows up on questions whose answer sits far from the
|
|
69
|
+
row's rare identifying token — ask about a field next to the vehicle id and both arms score 1.000,
|
|
70
|
+
because BM25 finds that token whatever the chunking did around it.
|
|
71
|
+
|
|
72
|
+
Leave `separator` unset for prose. Nothing about the default path changed.
|
|
73
|
+
|
|
32
74
|
## Two ways to use it with the agent
|
|
33
75
|
|
|
34
76
|
**Agentic (default) — the model decides when to search:**
|
|
@@ -268,8 +310,10 @@ library upgrade. The interface is open, so it can be added later without changin
|
|
|
268
310
|
|
|
269
311
|
## API
|
|
270
312
|
|
|
271
|
-
- `chunkText(text, { chunkSize?, overlap? })` — overlapping, boundary-aware chunks
|
|
272
|
-
|
|
313
|
+
- `chunkText(text, { chunkSize?, overlap?, separator? })` — overlapping, boundary-aware chunks;
|
|
314
|
+
`separator` packs whole records instead and never splits one (`overlap` is ignored in that mode).
|
|
315
|
+
- `ingestDocuments(docs, { embedder, store, chunkSize?, overlap?, separator? })` — chunk → embed →
|
|
316
|
+
upsert.
|
|
273
317
|
- `EmbeddingRetriever(embedder, store, { minScore? })` — the core `Retriever` from an embedder +
|
|
274
318
|
vector store. `minScore` drops passages below a similarity floor, so an unanswerable query can
|
|
275
319
|
return nothing.
|
package/dist/index.cjs
CHANGED
|
@@ -51,6 +51,9 @@ function chunkText(text, options = {}) {
|
|
|
51
51
|
if (normalized.length === 0) {
|
|
52
52
|
return [];
|
|
53
53
|
}
|
|
54
|
+
if (options.separator !== void 0 && options.separator.length > 0) {
|
|
55
|
+
return chunkByRecord(normalized, chunkSize, options.separator);
|
|
56
|
+
}
|
|
54
57
|
if (normalized.length <= chunkSize) {
|
|
55
58
|
return [
|
|
56
59
|
normalized
|
|
@@ -80,6 +83,40 @@ function chunkText(text, options = {}) {
|
|
|
80
83
|
return chunks;
|
|
81
84
|
}
|
|
82
85
|
__name(chunkText, "chunkText");
|
|
86
|
+
function chunkByRecord(text, chunkSize, separator) {
|
|
87
|
+
const chunks = [];
|
|
88
|
+
let current = "";
|
|
89
|
+
const flush = /* @__PURE__ */ __name(() => {
|
|
90
|
+
if (current.length > 0) {
|
|
91
|
+
chunks.push(current);
|
|
92
|
+
current = "";
|
|
93
|
+
}
|
|
94
|
+
}, "flush");
|
|
95
|
+
for (const record of text.split(separator)) {
|
|
96
|
+
const trimmed = record.trim();
|
|
97
|
+
if (trimmed.length === 0) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (trimmed.length > chunkSize) {
|
|
101
|
+
flush();
|
|
102
|
+
chunks.push(...chunkText(trimmed, {
|
|
103
|
+
chunkSize,
|
|
104
|
+
overlap: 0
|
|
105
|
+
}));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const candidate = current.length > 0 ? `${current}${separator}${trimmed}` : trimmed;
|
|
109
|
+
if (candidate.length > chunkSize) {
|
|
110
|
+
flush();
|
|
111
|
+
current = trimmed;
|
|
112
|
+
} else {
|
|
113
|
+
current = candidate;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
flush();
|
|
117
|
+
return chunks;
|
|
118
|
+
}
|
|
119
|
+
__name(chunkByRecord, "chunkByRecord");
|
|
83
120
|
|
|
84
121
|
// src/vector-store.ts
|
|
85
122
|
function isLexicalVectorStore(store) {
|