@dudousxd/nestjs-agent-rag 0.10.3 → 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/README.md +94 -14
- package/dist/index.cjs +666 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +336 -23
- package/dist/index.d.ts +336 -23
- package/dist/index.js +657 -73
- package/dist/index.js.map +1 -1
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -29,6 +29,34 @@ await ingestDocuments(
|
|
|
29
29
|
const retriever = new EmbeddingRetriever(embedder, store);
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## Embedding models and rerankers over HTTP
|
|
33
|
+
|
|
34
|
+
No SDK needed for the two HTTP contracts nearly every provider and self-hosted server speaks:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { HttpReranker, RerankingRetriever, openAiEmbeddings } from '@dudousxd/nestjs-agent-rag';
|
|
38
|
+
|
|
39
|
+
// Any OpenAI-compatible POST /v1/embeddings: OpenAI, a gateway, Ollama, TEI, vLLM, LocalAI…
|
|
40
|
+
const embedder = openAiEmbeddings({
|
|
41
|
+
baseUrl: 'http://tei:8080/v1', // default https://api.openai.com/v1
|
|
42
|
+
model: 'BAAI/bge-m3',
|
|
43
|
+
apiKey: process.env.EMBEDDINGS_API_KEY, // optional
|
|
44
|
+
onUsage: (tokens) => meter.add(tokens), // optional cost accounting
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Cohere / Jina / Voyage / TEI-style POST /rerank
|
|
48
|
+
const reranker = new HttpReranker({
|
|
49
|
+
url: 'https://api.cohere.com/v2/rerank',
|
|
50
|
+
model: 'rerank-v3.5',
|
|
51
|
+
apiKey: process.env.COHERE_API_KEY,
|
|
52
|
+
});
|
|
53
|
+
const precise = new RerankingRetriever(retriever, reranker, { fetchTopK: 30 });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Both throw an `HttpModelError` (with the HTTP `status` and the provider's message) on failure. For
|
|
57
|
+
offline tests, `hashedEmbeddings(dimensions)` from `@dudousxd/nestjs-agent-testing` is a
|
|
58
|
+
deterministic hashed bag-of-words embedder.
|
|
59
|
+
|
|
32
60
|
## Chunking record-shaped text
|
|
33
61
|
|
|
34
62
|
The default chunker is structure-blind: it fills a `chunkSize` window, breaks at the latest
|
|
@@ -153,20 +181,65 @@ outlive a restart persists it from there.
|
|
|
153
181
|
|
|
154
182
|
## pgvector store
|
|
155
183
|
|
|
156
|
-
`PgVectorStore` takes
|
|
184
|
+
`PgVectorStore` takes a `pg` `Pool`, a Drizzle database (`drizzle(pool)` or `drizzle(sql)` — no
|
|
185
|
+
`$client` cast), a `postgres.js` `sql`, or a hand-written `PgClient` (`{ query(sql, params) → rows }`).
|
|
186
|
+
The package still pulls in no driver; `toPgClient` does the adapting and is exported if you need it.
|
|
157
187
|
|
|
158
188
|
```ts
|
|
159
189
|
import { Pool } from 'pg';
|
|
160
190
|
import { PgVectorStore } from '@dudousxd/nestjs-agent-rag';
|
|
161
191
|
|
|
162
192
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
163
|
-
const store = new PgVectorStore(
|
|
164
|
-
{ query: (sql, params) => pool.query(sql, params).then((r) => r.rows) },
|
|
165
|
-
{ dimensions: 1536 },
|
|
166
|
-
);
|
|
193
|
+
const store = new PgVectorStore(pool, { dimensions: 1536 }); // or new PgVectorStore(db) with Drizzle
|
|
167
194
|
await store.ensureSchema(); // CREATE EXTENSION vector + table + HNSW cosine index
|
|
168
195
|
```
|
|
169
196
|
|
|
197
|
+
`store.schemaStatements()` returns the same DDL as strings, for a migration instead of a boot-time
|
|
198
|
+
side effect. Other options (all off by default, so existing tables behave exactly as before):
|
|
199
|
+
|
|
200
|
+
- **`upsertBatchSize`** (default 100) — `upsert` writes multi-row `INSERT … ON CONFLICT` statements;
|
|
201
|
+
duplicate ids in one call collapse to the last occurrence. NUL bytes are stripped from every
|
|
202
|
+
text/jsonb binding (Postgres rejects them).
|
|
203
|
+
- **`nullableEmbeddings: true`** — `embedding: []` is stored as `NULL`: index text now, embed later,
|
|
204
|
+
or keep full-text-only chunks. `search` skips them; the lexical leg below still finds them.
|
|
205
|
+
- **`dimensions: [768, 1536]`** — mixed widths in one table (say, while migrating models). The column
|
|
206
|
+
becomes an untyped `vector` with one partial HNSW index per width
|
|
207
|
+
(`USING hnsw ((embedding::vector(768)) vector_cosine_ops) WHERE vector_dims(embedding) = 768`), and
|
|
208
|
+
`search` only compares vectors as wide as the query. Put the model in the chunk metadata and filter
|
|
209
|
+
on it when two models share a width.
|
|
210
|
+
- **`iterativeScan: 'strict_order' | 'relaxed_order'`**, **`efSearch`** — pgvector ≥ 0.8 keeps
|
|
211
|
+
walking the HNSW graph until `topK` rows pass a selective filter (one tenant of many), instead of
|
|
212
|
+
returning short. Applied with `SET LOCAL` in a transaction, so it needs a client that can run one
|
|
213
|
+
(Pool, Drizzle and postgres.js can; a hand-written `PgClient` can add `transaction`). Skipped on
|
|
214
|
+
an older pgvector.
|
|
215
|
+
|
|
216
|
+
Every filtered statement goes through one protected method, `whereConditions(filter, params)`, so a
|
|
217
|
+
subclass that maps its filter onto its own columns (a tenant id, an ACL) overrides just that.
|
|
218
|
+
|
|
219
|
+
### Full-text search on Postgres
|
|
220
|
+
|
|
221
|
+
`PgLexicalVectorStore` is the same store plus `searchText` — a `LexicalVectorStore`, so
|
|
222
|
+
`LexicalRetriever` and `HybridRetriever` work on Postgres as they do on RediSearch:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { PgLexicalVectorStore } from '@dudousxd/nestjs-agent-rag';
|
|
226
|
+
|
|
227
|
+
const store = new PgLexicalVectorStore(db, {
|
|
228
|
+
dimensions: 1536,
|
|
229
|
+
fullText: { config: 'simple' }, // or { column: 'tsv' } for a tsvector column you maintain
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
A question is searched by its meaningful terms: its stop words are dropped (English, Portuguese and
|
|
234
|
+
Spanish lists by default, only the language(s) the question is written in; `stopWords` to change or
|
|
235
|
+
`false` to keep every word), and rows holding any remaining term are ranked by the sum of the
|
|
236
|
+
matched terms' IDF (BM25 without term frequencies), `ts_rank_cd` breaking ties, so three rare terms
|
|
237
|
+
beat a row full of "the/of/which". Quoted phrases and `-word` go through `websearch_to_tsquery` as
|
|
238
|
+
written first; `anyTermFallback: false` keeps only that (every word must appear). `keywordTerms()`
|
|
239
|
+
is exported for other stores. Without `column`, `ensureSchema`/`schemaStatements` add a GIN index over
|
|
240
|
+
`to_tsvector(config, text)` — the exact expression the query uses. On a populated table create it
|
|
241
|
+
`CONCURRENTLY` in a migration rather than at boot (see below).
|
|
242
|
+
|
|
170
243
|
## RediSearch store
|
|
171
244
|
|
|
172
245
|
`RedisVectorStore` takes anything that can send a raw command — `node-redis`, or `ioredis` wrapped —
|
|
@@ -354,14 +427,17 @@ const lexical = isLexicalVectorStore(store) ? new LexicalRetriever(store) : keyw
|
|
|
354
427
|
| Store | Lexical | Why |
|
|
355
428
|
| --- | --- | --- |
|
|
356
429
|
| `RedisVectorStore` | ✅ | RediSearch indexes the chunk text as `TEXT`; BM25 is already there |
|
|
357
|
-
| `
|
|
430
|
+
| `PgLexicalVectorStore` | ✅ | Postgres full-text search over a GIN-indexed `tsvector` you opt into |
|
|
431
|
+
| `PgVectorStore` | ❌ | the plain chunks table has no `tsvector`/GIN index; use the subclass above |
|
|
358
432
|
| `MemoryVectorStore` | ❌ | single-process by definition — use `KeywordRetriever` |
|
|
359
433
|
|
|
360
|
-
Postgres is a
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
434
|
+
On Postgres it is a separate class rather than a new method on `PgVectorStore`, deliberately.
|
|
435
|
+
Lexical search there needs new DDL (a GIN index), and `CREATE INDEX` in `ensureSchema` would take a
|
|
436
|
+
write lock on an already-populated chunks table at boot; it also forces a text-search-configuration
|
|
437
|
+
choice that must match between index and query or Postgres silently falls back to a sequential scan.
|
|
438
|
+
Had `PgVectorStore` simply grown `searchText`, every `isLexicalVectorStore(store)` check would have
|
|
439
|
+
started using it against an unindexed table on upgrade. Constructing `PgLexicalVectorStore` is the
|
|
440
|
+
opt-in; the index is a migration you run and watch (`schemaStatements()` prints it).
|
|
365
441
|
|
|
366
442
|
## API
|
|
367
443
|
|
|
@@ -376,7 +452,7 @@ library upgrade. The interface is open, so it can be added later without changin
|
|
|
376
452
|
- `KeywordRetriever` — in-process BM25 over chunks you feed it; the single-process lexical half:
|
|
377
453
|
`add(chunks)`, `remove(documentId)`, `clear()`, `size`.
|
|
378
454
|
- `HybridRetriever(retrievers, { k?, fetchTopK?, weights? })` — RRF fusion of the two.
|
|
379
|
-
- `MemoryVectorStore` / `PgVectorStore` / `RedisVectorStore` — `VectorStore` adapters:
|
|
455
|
+
- `MemoryVectorStore` / `PgVectorStore` / `PgLexicalVectorStore` / `RedisVectorStore` — `VectorStore` adapters:
|
|
380
456
|
`upsert(records)`, `search(embedding, opts)`, `remove(documentId)`,
|
|
381
457
|
`updateMetadata(documentId, patch)`, `listDocuments(filter?)`,
|
|
382
458
|
`listChunks(documentId, { limit?, offset? })`.
|
|
@@ -387,6 +463,10 @@ library upgrade. The interface is open, so it can be added later without changin
|
|
|
387
463
|
agentic-retrieval tool. `telemetry` defaults to on; pass `false` to opt out.
|
|
388
464
|
- `instrumentRetriever(retriever, { describe?, collection? })` — wrap any `Retriever` so each
|
|
389
465
|
retrieval publishes on `aviary:rag:retrieval` (`RAG_RETRIEVAL_CHANNEL`).
|
|
466
|
+
- `openAiEmbeddings({ model, baseUrl?, apiKey?, dimensions?, batchSize?, maxInputChars?, headers?,
|
|
467
|
+
timeoutMs?, onUsage?, fetch? })` — an `EmbeddingProvider` over any OpenAI-compatible `/embeddings`.
|
|
468
|
+
- `HttpReranker({ url, model?, apiKey?, format?, headers?, timeoutMs?, fetch? })` — a `Reranker` over
|
|
469
|
+
a Cohere/Jina/Voyage/TEI-style `/rerank` endpoint.
|
|
390
470
|
|
|
391
471
|
## Collection maintenance
|
|
392
472
|
|
|
@@ -458,8 +538,8 @@ permanent `if (supported)` and an unreachable "and if it isn't?" branch.
|
|
|
458
538
|
|
|
459
539
|
A method goes in an **optional capability interface** when some backend genuinely *cannot* provide it
|
|
460
540
|
without infrastructure the consumer has to adopt. `LexicalVectorStore.searchText` is the only one
|
|
461
|
-
today, and it earns it: RediSearch has BM25 natively, Postgres needs a `tsvector`
|
|
462
|
-
|
|
541
|
+
today, and it earns it: RediSearch has BM25 natively, Postgres needs a GIN-indexed `tsvector` and a
|
|
542
|
+
migration you run and watch (hence the separate `PgLexicalVectorStore`), and `MemoryVectorStore` would have to implement BM25 itself.
|
|
463
543
|
|
|
464
544
|
The test is whether a backend can do it at all — not whether adding it would inconvenience an
|
|
465
545
|
implementer who has already shipped.
|