@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 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 an injected `PgClient` — adapt your own `pg` / `postgres.js`:
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
- | `PgVectorStore` | ❌ | the chunks table has no `tsvector`/GIN index, and adding one is a migration |
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 deliberate omission. Lexical search there needs new DDL (a GIN index), and `CREATE
361
- INDEX` in `ensureSchema` would take a write lock on an already-populated chunks table at boot; it also
362
- forces a text-search-configuration choice that must match between index and query or Postgres silently
363
- falls back to a sequential scan. That wants a migration you run and watch, not a side effect of a
364
- library upgrade. The interface is open, so it can be added later without changing anything else.
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` column, a GIN index
462
- and a migration you run and watch, and `MemoryVectorStore` would have to implement BM25 itself.
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.