@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 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 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
+ 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
- | `PgVectorStore` | ❌ | the chunks table has no `tsvector`/GIN index, and adding one is a migration |
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 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.
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` column, a GIN index
462
- and a migration you run and watch, and `MemoryVectorStore` would have to implement BM25 itself.
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.