@dudousxd/nestjs-agent-rag 0.10.3 → 0.11.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 +90 -14
- package/dist/index.cjs +541 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +287 -23
- package/dist/index.d.ts +287 -23
- package/dist/index.js +536 -74
- 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,61 @@ 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
|
+
Ranking is `ts_rank_cd`; the query goes through `websearch_to_tsquery` (every word must appear), and
|
|
234
|
+
when that finds nothing it retries with any word (`anyTermFallback`, default on) so a long question
|
|
235
|
+
still matches. Without `column`, `ensureSchema`/`schemaStatements` add a GIN index over
|
|
236
|
+
`to_tsvector(config, text)` — the exact expression the query uses. On a populated table create it
|
|
237
|
+
`CONCURRENTLY` in a migration rather than at boot (see below).
|
|
238
|
+
|
|
170
239
|
## RediSearch store
|
|
171
240
|
|
|
172
241
|
`RedisVectorStore` takes anything that can send a raw command — `node-redis`, or `ioredis` wrapped —
|
|
@@ -354,14 +423,17 @@ const lexical = isLexicalVectorStore(store) ? new LexicalRetriever(store) : keyw
|
|
|
354
423
|
| Store | Lexical | Why |
|
|
355
424
|
| --- | --- | --- |
|
|
356
425
|
| `RedisVectorStore` | ✅ | RediSearch indexes the chunk text as `TEXT`; BM25 is already there |
|
|
357
|
-
| `
|
|
426
|
+
| `PgLexicalVectorStore` | ✅ | Postgres full-text search over a GIN-indexed `tsvector` you opt into |
|
|
427
|
+
| `PgVectorStore` | ❌ | the plain chunks table has no `tsvector`/GIN index; use the subclass above |
|
|
358
428
|
| `MemoryVectorStore` | ❌ | single-process by definition — use `KeywordRetriever` |
|
|
359
429
|
|
|
360
|
-
Postgres is a
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
430
|
+
On Postgres it is a separate class rather than a new method on `PgVectorStore`, deliberately.
|
|
431
|
+
Lexical search there needs new DDL (a GIN index), and `CREATE INDEX` in `ensureSchema` would take a
|
|
432
|
+
write lock on an already-populated chunks table at boot; it also forces a text-search-configuration
|
|
433
|
+
choice that must match between index and query or Postgres silently falls back to a sequential scan.
|
|
434
|
+
Had `PgVectorStore` simply grown `searchText`, every `isLexicalVectorStore(store)` check would have
|
|
435
|
+
started using it against an unindexed table on upgrade. Constructing `PgLexicalVectorStore` is the
|
|
436
|
+
opt-in; the index is a migration you run and watch (`schemaStatements()` prints it).
|
|
365
437
|
|
|
366
438
|
## API
|
|
367
439
|
|
|
@@ -376,7 +448,7 @@ library upgrade. The interface is open, so it can be added later without changin
|
|
|
376
448
|
- `KeywordRetriever` — in-process BM25 over chunks you feed it; the single-process lexical half:
|
|
377
449
|
`add(chunks)`, `remove(documentId)`, `clear()`, `size`.
|
|
378
450
|
- `HybridRetriever(retrievers, { k?, fetchTopK?, weights? })` — RRF fusion of the two.
|
|
379
|
-
- `MemoryVectorStore` / `PgVectorStore` / `RedisVectorStore` — `VectorStore` adapters:
|
|
451
|
+
- `MemoryVectorStore` / `PgVectorStore` / `PgLexicalVectorStore` / `RedisVectorStore` — `VectorStore` adapters:
|
|
380
452
|
`upsert(records)`, `search(embedding, opts)`, `remove(documentId)`,
|
|
381
453
|
`updateMetadata(documentId, patch)`, `listDocuments(filter?)`,
|
|
382
454
|
`listChunks(documentId, { limit?, offset? })`.
|
|
@@ -387,6 +459,10 @@ library upgrade. The interface is open, so it can be added later without changin
|
|
|
387
459
|
agentic-retrieval tool. `telemetry` defaults to on; pass `false` to opt out.
|
|
388
460
|
- `instrumentRetriever(retriever, { describe?, collection? })` — wrap any `Retriever` so each
|
|
389
461
|
retrieval publishes on `aviary:rag:retrieval` (`RAG_RETRIEVAL_CHANNEL`).
|
|
462
|
+
- `openAiEmbeddings({ model, baseUrl?, apiKey?, dimensions?, batchSize?, maxInputChars?, headers?,
|
|
463
|
+
timeoutMs?, onUsage?, fetch? })` — an `EmbeddingProvider` over any OpenAI-compatible `/embeddings`.
|
|
464
|
+
- `HttpReranker({ url, model?, apiKey?, format?, headers?, timeoutMs?, fetch? })` — a `Reranker` over
|
|
465
|
+
a Cohere/Jina/Voyage/TEI-style `/rerank` endpoint.
|
|
390
466
|
|
|
391
467
|
## Collection maintenance
|
|
392
468
|
|
|
@@ -458,8 +534,8 @@ permanent `if (supported)` and an unreachable "and if it isn't?" branch.
|
|
|
458
534
|
|
|
459
535
|
A method goes in an **optional capability interface** when some backend genuinely *cannot* provide it
|
|
460
536
|
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
|
-
|
|
537
|
+
today, and it earns it: RediSearch has BM25 natively, Postgres needs a GIN-indexed `tsvector` and a
|
|
538
|
+
migration you run and watch (hence the separate `PgLexicalVectorStore`), and `MemoryVectorStore` would have to implement BM25 itself.
|
|
463
539
|
|
|
464
540
|
The test is whether a backend can do it at all — not whether adding it would inconvenience an
|
|
465
541
|
implementer who has already shipped.
|