@dudousxd/nestjs-agent-rag 0.8.0 → 0.9.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 +29 -1
- package/dist/index.cjs +126 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -1
- package/dist/index.d.ts +88 -1
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -174,6 +174,32 @@ interface VectorStore {
|
|
|
174
174
|
* `size` changed), or {@link VectorStore.remove remove} (orphaned).
|
|
175
175
|
*/
|
|
176
176
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Read one document's chunks back — id, text, metadata — ordered by their position in the
|
|
179
|
+
* document, optionally paged with `limit`/`offset`.
|
|
180
|
+
*
|
|
181
|
+
* The gap this closes is that **the text a store holds is otherwise write-only**. Every other way
|
|
182
|
+
* back to it is a ranked query: {@link VectorStore.search} needs an embedding,
|
|
183
|
+
* {@link LexicalVectorStore.searchText} needs a query string, and both return whatever *matched*,
|
|
184
|
+
* in relevance order, capped at `topK`. Neither can answer "show me what was indexed for this
|
|
185
|
+
* document, in order" — the question you ask when retrieval returns the wrong passage and you need
|
|
186
|
+
* to know whether the chunker cut badly or the retriever chose badly. Without this, the only way to
|
|
187
|
+
* see stored text is to reach around the adapter into the backing engine, which is exactly the
|
|
188
|
+
* abstraction break a consumer adopts a store to avoid.
|
|
189
|
+
*
|
|
190
|
+
* Document-scoped rather than filter-scoped, matching {@link VectorStore.remove} and
|
|
191
|
+
* {@link VectorStore.updateMetadata}: ordering is the whole point, and "position within the
|
|
192
|
+
* document" is only meaningful once a document has been picked. Enumerate documents with
|
|
193
|
+
* {@link VectorStore.listDocuments} first when you need to sweep.
|
|
194
|
+
*
|
|
195
|
+
* An unknown `documentId` yields `[]`; it does not throw, matching `remove` and `updateMetadata`.
|
|
196
|
+
*
|
|
197
|
+
* NOT an access-control seam. It takes no metadata `filter` and applies none, so it cannot enforce
|
|
198
|
+
* one — a caller that gates retrieval on a metadata field (tenant, audience, collection) MUST
|
|
199
|
+
* resolve that gate itself before calling, exactly as it would before `remove`. It is deliberately
|
|
200
|
+
* shaped for an operator/debug surface, not for serving end users.
|
|
201
|
+
*/
|
|
202
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
177
203
|
/**
|
|
178
204
|
* The distinct source document ids currently indexed, optionally narrowed by a metadata `filter` —
|
|
179
205
|
* {@link VectorStore.listDocuments} without the metadata. Use it when you only need the id set (a
|
|
@@ -271,6 +297,25 @@ interface IndexedDocument {
|
|
|
271
297
|
id: string;
|
|
272
298
|
metadata?: Record<string, unknown>;
|
|
273
299
|
}
|
|
300
|
+
/** One stored chunk, read back verbatim — what {@link VectorStore.listChunks} yields. */
|
|
301
|
+
interface StoredChunk {
|
|
302
|
+
/** The chunk id: `${documentId}#<n>`, or the bare document id for a single-chunk document. */
|
|
303
|
+
id: string;
|
|
304
|
+
/** Zero-based position within the document, parsed from the id. A bare id is chunk `0`. */
|
|
305
|
+
index: number;
|
|
306
|
+
/** The chunk text as it was indexed — the exact string the embedding was computed from. */
|
|
307
|
+
text: string;
|
|
308
|
+
metadata?: Record<string, unknown>;
|
|
309
|
+
}
|
|
310
|
+
/** Paging for {@link VectorStore.listChunks}. Omit both to read the whole document. */
|
|
311
|
+
interface ListChunksOptions {
|
|
312
|
+
/** Max chunks to return, applied after ordering by {@link StoredChunk.index}. */
|
|
313
|
+
limit?: number;
|
|
314
|
+
/** Chunks to skip, applied after ordering. */
|
|
315
|
+
offset?: number;
|
|
316
|
+
}
|
|
317
|
+
/** Chunk `n` of `${documentId}#<n>`; a bare document id (single-chunk document) is `0`. */
|
|
318
|
+
declare function chunkIndexOf(chunkId: string): number;
|
|
274
319
|
/** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */
|
|
275
320
|
declare function documentIdOf(chunkId: string): string;
|
|
276
321
|
|
|
@@ -296,6 +341,16 @@ declare class MemoryVectorStore implements VectorStore {
|
|
|
296
341
|
*/
|
|
297
342
|
updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;
|
|
298
343
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
344
|
+
/**
|
|
345
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
346
|
+
*
|
|
347
|
+
* Insertion order is not document order — `upsert` stores whatever the caller passed, and a
|
|
348
|
+
* re-ingest that rewrites chunk 3 alone moves it to the end of the Map. So this sorts by the index
|
|
349
|
+
* parsed from the id, the same thing the SQL and Redis adapters order by, rather than trusting the
|
|
350
|
+
* Map. A reference adapter that returned "usually ordered" would let a consumer ship a bug that
|
|
351
|
+
* only shows up on the store tests are NOT written against.
|
|
352
|
+
*/
|
|
353
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
299
354
|
listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;
|
|
300
355
|
removeMany(documentIds: string[]): Promise<void>;
|
|
301
356
|
/**
|
|
@@ -374,6 +429,19 @@ declare class PgVectorStore implements VectorStore {
|
|
|
374
429
|
*/
|
|
375
430
|
updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;
|
|
376
431
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
432
|
+
/**
|
|
433
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
434
|
+
*
|
|
435
|
+
* The order is computed from the id rather than stored: `#12` must sort after `#2`, which a plain
|
|
436
|
+
* `ORDER BY id` gets wrong on every document past ten chunks because it compares text. So the
|
|
437
|
+
* trailing number is extracted and cast to `int`, and a bare id (a single-chunk document, which
|
|
438
|
+
* carries no `#n` suffix) collapses to `0` — the same rule `chunkIndexOf` applies in the other two
|
|
439
|
+
* adapters.
|
|
440
|
+
*
|
|
441
|
+
* `embedding` is left out of the projection: it is the widest column in the table and useless to a
|
|
442
|
+
* caller reading text back.
|
|
443
|
+
*/
|
|
444
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
377
445
|
/** {@link listDocuments} without the metadata: one `DISTINCT` over the derived document id. */
|
|
378
446
|
listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;
|
|
379
447
|
/** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */
|
|
@@ -550,6 +618,25 @@ declare class RedisVectorStore implements LexicalVectorStore {
|
|
|
550
618
|
* document owns. The glob is escaped so `gone` does not reach into `goner#0`.
|
|
551
619
|
*/
|
|
552
620
|
private chunkKeys;
|
|
621
|
+
/**
|
|
622
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
623
|
+
*
|
|
624
|
+
* Reads the hashes directly (`HMGET text metadata_json`) rather than going through `FT.SEARCH`.
|
|
625
|
+
* Two reasons, and the second is the one that matters: the query language has no way to ask for
|
|
626
|
+
* "every chunk of this document" — `id` is the key name, not an indexed field — and `FT.SEARCH`
|
|
627
|
+
* caps at `LIMIT`, so paging a document through it would mean ordering by a field the index does
|
|
628
|
+
* not hold. `chunkKeys` is the same enumeration {@link RedisVectorStore.remove} and
|
|
629
|
+
* {@link RedisVectorStore.updateMetadata} already use, so "the chunks this returns" and "the chunks
|
|
630
|
+
* a delete would take" cannot drift apart.
|
|
631
|
+
*
|
|
632
|
+
* It inherits that method's cost too: one keyspace `SCAN` per call. Acceptable here because this
|
|
633
|
+
* is an inspection path (one document, on demand) and not something retrieval runs — but it is why
|
|
634
|
+
* this must not be put on a hot path or called in a loop over a corpus.
|
|
635
|
+
*
|
|
636
|
+
* `embedding` is deliberately not fetched: it is by far the largest field on the hash and nothing
|
|
637
|
+
* a caller reading text back can do with it.
|
|
638
|
+
*/
|
|
639
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
553
640
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
554
641
|
/**
|
|
555
642
|
* The id-only half of {@link RedisVectorStore.listDocuments}, served by `FT.SEARCH … NOCONTENT`:
|
|
@@ -868,4 +955,4 @@ interface RetrievalTool {
|
|
|
868
955
|
*/
|
|
869
956
|
declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
|
|
870
957
|
|
|
871
|
-
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|
|
958
|
+
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, type StoredChunk, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkIndexOf, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|
package/dist/index.d.ts
CHANGED
|
@@ -174,6 +174,32 @@ interface VectorStore {
|
|
|
174
174
|
* `size` changed), or {@link VectorStore.remove remove} (orphaned).
|
|
175
175
|
*/
|
|
176
176
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Read one document's chunks back — id, text, metadata — ordered by their position in the
|
|
179
|
+
* document, optionally paged with `limit`/`offset`.
|
|
180
|
+
*
|
|
181
|
+
* The gap this closes is that **the text a store holds is otherwise write-only**. Every other way
|
|
182
|
+
* back to it is a ranked query: {@link VectorStore.search} needs an embedding,
|
|
183
|
+
* {@link LexicalVectorStore.searchText} needs a query string, and both return whatever *matched*,
|
|
184
|
+
* in relevance order, capped at `topK`. Neither can answer "show me what was indexed for this
|
|
185
|
+
* document, in order" — the question you ask when retrieval returns the wrong passage and you need
|
|
186
|
+
* to know whether the chunker cut badly or the retriever chose badly. Without this, the only way to
|
|
187
|
+
* see stored text is to reach around the adapter into the backing engine, which is exactly the
|
|
188
|
+
* abstraction break a consumer adopts a store to avoid.
|
|
189
|
+
*
|
|
190
|
+
* Document-scoped rather than filter-scoped, matching {@link VectorStore.remove} and
|
|
191
|
+
* {@link VectorStore.updateMetadata}: ordering is the whole point, and "position within the
|
|
192
|
+
* document" is only meaningful once a document has been picked. Enumerate documents with
|
|
193
|
+
* {@link VectorStore.listDocuments} first when you need to sweep.
|
|
194
|
+
*
|
|
195
|
+
* An unknown `documentId` yields `[]`; it does not throw, matching `remove` and `updateMetadata`.
|
|
196
|
+
*
|
|
197
|
+
* NOT an access-control seam. It takes no metadata `filter` and applies none, so it cannot enforce
|
|
198
|
+
* one — a caller that gates retrieval on a metadata field (tenant, audience, collection) MUST
|
|
199
|
+
* resolve that gate itself before calling, exactly as it would before `remove`. It is deliberately
|
|
200
|
+
* shaped for an operator/debug surface, not for serving end users.
|
|
201
|
+
*/
|
|
202
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
177
203
|
/**
|
|
178
204
|
* The distinct source document ids currently indexed, optionally narrowed by a metadata `filter` —
|
|
179
205
|
* {@link VectorStore.listDocuments} without the metadata. Use it when you only need the id set (a
|
|
@@ -271,6 +297,25 @@ interface IndexedDocument {
|
|
|
271
297
|
id: string;
|
|
272
298
|
metadata?: Record<string, unknown>;
|
|
273
299
|
}
|
|
300
|
+
/** One stored chunk, read back verbatim — what {@link VectorStore.listChunks} yields. */
|
|
301
|
+
interface StoredChunk {
|
|
302
|
+
/** The chunk id: `${documentId}#<n>`, or the bare document id for a single-chunk document. */
|
|
303
|
+
id: string;
|
|
304
|
+
/** Zero-based position within the document, parsed from the id. A bare id is chunk `0`. */
|
|
305
|
+
index: number;
|
|
306
|
+
/** The chunk text as it was indexed — the exact string the embedding was computed from. */
|
|
307
|
+
text: string;
|
|
308
|
+
metadata?: Record<string, unknown>;
|
|
309
|
+
}
|
|
310
|
+
/** Paging for {@link VectorStore.listChunks}. Omit both to read the whole document. */
|
|
311
|
+
interface ListChunksOptions {
|
|
312
|
+
/** Max chunks to return, applied after ordering by {@link StoredChunk.index}. */
|
|
313
|
+
limit?: number;
|
|
314
|
+
/** Chunks to skip, applied after ordering. */
|
|
315
|
+
offset?: number;
|
|
316
|
+
}
|
|
317
|
+
/** Chunk `n` of `${documentId}#<n>`; a bare document id (single-chunk document) is `0`. */
|
|
318
|
+
declare function chunkIndexOf(chunkId: string): number;
|
|
274
319
|
/** Collapse a chunk id (`${documentId}#<n>`) back to its source document id. */
|
|
275
320
|
declare function documentIdOf(chunkId: string): string;
|
|
276
321
|
|
|
@@ -296,6 +341,16 @@ declare class MemoryVectorStore implements VectorStore {
|
|
|
296
341
|
*/
|
|
297
342
|
updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;
|
|
298
343
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
344
|
+
/**
|
|
345
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
346
|
+
*
|
|
347
|
+
* Insertion order is not document order — `upsert` stores whatever the caller passed, and a
|
|
348
|
+
* re-ingest that rewrites chunk 3 alone moves it to the end of the Map. So this sorts by the index
|
|
349
|
+
* parsed from the id, the same thing the SQL and Redis adapters order by, rather than trusting the
|
|
350
|
+
* Map. A reference adapter that returned "usually ordered" would let a consumer ship a bug that
|
|
351
|
+
* only shows up on the store tests are NOT written against.
|
|
352
|
+
*/
|
|
353
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
299
354
|
listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;
|
|
300
355
|
removeMany(documentIds: string[]): Promise<void>;
|
|
301
356
|
/**
|
|
@@ -374,6 +429,19 @@ declare class PgVectorStore implements VectorStore {
|
|
|
374
429
|
*/
|
|
375
430
|
updateMetadata(documentId: string, patch: MetadataPatch): Promise<number>;
|
|
376
431
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
432
|
+
/**
|
|
433
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
434
|
+
*
|
|
435
|
+
* The order is computed from the id rather than stored: `#12` must sort after `#2`, which a plain
|
|
436
|
+
* `ORDER BY id` gets wrong on every document past ten chunks because it compares text. So the
|
|
437
|
+
* trailing number is extracted and cast to `int`, and a bare id (a single-chunk document, which
|
|
438
|
+
* carries no `#n` suffix) collapses to `0` — the same rule `chunkIndexOf` applies in the other two
|
|
439
|
+
* adapters.
|
|
440
|
+
*
|
|
441
|
+
* `embedding` is left out of the projection: it is the widest column in the table and useless to a
|
|
442
|
+
* caller reading text back.
|
|
443
|
+
*/
|
|
444
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
377
445
|
/** {@link listDocuments} without the metadata: one `DISTINCT` over the derived document id. */
|
|
378
446
|
listDocumentIds(filter?: Record<string, unknown>): Promise<string[]>;
|
|
379
447
|
/** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */
|
|
@@ -550,6 +618,25 @@ declare class RedisVectorStore implements LexicalVectorStore {
|
|
|
550
618
|
* document owns. The glob is escaped so `gone` does not reach into `goner#0`.
|
|
551
619
|
*/
|
|
552
620
|
private chunkKeys;
|
|
621
|
+
/**
|
|
622
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
623
|
+
*
|
|
624
|
+
* Reads the hashes directly (`HMGET text metadata_json`) rather than going through `FT.SEARCH`.
|
|
625
|
+
* Two reasons, and the second is the one that matters: the query language has no way to ask for
|
|
626
|
+
* "every chunk of this document" — `id` is the key name, not an indexed field — and `FT.SEARCH`
|
|
627
|
+
* caps at `LIMIT`, so paging a document through it would mean ordering by a field the index does
|
|
628
|
+
* not hold. `chunkKeys` is the same enumeration {@link RedisVectorStore.remove} and
|
|
629
|
+
* {@link RedisVectorStore.updateMetadata} already use, so "the chunks this returns" and "the chunks
|
|
630
|
+
* a delete would take" cannot drift apart.
|
|
631
|
+
*
|
|
632
|
+
* It inherits that method's cost too: one keyspace `SCAN` per call. Acceptable here because this
|
|
633
|
+
* is an inspection path (one document, on demand) and not something retrieval runs — but it is why
|
|
634
|
+
* this must not be put on a hot path or called in a loop over a corpus.
|
|
635
|
+
*
|
|
636
|
+
* `embedding` is deliberately not fetched: it is by far the largest field on the hash and nothing
|
|
637
|
+
* a caller reading text back can do with it.
|
|
638
|
+
*/
|
|
639
|
+
listChunks(documentId: string, options?: ListChunksOptions): Promise<StoredChunk[]>;
|
|
553
640
|
listDocuments(filter?: Record<string, unknown>): Promise<IndexedDocument[]>;
|
|
554
641
|
/**
|
|
555
642
|
* The id-only half of {@link RedisVectorStore.listDocuments}, served by `FT.SEARCH … NOCONTENT`:
|
|
@@ -868,4 +955,4 @@ interface RetrievalTool {
|
|
|
868
955
|
*/
|
|
869
956
|
declare function createRetrievalTool(retriever: Retriever, options?: RetrievalToolOptions): RetrievalTool;
|
|
870
957
|
|
|
871
|
-
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|
|
958
|
+
export { type ChunkOptions, type ChunkRecord, EmbeddingRetriever, type EmbeddingRetrieverOptions, FilteredRetriever, HybridRetriever, type HybridRetrieverOptions, type IndexedDocument, type IngestChunksOptions, type IngestDocument, type IngestOptions, KeywordRetriever, type KeywordRetrieverOptions, LexicalRetriever, type LexicalVectorStore, type ListChunksOptions, MemoryVectorStore, type MetadataPatch, type PgClient, PgVectorStore, type PgVectorStoreOptions, type RedisSearchClient, RedisVectorSchemaMismatchError, RedisVectorStore, type RedisVectorStoreOptions, RerankingRetriever, type RerankingRetrieverOptions, type RetrievalTool, type RetrievalToolOptions, type StoredChunk, UnsafeRemovalError, type VectorRecord, type VectorSearchOptions, type VectorStore, applyMetadataPatch, chunkDocuments, chunkIndexOf, chunkText, createRetrievalTool, documentIdOf, ingestChunks, ingestDocuments, isLexicalVectorStore };
|
package/dist/index.js
CHANGED
|
@@ -91,6 +91,11 @@ var UnsafeRemovalError = class extends Error {
|
|
|
91
91
|
this.name = "UnsafeRemovalError";
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
|
+
function chunkIndexOf(chunkId) {
|
|
95
|
+
const match = /#(\d+)$/.exec(chunkId);
|
|
96
|
+
return match?.[1] === void 0 ? 0 : Number(match[1]);
|
|
97
|
+
}
|
|
98
|
+
__name(chunkIndexOf, "chunkIndexOf");
|
|
94
99
|
function documentIdOf(chunkId) {
|
|
95
100
|
return chunkId.replace(/#\d+$/, "");
|
|
96
101
|
}
|
|
@@ -215,6 +220,34 @@ var MemoryVectorStore = class {
|
|
|
215
220
|
...documents.values()
|
|
216
221
|
];
|
|
217
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
225
|
+
*
|
|
226
|
+
* Insertion order is not document order — `upsert` stores whatever the caller passed, and a
|
|
227
|
+
* re-ingest that rewrites chunk 3 alone moves it to the end of the Map. So this sorts by the index
|
|
228
|
+
* parsed from the id, the same thing the SQL and Redis adapters order by, rather than trusting the
|
|
229
|
+
* Map. A reference adapter that returned "usually ordered" would let a consumer ship a bug that
|
|
230
|
+
* only shows up on the store tests are NOT written against.
|
|
231
|
+
*/
|
|
232
|
+
async listChunks(documentId, options) {
|
|
233
|
+
const chunks = [];
|
|
234
|
+
for (const record of this.records.values()) {
|
|
235
|
+
if (documentIdOf(record.id) !== documentId) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
chunks.push({
|
|
239
|
+
id: record.id,
|
|
240
|
+
index: chunkIndexOf(record.id),
|
|
241
|
+
text: record.text,
|
|
242
|
+
...record.metadata !== void 0 ? {
|
|
243
|
+
metadata: record.metadata
|
|
244
|
+
} : {}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
chunks.sort((a, b) => a.index - b.index);
|
|
248
|
+
const offset = options?.offset ?? 0;
|
|
249
|
+
return options?.limit === void 0 ? chunks.slice(offset) : chunks.slice(offset, offset + options.limit);
|
|
250
|
+
}
|
|
218
251
|
async listDocumentIds(filter) {
|
|
219
252
|
if (filterMatchesNothing(filter)) {
|
|
220
253
|
return [];
|
|
@@ -401,6 +434,44 @@ var PgVectorStore = class {
|
|
|
401
434
|
} : {}
|
|
402
435
|
}));
|
|
403
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
439
|
+
*
|
|
440
|
+
* The order is computed from the id rather than stored: `#12` must sort after `#2`, which a plain
|
|
441
|
+
* `ORDER BY id` gets wrong on every document past ten chunks because it compares text. So the
|
|
442
|
+
* trailing number is extracted and cast to `int`, and a bare id (a single-chunk document, which
|
|
443
|
+
* carries no `#n` suffix) collapses to `0` — the same rule `chunkIndexOf` applies in the other two
|
|
444
|
+
* adapters.
|
|
445
|
+
*
|
|
446
|
+
* `embedding` is left out of the projection: it is the widest column in the table and useless to a
|
|
447
|
+
* caller reading text back.
|
|
448
|
+
*/
|
|
449
|
+
async listChunks(documentId, options) {
|
|
450
|
+
const params = [
|
|
451
|
+
documentId
|
|
452
|
+
];
|
|
453
|
+
let sql = `SELECT id, ${CHUNK_INDEX_FROM_ID} AS chunk_index, text, metadata
|
|
454
|
+
FROM ${this.table}
|
|
455
|
+
WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1
|
|
456
|
+
ORDER BY chunk_index`;
|
|
457
|
+
if (options?.limit !== void 0) {
|
|
458
|
+
params.push(options.limit);
|
|
459
|
+
sql += ` LIMIT $${params.length}`;
|
|
460
|
+
}
|
|
461
|
+
if (options?.offset !== void 0) {
|
|
462
|
+
params.push(options.offset);
|
|
463
|
+
sql += ` OFFSET $${params.length}`;
|
|
464
|
+
}
|
|
465
|
+
const rows = await this.client.query(sql, params);
|
|
466
|
+
return rows.map((row) => ({
|
|
467
|
+
id: row.id,
|
|
468
|
+
index: Number(row.chunk_index),
|
|
469
|
+
text: row.text,
|
|
470
|
+
...row.metadata !== null ? {
|
|
471
|
+
metadata: row.metadata
|
|
472
|
+
} : {}
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
404
475
|
/** {@link listDocuments} without the metadata: one `DISTINCT` over the derived document id. */
|
|
405
476
|
async listDocumentIds(filter) {
|
|
406
477
|
if (filterMatchesNothing(filter)) {
|
|
@@ -511,6 +582,7 @@ function buildWhere(filter, startIndex) {
|
|
|
511
582
|
}
|
|
512
583
|
__name(buildWhere, "buildWhere");
|
|
513
584
|
var DOCUMENT_ID_FROM_CHUNK = "regexp_replace(id, '#[0-9]+$', '')";
|
|
585
|
+
var CHUNK_INDEX_FROM_ID = "COALESCE((substring(id from '#([0-9]+)$'))::int, 0)";
|
|
514
586
|
function toVectorLiteral(embedding) {
|
|
515
587
|
return `[${embedding.join(",")}]`;
|
|
516
588
|
}
|
|
@@ -803,6 +875,58 @@ var RedisVectorStore = class {
|
|
|
803
875
|
...keys
|
|
804
876
|
];
|
|
805
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* The chunks of one document, in document order. See {@link VectorStore.listChunks}.
|
|
880
|
+
*
|
|
881
|
+
* Reads the hashes directly (`HMGET text metadata_json`) rather than going through `FT.SEARCH`.
|
|
882
|
+
* Two reasons, and the second is the one that matters: the query language has no way to ask for
|
|
883
|
+
* "every chunk of this document" — `id` is the key name, not an indexed field — and `FT.SEARCH`
|
|
884
|
+
* caps at `LIMIT`, so paging a document through it would mean ordering by a field the index does
|
|
885
|
+
* not hold. `chunkKeys` is the same enumeration {@link RedisVectorStore.remove} and
|
|
886
|
+
* {@link RedisVectorStore.updateMetadata} already use, so "the chunks this returns" and "the chunks
|
|
887
|
+
* a delete would take" cannot drift apart.
|
|
888
|
+
*
|
|
889
|
+
* It inherits that method's cost too: one keyspace `SCAN` per call. Acceptable here because this
|
|
890
|
+
* is an inspection path (one document, on demand) and not something retrieval runs — but it is why
|
|
891
|
+
* this must not be put on a hot path or called in a loop over a corpus.
|
|
892
|
+
*
|
|
893
|
+
* `embedding` is deliberately not fetched: it is by far the largest field on the hash and nothing
|
|
894
|
+
* a caller reading text back can do with it.
|
|
895
|
+
*/
|
|
896
|
+
async listChunks(documentId, options) {
|
|
897
|
+
const keys = await this.chunkKeys(documentId);
|
|
898
|
+
const ordered = keys.map((key) => ({
|
|
899
|
+
key,
|
|
900
|
+
id: key.slice(this.prefix.length)
|
|
901
|
+
})).map((entry) => ({
|
|
902
|
+
...entry,
|
|
903
|
+
index: chunkIndexOf(entry.id)
|
|
904
|
+
})).sort((a, b) => a.index - b.index);
|
|
905
|
+
const offset = options?.offset ?? 0;
|
|
906
|
+
const page = options?.limit === void 0 ? ordered.slice(offset) : ordered.slice(offset, offset + options.limit);
|
|
907
|
+
const chunks = [];
|
|
908
|
+
for (const entry of page) {
|
|
909
|
+
const reply = await this.client.sendCommand([
|
|
910
|
+
"HMGET",
|
|
911
|
+
entry.key,
|
|
912
|
+
"text",
|
|
913
|
+
"metadata_json"
|
|
914
|
+
]);
|
|
915
|
+
const fields = Array.isArray(reply) ? reply : [];
|
|
916
|
+
const text = fields[0] === null || fields[0] === void 0 ? void 0 : toStr(fields[0]);
|
|
917
|
+
if (text === void 0) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
const raw = fields[1] === null || fields[1] === void 0 ? void 0 : toStr(fields[1]);
|
|
921
|
+
chunks.push({
|
|
922
|
+
id: entry.id,
|
|
923
|
+
index: entry.index,
|
|
924
|
+
text,
|
|
925
|
+
...parseMetadata(raw)
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
return chunks;
|
|
929
|
+
}
|
|
806
930
|
async listDocuments(filter) {
|
|
807
931
|
if (filterMatchesNothing(filter)) {
|
|
808
932
|
return [];
|
|
@@ -1692,6 +1816,7 @@ export {
|
|
|
1692
1816
|
UnsafeRemovalError,
|
|
1693
1817
|
applyMetadataPatch,
|
|
1694
1818
|
chunkDocuments,
|
|
1819
|
+
chunkIndexOf,
|
|
1695
1820
|
chunkText,
|
|
1696
1821
|
createRetrievalTool,
|
|
1697
1822
|
documentIdOf,
|