@dudousxd/nestjs-agent-rag 0.4.0 → 0.5.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 +122 -3
- package/dist/index.cjs +250 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +177 -4
- package/dist/index.d.ts +177 -4
- package/dist/index.js +246 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -54,6 +54,37 @@ interface VectorStore {
|
|
|
54
54
|
*/
|
|
55
55
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* An **optional** capability a {@link VectorStore} may also implement: full-text (lexical) search
|
|
59
|
+
* over the chunk text it already stores, using the backing engine's own keyword index — RediSearch's
|
|
60
|
+
* BM25 over the `TEXT` field, say. Its point is the multi-process deployment: when ingestion runs on
|
|
61
|
+
* a worker and search runs on an API pod, the API process never sees the chunks, so the in-process
|
|
62
|
+
* {@link import('./keyword-retriever.js').KeywordRetriever} has nothing to index. A store that
|
|
63
|
+
* implements this needs no second index, no corpus in JS heap, and no refresh window — a chunk is
|
|
64
|
+
* lexically findable the moment it is upserted.
|
|
65
|
+
*
|
|
66
|
+
* Wrap it as a core `Retriever` with {@link import('./lexical-retriever.js').LexicalRetriever}.
|
|
67
|
+
*
|
|
68
|
+
* Implementations MUST apply `options.filter` with exactly the same semantics as
|
|
69
|
+
* {@link VectorStore.search} — including the empty-array deny primitive — because the filter is
|
|
70
|
+
* routinely an access-control boundary, not a hint.
|
|
71
|
+
*/
|
|
72
|
+
interface LexicalVectorStore extends VectorStore {
|
|
73
|
+
/**
|
|
74
|
+
* Rank stored chunks against a natural-language `query` by keyword relevance. `options.filter`
|
|
75
|
+
* narrows exactly as in {@link VectorStore.search}. The returned `score` is the engine's own
|
|
76
|
+
* relevance score (BM25), which shares no scale with `search`'s cosine similarity — fuse the two
|
|
77
|
+
* with {@link import('./hybrid-retriever.js').HybridRetriever}, whose RRF is rank-based and so
|
|
78
|
+
* needs no common scale.
|
|
79
|
+
*/
|
|
80
|
+
searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Does this store carry the optional {@link LexicalVectorStore} capability? Lets a consumer pick the
|
|
84
|
+
* store-backed lexical leg when available and fall back to `KeywordRetriever` (or to dense-only)
|
|
85
|
+
* when it is not, without knowing which adapter it was handed.
|
|
86
|
+
*/
|
|
87
|
+
declare function isLexicalVectorStore(store: VectorStore): store is LexicalVectorStore;
|
|
57
88
|
/** A distinct source document as seen by the index — its id plus a representative chunk's metadata. */
|
|
58
89
|
interface IndexedDocument {
|
|
59
90
|
id: string;
|
|
@@ -93,6 +124,24 @@ interface PgVectorStoreOptions {
|
|
|
93
124
|
* A pgvector-backed {@link VectorStore} — the production reference adapter. Cosine distance via the
|
|
94
125
|
* `<=>` operator over an HNSW index; metadata in a `jsonb` column filtered with `@>`. Call
|
|
95
126
|
* {@link PgVectorStore.ensureSchema} once at boot to create the extension, table, and index.
|
|
127
|
+
*
|
|
128
|
+
* This store does **not** implement the optional
|
|
129
|
+
* {@link import('./vector-store.js').LexicalVectorStore} capability, unlike
|
|
130
|
+
* {@link import('./redis-vector-store.js').RedisVectorStore}. That is a deliberate omission, not an
|
|
131
|
+
* oversight: RediSearch already indexes the chunk text (the schema declares it `TEXT`), so BM25 is
|
|
132
|
+
* free there, whereas the table below indexes nothing lexically — `text` is a plain column with no
|
|
133
|
+
* `tsvector` and no GIN index. Adding one is not a transparent upgrade:
|
|
134
|
+
*
|
|
135
|
+
* - it is new DDL in `ensureSchema`, and `CREATE INDEX` (non-concurrently, as this method must, since
|
|
136
|
+
* `CONCURRENTLY` cannot run in a transaction) takes a write lock on an already-populated chunks
|
|
137
|
+
* table — a boot-time stall proportional to corpus size for every existing deployment;
|
|
138
|
+
* - it forces a text-search-configuration choice (`english` vs `simple` vs …) that must match between
|
|
139
|
+
* the index expression and every query, or Postgres silently ignores the index and sequentially
|
|
140
|
+
* scans the corpus — a failure mode that stays *correct* while quietly getting slower with scale.
|
|
141
|
+
*
|
|
142
|
+
* Both want a migration a consumer runs and observes, not a side effect of upgrading the library. The
|
|
143
|
+
* capability interface is open, so a `PgLexicalVectorStore` (or an option here, gated on an
|
|
144
|
+
* explicitly-created index) can be added later without changing anything else.
|
|
96
145
|
*/
|
|
97
146
|
declare class PgVectorStore implements VectorStore {
|
|
98
147
|
private readonly client;
|
|
@@ -116,6 +165,35 @@ declare class PgVectorStore implements VectorStore {
|
|
|
116
165
|
interface RedisSearchClient {
|
|
117
166
|
sendCommand(args: (string | Buffer)[]): Promise<unknown>;
|
|
118
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* The index that already exists in Redis contradicts this store's configuration in a way that cannot
|
|
170
|
+
* be repaired in place. Thrown by {@link RedisVectorStore.ensureSchema} — which would otherwise have
|
|
171
|
+
* returned happily and left every subsequent query quietly wrong.
|
|
172
|
+
*
|
|
173
|
+
* The canonical case is a changed embedding width (a model swap, 1024 → 1536): RediSearch fixes a
|
|
174
|
+
* vector field's `DIM` at `FT.CREATE`, and every vector already stored was produced by the OLD model,
|
|
175
|
+
* so there is no in-place fix — the index has to be dropped and the corpus re-embedded. That is
|
|
176
|
+
* destructive and belongs to the host, never to a boot-time `ensureSchema`.
|
|
177
|
+
*/
|
|
178
|
+
declare class RedisVectorSchemaMismatchError extends Error {
|
|
179
|
+
/** The RediSearch index that was inspected. */
|
|
180
|
+
readonly index: string;
|
|
181
|
+
/** The index attribute that differs (e.g. `embedding`, `meta_tenant`). */
|
|
182
|
+
readonly field: string;
|
|
183
|
+
/** What this store is configured for. */
|
|
184
|
+
readonly expected: string;
|
|
185
|
+
/** What `FT.INFO` reports the live index has. */
|
|
186
|
+
readonly actual: string;
|
|
187
|
+
constructor(
|
|
188
|
+
/** The RediSearch index that was inspected. */
|
|
189
|
+
index: string,
|
|
190
|
+
/** The index attribute that differs (e.g. `embedding`, `meta_tenant`). */
|
|
191
|
+
field: string,
|
|
192
|
+
/** What this store is configured for. */
|
|
193
|
+
expected: string,
|
|
194
|
+
/** What `FT.INFO` reports the live index has. */
|
|
195
|
+
actual: string, remedy: string);
|
|
196
|
+
}
|
|
119
197
|
interface RedisVectorStoreOptions {
|
|
120
198
|
/** RediSearch index name. Default `agent_rag_idx`. */
|
|
121
199
|
index?: string;
|
|
@@ -128,26 +206,74 @@ interface RedisVectorStoreOptions {
|
|
|
128
206
|
* front, so only keys listed here can be passed in `search`'s `filter`. Default none.
|
|
129
207
|
*/
|
|
130
208
|
filterableFields?: string[];
|
|
209
|
+
/**
|
|
210
|
+
* RediSearch scorer used by {@link RedisVectorStore.searchText}. Default `BM25` — the standard
|
|
211
|
+
* lexical ranking, and the same family the in-process `KeywordRetriever` implements. Override for
|
|
212
|
+
* a server that names it differently (`BM25STD` on newer builds) or to pick `TFIDF`/`DISMAX`.
|
|
213
|
+
*/
|
|
214
|
+
lexicalScorer?: string;
|
|
131
215
|
}
|
|
132
216
|
/**
|
|
133
217
|
* A RediSearch-backed {@link VectorStore} — HNSW + cosine over Redis, the ecosystem-native option
|
|
134
218
|
* for anyone already running Redis (see `-transport-redis`). Chunks are hashes under `prefix`, the
|
|
135
219
|
* embedding a FLOAT32 vector field; `search` is a KNN query. Call {@link RedisVectorStore.ensureSchema}
|
|
136
220
|
* once at boot to create the index.
|
|
221
|
+
*
|
|
222
|
+
* The chunk text is declared as a RediSearch `TEXT` field, so this store also satisfies
|
|
223
|
+
* {@link LexicalVectorStore}: {@link RedisVectorStore.searchText} is BM25 over that same index —
|
|
224
|
+
* the lexical half of hybrid search with no second index to build, feed, or invalidate.
|
|
137
225
|
*/
|
|
138
|
-
declare class RedisVectorStore implements
|
|
226
|
+
declare class RedisVectorStore implements LexicalVectorStore {
|
|
139
227
|
private readonly client;
|
|
140
228
|
private readonly index;
|
|
141
229
|
private readonly prefix;
|
|
142
230
|
private readonly dimensions;
|
|
143
231
|
private readonly filterableFields;
|
|
232
|
+
private readonly lexicalScorer;
|
|
144
233
|
constructor(client: RedisSearchClient, options?: RedisVectorStoreOptions);
|
|
145
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —
|
|
236
|
+
* and if it DOES exist, compare what's there against this store's configuration instead of
|
|
237
|
+
* assuming they agree. An index that outlives a config change is the normal case (schemas live in
|
|
238
|
+
* Redis, config lives in a redeployed process), and the two failure modes it produces are both
|
|
239
|
+
* invisible at runtime: a `filterableFields` entry added later has no `meta_*` TAG, so every search
|
|
240
|
+
* filtering on it matches *nothing*; a changed `dimensions` leaves the index on the old width, so
|
|
241
|
+
* writes are rejected or KNN ranks garbage. Neither raises anything on its own.
|
|
242
|
+
*
|
|
243
|
+
* The two are repaired differently, because only one of them CAN be repaired:
|
|
244
|
+
*
|
|
245
|
+
* - **A missing filterable TAG is additive** → `FT.ALTER … SCHEMA ADD`. Nothing that already works
|
|
246
|
+
* changes, so doing it automatically is safe. Note the chunks written *before* the alter carry no
|
|
247
|
+
* `meta_*` hash field at all (`upsert` only writes the fields configured at the time), so they
|
|
248
|
+
* only become filterable on that key once they're re-ingested — the schema is repaired here, the
|
|
249
|
+
* backfill is the host's call.
|
|
250
|
+
* - **A dimension (or field-type) change is not repairable** → {@link RedisVectorSchemaMismatchError}.
|
|
251
|
+
* It needs a drop + full reindex, which would destroy the host's corpus; failing loudly at boot is
|
|
252
|
+
* the only honest option.
|
|
253
|
+
*
|
|
254
|
+
* If `FT.INFO` comes back in a shape this parser doesn't recognise, no drift is inferred — an
|
|
255
|
+
* unreadable reply must not be turned into a false alarm that blocks a boot.
|
|
256
|
+
*/
|
|
146
257
|
ensureSchema(): Promise<void>;
|
|
258
|
+
/** Repair what can be repaired in place; throw on what can't. */
|
|
259
|
+
private reconcileSchema;
|
|
260
|
+
private createIndex;
|
|
147
261
|
upsert(records: VectorRecord[]): Promise<void>;
|
|
148
262
|
remove(documentId: string): Promise<void>;
|
|
149
263
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
150
264
|
search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
|
|
265
|
+
/**
|
|
266
|
+
* Lexical (BM25) search over the same index `search` uses — the {@link LexicalVectorStore} half of
|
|
267
|
+
* hybrid retrieval, served by RediSearch's own keyword index rather than an in-process copy of the
|
|
268
|
+
* corpus. `options.filter` is ANDed onto the text clause with the identical TAG semantics as
|
|
269
|
+
* `search` (array = OR, empty array = deny), so a lexical hit can never escape a filter a vector
|
|
270
|
+
* hit would have respected.
|
|
271
|
+
*
|
|
272
|
+
* A query with no searchable terms — empty, whitespace, or pure punctuation — returns `[]`. That is
|
|
273
|
+
* deliberate: RediSearch's `*` would match the entire corpus, which is the opposite of what a
|
|
274
|
+
* relevance query with nothing to be relevant to should do.
|
|
275
|
+
*/
|
|
276
|
+
searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
|
|
151
277
|
}
|
|
152
278
|
|
|
153
279
|
/**
|
|
@@ -205,6 +331,11 @@ interface KeywordRetrieverOptions {
|
|
|
205
331
|
* {@link ChunkRecord}s you upsert into the vector store (from `chunkDocuments`) so their chunk ids
|
|
206
332
|
* line up and `HybridRetriever` can fuse the two rankings. Full BM25: idf, term-frequency saturation
|
|
207
333
|
* (`k1`), and document-length normalization (`b`).
|
|
334
|
+
*
|
|
335
|
+
* Because it holds its OWN copy of each chunk's text, it needs the same delete-sync as the vector
|
|
336
|
+
* store: whenever you call {@link import('./vector-store.js').VectorStore.remove}, call
|
|
337
|
+
* {@link KeywordRetriever.remove} with the same document id. Skipping it leaves the document
|
|
338
|
+
* lexically retrievable — `retrieve` would keep returning the removed passage's full text.
|
|
208
339
|
*/
|
|
209
340
|
declare class KeywordRetriever implements Retriever {
|
|
210
341
|
private readonly docs;
|
|
@@ -213,9 +344,51 @@ declare class KeywordRetriever implements Retriever {
|
|
|
213
344
|
private readonly k1;
|
|
214
345
|
private readonly b;
|
|
215
346
|
constructor(options?: KeywordRetrieverOptions);
|
|
347
|
+
/** How many chunks are currently indexed. */
|
|
348
|
+
get size(): number;
|
|
216
349
|
/** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */
|
|
217
350
|
add(records: ChunkRecord[]): void;
|
|
218
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Forget a source document — every chunk whose id is `${documentId}` or `${documentId}#<n>`, the
|
|
353
|
+
* same id scheme (and the same {@link documentIdOf} collapse) the vector stores use for their
|
|
354
|
+
* `remove`. The keyword half of a hybrid index keeps its own copy of the chunk text, so a document
|
|
355
|
+
* dropped from the vector store stays fully retrievable here until this is called. Unknown ids are
|
|
356
|
+
* a no-op.
|
|
357
|
+
*/
|
|
358
|
+
remove(documentId: string): void;
|
|
359
|
+
/** Drop every indexed chunk, resetting the corpus statistics with it. */
|
|
360
|
+
clear(): void;
|
|
361
|
+
/** Drop ONE chunk by its exact id, decrementing the corpus statistics it contributed. */
|
|
362
|
+
private removeChunk;
|
|
363
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The keyword half of hybrid search, served by the store's own full-text index — the mirror of
|
|
368
|
+
* {@link import('./embedding-retriever.js').EmbeddingRetriever}, which serves the dense half. It
|
|
369
|
+
* holds no state: every query goes to the store, so any process that can reach the store can run
|
|
370
|
+
* lexical search, whether or not it did the ingestion.
|
|
371
|
+
*
|
|
372
|
+
* Compose it exactly like the dense leg:
|
|
373
|
+
*
|
|
374
|
+
* ```ts
|
|
375
|
+
* const retriever = new HybridRetriever([
|
|
376
|
+
* new EmbeddingRetriever(embedder, store),
|
|
377
|
+
* new LexicalRetriever(store),
|
|
378
|
+
* ]);
|
|
379
|
+
* ```
|
|
380
|
+
*
|
|
381
|
+
* Both legs read the same chunks from the same index, so their ids line up by construction and
|
|
382
|
+
* `HybridRetriever`'s RRF fuses them without either side needing a score on a common scale.
|
|
383
|
+
*
|
|
384
|
+
* Prefer this over {@link import('./keyword-retriever.js').KeywordRetriever} whenever the store
|
|
385
|
+
* supports it (see {@link import('./vector-store.js').isLexicalVectorStore}) and ingestion and
|
|
386
|
+
* search can run in different processes; `KeywordRetriever` remains the answer for `MemoryVectorStore`
|
|
387
|
+
* and single-process deployments.
|
|
388
|
+
*/
|
|
389
|
+
declare class LexicalRetriever implements Retriever {
|
|
390
|
+
private readonly store;
|
|
391
|
+
constructor(store: LexicalVectorStore);
|
|
219
392
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
220
393
|
}
|
|
221
394
|
|
|
@@ -297,4 +470,4 @@ interface RetrievalTool {
|
|
|
297
470
|
*/
|
|
298
471
|
declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
|
|
299
472
|
|
|
300
|
-
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, MemoryVectorStore, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, type VectorRecord, type VectorSearchOptions, type VectorStore, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments };
|
|
473
|
+
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, type VectorRecord, type VectorSearchOptions, type VectorStore, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,37 @@ interface VectorStore {
|
|
|
54
54
|
*/
|
|
55
55
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* An **optional** capability a {@link VectorStore} may also implement: full-text (lexical) search
|
|
59
|
+
* over the chunk text it already stores, using the backing engine's own keyword index — RediSearch's
|
|
60
|
+
* BM25 over the `TEXT` field, say. Its point is the multi-process deployment: when ingestion runs on
|
|
61
|
+
* a worker and search runs on an API pod, the API process never sees the chunks, so the in-process
|
|
62
|
+
* {@link import('./keyword-retriever.js').KeywordRetriever} has nothing to index. A store that
|
|
63
|
+
* implements this needs no second index, no corpus in JS heap, and no refresh window — a chunk is
|
|
64
|
+
* lexically findable the moment it is upserted.
|
|
65
|
+
*
|
|
66
|
+
* Wrap it as a core `Retriever` with {@link import('./lexical-retriever.js').LexicalRetriever}.
|
|
67
|
+
*
|
|
68
|
+
* Implementations MUST apply `options.filter` with exactly the same semantics as
|
|
69
|
+
* {@link VectorStore.search} — including the empty-array deny primitive — because the filter is
|
|
70
|
+
* routinely an access-control boundary, not a hint.
|
|
71
|
+
*/
|
|
72
|
+
interface LexicalVectorStore extends VectorStore {
|
|
73
|
+
/**
|
|
74
|
+
* Rank stored chunks against a natural-language `query` by keyword relevance. `options.filter`
|
|
75
|
+
* narrows exactly as in {@link VectorStore.search}. The returned `score` is the engine's own
|
|
76
|
+
* relevance score (BM25), which shares no scale with `search`'s cosine similarity — fuse the two
|
|
77
|
+
* with {@link import('./hybrid-retriever.js').HybridRetriever}, whose RRF is rank-based and so
|
|
78
|
+
* needs no common scale.
|
|
79
|
+
*/
|
|
80
|
+
searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Does this store carry the optional {@link LexicalVectorStore} capability? Lets a consumer pick the
|
|
84
|
+
* store-backed lexical leg when available and fall back to `KeywordRetriever` (or to dense-only)
|
|
85
|
+
* when it is not, without knowing which adapter it was handed.
|
|
86
|
+
*/
|
|
87
|
+
declare function isLexicalVectorStore(store: VectorStore): store is LexicalVectorStore;
|
|
57
88
|
/** A distinct source document as seen by the index — its id plus a representative chunk's metadata. */
|
|
58
89
|
interface IndexedDocument {
|
|
59
90
|
id: string;
|
|
@@ -93,6 +124,24 @@ interface PgVectorStoreOptions {
|
|
|
93
124
|
* A pgvector-backed {@link VectorStore} — the production reference adapter. Cosine distance via the
|
|
94
125
|
* `<=>` operator over an HNSW index; metadata in a `jsonb` column filtered with `@>`. Call
|
|
95
126
|
* {@link PgVectorStore.ensureSchema} once at boot to create the extension, table, and index.
|
|
127
|
+
*
|
|
128
|
+
* This store does **not** implement the optional
|
|
129
|
+
* {@link import('./vector-store.js').LexicalVectorStore} capability, unlike
|
|
130
|
+
* {@link import('./redis-vector-store.js').RedisVectorStore}. That is a deliberate omission, not an
|
|
131
|
+
* oversight: RediSearch already indexes the chunk text (the schema declares it `TEXT`), so BM25 is
|
|
132
|
+
* free there, whereas the table below indexes nothing lexically — `text` is a plain column with no
|
|
133
|
+
* `tsvector` and no GIN index. Adding one is not a transparent upgrade:
|
|
134
|
+
*
|
|
135
|
+
* - it is new DDL in `ensureSchema`, and `CREATE INDEX` (non-concurrently, as this method must, since
|
|
136
|
+
* `CONCURRENTLY` cannot run in a transaction) takes a write lock on an already-populated chunks
|
|
137
|
+
* table — a boot-time stall proportional to corpus size for every existing deployment;
|
|
138
|
+
* - it forces a text-search-configuration choice (`english` vs `simple` vs …) that must match between
|
|
139
|
+
* the index expression and every query, or Postgres silently ignores the index and sequentially
|
|
140
|
+
* scans the corpus — a failure mode that stays *correct* while quietly getting slower with scale.
|
|
141
|
+
*
|
|
142
|
+
* Both want a migration a consumer runs and observes, not a side effect of upgrading the library. The
|
|
143
|
+
* capability interface is open, so a `PgLexicalVectorStore` (or an option here, gated on an
|
|
144
|
+
* explicitly-created index) can be added later without changing anything else.
|
|
96
145
|
*/
|
|
97
146
|
declare class PgVectorStore implements VectorStore {
|
|
98
147
|
private readonly client;
|
|
@@ -116,6 +165,35 @@ declare class PgVectorStore implements VectorStore {
|
|
|
116
165
|
interface RedisSearchClient {
|
|
117
166
|
sendCommand(args: (string | Buffer)[]): Promise<unknown>;
|
|
118
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* The index that already exists in Redis contradicts this store's configuration in a way that cannot
|
|
170
|
+
* be repaired in place. Thrown by {@link RedisVectorStore.ensureSchema} — which would otherwise have
|
|
171
|
+
* returned happily and left every subsequent query quietly wrong.
|
|
172
|
+
*
|
|
173
|
+
* The canonical case is a changed embedding width (a model swap, 1024 → 1536): RediSearch fixes a
|
|
174
|
+
* vector field's `DIM` at `FT.CREATE`, and every vector already stored was produced by the OLD model,
|
|
175
|
+
* so there is no in-place fix — the index has to be dropped and the corpus re-embedded. That is
|
|
176
|
+
* destructive and belongs to the host, never to a boot-time `ensureSchema`.
|
|
177
|
+
*/
|
|
178
|
+
declare class RedisVectorSchemaMismatchError extends Error {
|
|
179
|
+
/** The RediSearch index that was inspected. */
|
|
180
|
+
readonly index: string;
|
|
181
|
+
/** The index attribute that differs (e.g. `embedding`, `meta_tenant`). */
|
|
182
|
+
readonly field: string;
|
|
183
|
+
/** What this store is configured for. */
|
|
184
|
+
readonly expected: string;
|
|
185
|
+
/** What `FT.INFO` reports the live index has. */
|
|
186
|
+
readonly actual: string;
|
|
187
|
+
constructor(
|
|
188
|
+
/** The RediSearch index that was inspected. */
|
|
189
|
+
index: string,
|
|
190
|
+
/** The index attribute that differs (e.g. `embedding`, `meta_tenant`). */
|
|
191
|
+
field: string,
|
|
192
|
+
/** What this store is configured for. */
|
|
193
|
+
expected: string,
|
|
194
|
+
/** What `FT.INFO` reports the live index has. */
|
|
195
|
+
actual: string, remedy: string);
|
|
196
|
+
}
|
|
119
197
|
interface RedisVectorStoreOptions {
|
|
120
198
|
/** RediSearch index name. Default `agent_rag_idx`. */
|
|
121
199
|
index?: string;
|
|
@@ -128,26 +206,74 @@ interface RedisVectorStoreOptions {
|
|
|
128
206
|
* front, so only keys listed here can be passed in `search`'s `filter`. Default none.
|
|
129
207
|
*/
|
|
130
208
|
filterableFields?: string[];
|
|
209
|
+
/**
|
|
210
|
+
* RediSearch scorer used by {@link RedisVectorStore.searchText}. Default `BM25` — the standard
|
|
211
|
+
* lexical ranking, and the same family the in-process `KeywordRetriever` implements. Override for
|
|
212
|
+
* a server that names it differently (`BM25STD` on newer builds) or to pick `TFIDF`/`DISMAX`.
|
|
213
|
+
*/
|
|
214
|
+
lexicalScorer?: string;
|
|
131
215
|
}
|
|
132
216
|
/**
|
|
133
217
|
* A RediSearch-backed {@link VectorStore} — HNSW + cosine over Redis, the ecosystem-native option
|
|
134
218
|
* for anyone already running Redis (see `-transport-redis`). Chunks are hashes under `prefix`, the
|
|
135
219
|
* embedding a FLOAT32 vector field; `search` is a KNN query. Call {@link RedisVectorStore.ensureSchema}
|
|
136
220
|
* once at boot to create the index.
|
|
221
|
+
*
|
|
222
|
+
* The chunk text is declared as a RediSearch `TEXT` field, so this store also satisfies
|
|
223
|
+
* {@link LexicalVectorStore}: {@link RedisVectorStore.searchText} is BM25 over that same index —
|
|
224
|
+
* the lexical half of hybrid search with no second index to build, feed, or invalidate.
|
|
137
225
|
*/
|
|
138
|
-
declare class RedisVectorStore implements
|
|
226
|
+
declare class RedisVectorStore implements LexicalVectorStore {
|
|
139
227
|
private readonly client;
|
|
140
228
|
private readonly index;
|
|
141
229
|
private readonly prefix;
|
|
142
230
|
private readonly dimensions;
|
|
143
231
|
private readonly filterableFields;
|
|
232
|
+
private readonly lexicalScorer;
|
|
144
233
|
constructor(client: RedisSearchClient, options?: RedisVectorStoreOptions);
|
|
145
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —
|
|
236
|
+
* and if it DOES exist, compare what's there against this store's configuration instead of
|
|
237
|
+
* assuming they agree. An index that outlives a config change is the normal case (schemas live in
|
|
238
|
+
* Redis, config lives in a redeployed process), and the two failure modes it produces are both
|
|
239
|
+
* invisible at runtime: a `filterableFields` entry added later has no `meta_*` TAG, so every search
|
|
240
|
+
* filtering on it matches *nothing*; a changed `dimensions` leaves the index on the old width, so
|
|
241
|
+
* writes are rejected or KNN ranks garbage. Neither raises anything on its own.
|
|
242
|
+
*
|
|
243
|
+
* The two are repaired differently, because only one of them CAN be repaired:
|
|
244
|
+
*
|
|
245
|
+
* - **A missing filterable TAG is additive** → `FT.ALTER … SCHEMA ADD`. Nothing that already works
|
|
246
|
+
* changes, so doing it automatically is safe. Note the chunks written *before* the alter carry no
|
|
247
|
+
* `meta_*` hash field at all (`upsert` only writes the fields configured at the time), so they
|
|
248
|
+
* only become filterable on that key once they're re-ingested — the schema is repaired here, the
|
|
249
|
+
* backfill is the host's call.
|
|
250
|
+
* - **A dimension (or field-type) change is not repairable** → {@link RedisVectorSchemaMismatchError}.
|
|
251
|
+
* It needs a drop + full reindex, which would destroy the host's corpus; failing loudly at boot is
|
|
252
|
+
* the only honest option.
|
|
253
|
+
*
|
|
254
|
+
* If `FT.INFO` comes back in a shape this parser doesn't recognise, no drift is inferred — an
|
|
255
|
+
* unreadable reply must not be turned into a false alarm that blocks a boot.
|
|
256
|
+
*/
|
|
146
257
|
ensureSchema(): Promise<void>;
|
|
258
|
+
/** Repair what can be repaired in place; throw on what can't. */
|
|
259
|
+
private reconcileSchema;
|
|
260
|
+
private createIndex;
|
|
147
261
|
upsert(records: VectorRecord[]): Promise<void>;
|
|
148
262
|
remove(documentId: string): Promise<void>;
|
|
149
263
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
150
264
|
search(embedding: number[], options: VectorSearchOptions): Promise<Passage[]>;
|
|
265
|
+
/**
|
|
266
|
+
* Lexical (BM25) search over the same index `search` uses — the {@link LexicalVectorStore} half of
|
|
267
|
+
* hybrid retrieval, served by RediSearch's own keyword index rather than an in-process copy of the
|
|
268
|
+
* corpus. `options.filter` is ANDed onto the text clause with the identical TAG semantics as
|
|
269
|
+
* `search` (array = OR, empty array = deny), so a lexical hit can never escape a filter a vector
|
|
270
|
+
* hit would have respected.
|
|
271
|
+
*
|
|
272
|
+
* A query with no searchable terms — empty, whitespace, or pure punctuation — returns `[]`. That is
|
|
273
|
+
* deliberate: RediSearch's `*` would match the entire corpus, which is the opposite of what a
|
|
274
|
+
* relevance query with nothing to be relevant to should do.
|
|
275
|
+
*/
|
|
276
|
+
searchText(query: string, options: VectorSearchOptions): Promise<Passage[]>;
|
|
151
277
|
}
|
|
152
278
|
|
|
153
279
|
/**
|
|
@@ -205,6 +331,11 @@ interface KeywordRetrieverOptions {
|
|
|
205
331
|
* {@link ChunkRecord}s you upsert into the vector store (from `chunkDocuments`) so their chunk ids
|
|
206
332
|
* line up and `HybridRetriever` can fuse the two rankings. Full BM25: idf, term-frequency saturation
|
|
207
333
|
* (`k1`), and document-length normalization (`b`).
|
|
334
|
+
*
|
|
335
|
+
* Because it holds its OWN copy of each chunk's text, it needs the same delete-sync as the vector
|
|
336
|
+
* store: whenever you call {@link import('./vector-store.js').VectorStore.remove}, call
|
|
337
|
+
* {@link KeywordRetriever.remove} with the same document id. Skipping it leaves the document
|
|
338
|
+
* lexically retrievable — `retrieve` would keep returning the removed passage's full text.
|
|
208
339
|
*/
|
|
209
340
|
declare class KeywordRetriever implements Retriever {
|
|
210
341
|
private readonly docs;
|
|
@@ -213,9 +344,51 @@ declare class KeywordRetriever implements Retriever {
|
|
|
213
344
|
private readonly k1;
|
|
214
345
|
private readonly b;
|
|
215
346
|
constructor(options?: KeywordRetrieverOptions);
|
|
347
|
+
/** How many chunks are currently indexed. */
|
|
348
|
+
get size(): number;
|
|
216
349
|
/** Index (or re-index) records. Re-adding an existing id replaces its posting and updates stats. */
|
|
217
350
|
add(records: ChunkRecord[]): void;
|
|
218
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Forget a source document — every chunk whose id is `${documentId}` or `${documentId}#<n>`, the
|
|
353
|
+
* same id scheme (and the same {@link documentIdOf} collapse) the vector stores use for their
|
|
354
|
+
* `remove`. The keyword half of a hybrid index keeps its own copy of the chunk text, so a document
|
|
355
|
+
* dropped from the vector store stays fully retrievable here until this is called. Unknown ids are
|
|
356
|
+
* a no-op.
|
|
357
|
+
*/
|
|
358
|
+
remove(documentId: string): void;
|
|
359
|
+
/** Drop every indexed chunk, resetting the corpus statistics with it. */
|
|
360
|
+
clear(): void;
|
|
361
|
+
/** Drop ONE chunk by its exact id, decrementing the corpus statistics it contributed. */
|
|
362
|
+
private removeChunk;
|
|
363
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The keyword half of hybrid search, served by the store's own full-text index — the mirror of
|
|
368
|
+
* {@link import('./embedding-retriever.js').EmbeddingRetriever}, which serves the dense half. It
|
|
369
|
+
* holds no state: every query goes to the store, so any process that can reach the store can run
|
|
370
|
+
* lexical search, whether or not it did the ingestion.
|
|
371
|
+
*
|
|
372
|
+
* Compose it exactly like the dense leg:
|
|
373
|
+
*
|
|
374
|
+
* ```ts
|
|
375
|
+
* const retriever = new HybridRetriever([
|
|
376
|
+
* new EmbeddingRetriever(embedder, store),
|
|
377
|
+
* new LexicalRetriever(store),
|
|
378
|
+
* ]);
|
|
379
|
+
* ```
|
|
380
|
+
*
|
|
381
|
+
* Both legs read the same chunks from the same index, so their ids line up by construction and
|
|
382
|
+
* `HybridRetriever`'s RRF fuses them without either side needing a score on a common scale.
|
|
383
|
+
*
|
|
384
|
+
* Prefer this over {@link import('./keyword-retriever.js').KeywordRetriever} whenever the store
|
|
385
|
+
* supports it (see {@link import('./vector-store.js').isLexicalVectorStore}) and ingestion and
|
|
386
|
+
* search can run in different processes; `KeywordRetriever` remains the answer for `MemoryVectorStore`
|
|
387
|
+
* and single-process deployments.
|
|
388
|
+
*/
|
|
389
|
+
declare class LexicalRetriever implements Retriever {
|
|
390
|
+
private readonly store;
|
|
391
|
+
constructor(store: LexicalVectorStore);
|
|
219
392
|
retrieve(query: string, options?: RetrieveOptions): Promise<Passage[]>;
|
|
220
393
|
}
|
|
221
394
|
|
|
@@ -297,4 +470,4 @@ interface RetrievalTool {
|
|
|
297
470
|
*/
|
|
298
471
|
declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
|
|
299
472
|
|
|
300
|
-
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, MemoryVectorStore, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, type VectorRecord, type VectorSearchOptions, type VectorStore, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments };
|
|
473
|
+
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, type VectorRecord, type VectorSearchOptions, type VectorStore, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|