@dudousxd/nestjs-agent-rag 0.4.1 → 0.6.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 +208 -20
- package/dist/index.cjs +634 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +422 -2
- package/dist/index.d.ts +422 -2
- package/dist/index.js +629 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -25,17 +25,21 @@ __export(index_exports, {
|
|
|
25
25
|
FilteredRetriever: () => FilteredRetriever,
|
|
26
26
|
HybridRetriever: () => HybridRetriever,
|
|
27
27
|
KeywordRetriever: () => KeywordRetriever,
|
|
28
|
+
LexicalRetriever: () => LexicalRetriever,
|
|
28
29
|
MemoryVectorStore: () => MemoryVectorStore,
|
|
29
30
|
PgVectorStore: () => PgVectorStore,
|
|
30
31
|
RedisVectorSchemaMismatchError: () => RedisVectorSchemaMismatchError,
|
|
31
32
|
RedisVectorStore: () => RedisVectorStore,
|
|
32
33
|
RerankingRetriever: () => RerankingRetriever,
|
|
34
|
+
UnsafeRemovalError: () => UnsafeRemovalError,
|
|
35
|
+
applyMetadataPatch: () => applyMetadataPatch,
|
|
33
36
|
chunkDocuments: () => chunkDocuments,
|
|
34
37
|
chunkText: () => chunkText,
|
|
35
38
|
createRetrievalTool: () => createRetrievalTool,
|
|
36
39
|
documentIdOf: () => documentIdOf,
|
|
37
40
|
ingestChunks: () => ingestChunks,
|
|
38
|
-
ingestDocuments: () => ingestDocuments
|
|
41
|
+
ingestDocuments: () => ingestDocuments,
|
|
42
|
+
isLexicalVectorStore: () => isLexicalVectorStore
|
|
39
43
|
});
|
|
40
44
|
module.exports = __toCommonJS(index_exports);
|
|
41
45
|
|
|
@@ -78,11 +82,60 @@ function chunkText(text, options = {}) {
|
|
|
78
82
|
__name(chunkText, "chunkText");
|
|
79
83
|
|
|
80
84
|
// src/vector-store.ts
|
|
85
|
+
function isLexicalVectorStore(store) {
|
|
86
|
+
return typeof store.searchText === "function";
|
|
87
|
+
}
|
|
88
|
+
__name(isLexicalVectorStore, "isLexicalVectorStore");
|
|
89
|
+
var UnsafeRemovalError = class extends Error {
|
|
90
|
+
static {
|
|
91
|
+
__name(this, "UnsafeRemovalError");
|
|
92
|
+
}
|
|
93
|
+
reason;
|
|
94
|
+
constructor(reason, message) {
|
|
95
|
+
super(message), this.reason = reason;
|
|
96
|
+
this.name = "UnsafeRemovalError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
81
99
|
function documentIdOf(chunkId) {
|
|
82
100
|
return chunkId.replace(/#\d+$/, "");
|
|
83
101
|
}
|
|
84
102
|
__name(documentIdOf, "documentIdOf");
|
|
85
103
|
|
|
104
|
+
// src/metadata-patch.ts
|
|
105
|
+
function splitMetadataPatch(patch) {
|
|
106
|
+
const set = {};
|
|
107
|
+
const remove = [];
|
|
108
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
109
|
+
if (value === null) {
|
|
110
|
+
remove.push(key);
|
|
111
|
+
} else if (value !== void 0) {
|
|
112
|
+
set[key] = value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
set,
|
|
117
|
+
remove
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
__name(splitMetadataPatch, "splitMetadataPatch");
|
|
121
|
+
function isEmptyMetadataPatch(patch) {
|
|
122
|
+
const { set, remove } = splitMetadataPatch(patch);
|
|
123
|
+
return Object.keys(set).length === 0 && remove.length === 0;
|
|
124
|
+
}
|
|
125
|
+
__name(isEmptyMetadataPatch, "isEmptyMetadataPatch");
|
|
126
|
+
function applyMetadataPatch(metadata, patch) {
|
|
127
|
+
const { set, remove } = splitMetadataPatch(patch);
|
|
128
|
+
const next = {
|
|
129
|
+
...metadata,
|
|
130
|
+
...set
|
|
131
|
+
};
|
|
132
|
+
for (const key of remove) {
|
|
133
|
+
delete next[key];
|
|
134
|
+
}
|
|
135
|
+
return next;
|
|
136
|
+
}
|
|
137
|
+
__name(applyMetadataPatch, "applyMetadataPatch");
|
|
138
|
+
|
|
86
139
|
// src/filter.ts
|
|
87
140
|
function matchesFilter(metadata, filter) {
|
|
88
141
|
const entries = Object.entries(filter);
|
|
@@ -98,6 +151,13 @@ function matchesFilter(metadata, filter) {
|
|
|
98
151
|
});
|
|
99
152
|
}
|
|
100
153
|
__name(matchesFilter, "matchesFilter");
|
|
154
|
+
function filterMatchesNothing(filter) {
|
|
155
|
+
if (filter === void 0) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return Object.values(filter).some((value) => Array.isArray(value) && value.length === 0);
|
|
159
|
+
}
|
|
160
|
+
__name(filterMatchesNothing, "filterMatchesNothing");
|
|
101
161
|
|
|
102
162
|
// src/memory-vector-store.ts
|
|
103
163
|
var MemoryVectorStore = class {
|
|
@@ -117,6 +177,29 @@ var MemoryVectorStore = class {
|
|
|
117
177
|
}
|
|
118
178
|
}
|
|
119
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Merge `patch` into every chunk of `documentId`, leaving `text` and `embedding` untouched. The
|
|
182
|
+
* record is *replaced* rather than mutated: `upsert` stores the caller's own object, and patching
|
|
183
|
+
* it in place would edit an object the caller still holds — a surprise no out-of-process store
|
|
184
|
+
* could reproduce, and therefore one this reference adapter must not teach.
|
|
185
|
+
*/
|
|
186
|
+
async updateMetadata(documentId, patch) {
|
|
187
|
+
if (isEmptyMetadataPatch(patch)) {
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
let updated = 0;
|
|
191
|
+
for (const [id, record] of this.records) {
|
|
192
|
+
if (documentIdOf(id) !== documentId) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
this.records.set(id, {
|
|
196
|
+
...record,
|
|
197
|
+
metadata: applyMetadataPatch(record.metadata, patch)
|
|
198
|
+
});
|
|
199
|
+
updated += 1;
|
|
200
|
+
}
|
|
201
|
+
return updated;
|
|
202
|
+
}
|
|
120
203
|
async listDocuments(filter) {
|
|
121
204
|
const documents = /* @__PURE__ */ new Map();
|
|
122
205
|
for (const record of this.records.values()) {
|
|
@@ -137,6 +220,66 @@ var MemoryVectorStore = class {
|
|
|
137
220
|
...documents.values()
|
|
138
221
|
];
|
|
139
222
|
}
|
|
223
|
+
async listDocumentIds(filter) {
|
|
224
|
+
if (filterMatchesNothing(filter)) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const documents = /* @__PURE__ */ new Set();
|
|
228
|
+
for (const record of this.records.values()) {
|
|
229
|
+
if (filter !== void 0 && !matchesFilter(record.metadata, filter)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
documents.add(documentIdOf(record.id));
|
|
233
|
+
}
|
|
234
|
+
return [
|
|
235
|
+
...documents
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
async removeMany(documentIds) {
|
|
239
|
+
if (documentIds.length === 0) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const wanted = new Set(documentIds);
|
|
243
|
+
for (const id of this.records.keys()) {
|
|
244
|
+
if (wanted.has(documentIdOf(id))) {
|
|
245
|
+
this.records.delete(id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* See {@link VectorStore.removeWhere}. The empty-array short-circuit below is redundant
|
|
251
|
+
* against `matchesFilter` (which already matches nothing for an empty array) and is kept anyway:
|
|
252
|
+
* this is the method where "the filter accidentally means everything" destroys data, so the deny is
|
|
253
|
+
* stated where a reader can see it rather than inferred from another module's truth table.
|
|
254
|
+
*/
|
|
255
|
+
async removeWhere(filter) {
|
|
256
|
+
if (Object.keys(filter).length === 0) {
|
|
257
|
+
throw new UnsafeRemovalError("empty-filter", "removeWhere() refuses an empty filter: it would delete every chunk in the store. Pass a filter that scopes the removal, or delete deliberately with removeMany(await store.listDocumentIds()).");
|
|
258
|
+
}
|
|
259
|
+
if (filterMatchesNothing(filter)) {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
let removed = 0;
|
|
263
|
+
for (const [id, record] of this.records) {
|
|
264
|
+
if (matchesFilter(record.metadata, filter)) {
|
|
265
|
+
this.records.delete(id);
|
|
266
|
+
removed += 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return removed;
|
|
270
|
+
}
|
|
271
|
+
async countChunks(filter) {
|
|
272
|
+
if (filterMatchesNothing(filter)) {
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
let count = 0;
|
|
276
|
+
for (const record of this.records.values()) {
|
|
277
|
+
if (filter === void 0 || matchesFilter(record.metadata, filter)) {
|
|
278
|
+
count += 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return count;
|
|
282
|
+
}
|
|
140
283
|
async search(embedding, options) {
|
|
141
284
|
const scored = [];
|
|
142
285
|
for (const record of this.records.values()) {
|
|
@@ -223,6 +366,33 @@ var PgVectorStore = class {
|
|
|
223
366
|
documentId
|
|
224
367
|
]);
|
|
225
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Rewrite a document's metadata without re-embedding it. See {@link VectorStore.updateMetadata}
|
|
371
|
+
* for the semantics; here the whole thing is one statement, because jsonb already *is* the merge:
|
|
372
|
+
* `||` is a shallow key-wise merge (right side wins, arrays replaced wholesale — precisely
|
|
373
|
+
* {@link MetadataPatch}) and `- text[]` drops the keys the patch removed. `COALESCE` covers a chunk
|
|
374
|
+
* ingested with no metadata at all, so a patch can create the object rather than no-op on `NULL`.
|
|
375
|
+
*
|
|
376
|
+
* Unlike RediSearch there is no second representation to keep in step — the `metadata` column is
|
|
377
|
+
* both what `search` filters on and what it returns — so the correctness trap the Redis adapter has
|
|
378
|
+
* to work for simply does not exist here. `RETURNING id` is what supplies the chunk count, since
|
|
379
|
+
* {@link PgClient} is a rows-only surface with no `rowCount`.
|
|
380
|
+
*/
|
|
381
|
+
async updateMetadata(documentId, patch) {
|
|
382
|
+
if (isEmptyMetadataPatch(patch)) {
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
const { set, remove } = splitMetadataPatch(patch);
|
|
386
|
+
const rows = await this.client.query(`UPDATE ${this.table}
|
|
387
|
+
SET metadata = (COALESCE(metadata, '{}'::jsonb) || $2::jsonb) - $3::text[]
|
|
388
|
+
WHERE ${DOCUMENT_ID_FROM_CHUNK} = $1
|
|
389
|
+
RETURNING id`, [
|
|
390
|
+
documentId,
|
|
391
|
+
JSON.stringify(set),
|
|
392
|
+
remove
|
|
393
|
+
]);
|
|
394
|
+
return rows.length;
|
|
395
|
+
}
|
|
226
396
|
async listDocuments(filter) {
|
|
227
397
|
const where = buildWhere(filter, 1);
|
|
228
398
|
const rows = await this.client.query(`SELECT DISTINCT ON (${DOCUMENT_ID_FROM_CHUNK}) ${DOCUMENT_ID_FROM_CHUNK} AS doc_id, metadata
|
|
@@ -236,6 +406,51 @@ var PgVectorStore = class {
|
|
|
236
406
|
} : {}
|
|
237
407
|
}));
|
|
238
408
|
}
|
|
409
|
+
/** {@link listDocuments} without the metadata: one `DISTINCT` over the derived document id. */
|
|
410
|
+
async listDocumentIds(filter) {
|
|
411
|
+
if (filterMatchesNothing(filter)) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
const where = buildWhere(filter, 1);
|
|
415
|
+
const rows = await this.client.query(`SELECT DISTINCT ${DOCUMENT_ID_FROM_CHUNK} AS doc_id
|
|
416
|
+
FROM ${this.table}
|
|
417
|
+
${where.sql}
|
|
418
|
+
ORDER BY doc_id`, where.params);
|
|
419
|
+
return rows.map((row) => row.doc_id);
|
|
420
|
+
}
|
|
421
|
+
/** N documents in one statement — the set-based form of {@link PgVectorStore.remove}. */
|
|
422
|
+
async removeMany(documentIds) {
|
|
423
|
+
if (documentIds.length === 0) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
await this.client.query(`DELETE FROM ${this.table} WHERE ${DOCUMENT_ID_FROM_CHUNK} = ANY($1::text[])`, [
|
|
427
|
+
documentIds
|
|
428
|
+
]);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* See {@link VectorStore.removeWhere}: empty filter object throws, empty-array value
|
|
432
|
+
* deletes nothing. The count comes from `RETURNING id` rather than a driver-specific `rowCount`,
|
|
433
|
+
* because {@link PgClient} is deliberately just "run SQL, get rows".
|
|
434
|
+
*/
|
|
435
|
+
async removeWhere(filter) {
|
|
436
|
+
if (Object.keys(filter).length === 0) {
|
|
437
|
+
throw new UnsafeRemovalError("empty-filter", "removeWhere() refuses an empty filter: it would delete every chunk in the table. Pass a filter that scopes the removal, or delete deliberately with removeMany(await store.listDocumentIds()).");
|
|
438
|
+
}
|
|
439
|
+
if (filterMatchesNothing(filter)) {
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
const where = buildWhere(filter, 1);
|
|
443
|
+
const rows = await this.client.query(`DELETE FROM ${this.table} ${where.sql} RETURNING id`, where.params);
|
|
444
|
+
return rows.length;
|
|
445
|
+
}
|
|
446
|
+
async countChunks(filter) {
|
|
447
|
+
if (filterMatchesNothing(filter)) {
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
450
|
+
const where = buildWhere(filter, 1);
|
|
451
|
+
const rows = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table} ${where.sql}`, where.params);
|
|
452
|
+
return Number(rows[0]?.count ?? 0);
|
|
453
|
+
}
|
|
239
454
|
async search(embedding, options) {
|
|
240
455
|
const vector = toVectorLiteral(embedding);
|
|
241
456
|
const where = buildWhere(options.filter, 3);
|
|
@@ -329,12 +544,14 @@ var RedisVectorStore = class {
|
|
|
329
544
|
prefix;
|
|
330
545
|
dimensions;
|
|
331
546
|
filterableFields;
|
|
547
|
+
lexicalScorer;
|
|
332
548
|
constructor(client, options = {}) {
|
|
333
549
|
this.client = client;
|
|
334
550
|
this.index = options.index ?? "agent_rag_idx";
|
|
335
551
|
this.prefix = options.prefix ?? "agent_rag:";
|
|
336
552
|
this.dimensions = options.dimensions ?? 1536;
|
|
337
553
|
this.filterableFields = options.filterableFields ?? [];
|
|
554
|
+
this.lexicalScorer = options.lexicalScorer ?? "BM25";
|
|
338
555
|
}
|
|
339
556
|
/**
|
|
340
557
|
* Idempotent, and drift-aware: create the HNSW/cosine index if `FT.INFO` says it doesn't exist —
|
|
@@ -447,18 +664,129 @@ var RedisVectorStore = class {
|
|
|
447
664
|
for (const field of this.filterableFields) {
|
|
448
665
|
const value = record.metadata[field];
|
|
449
666
|
if (value !== void 0) {
|
|
450
|
-
|
|
451
|
-
args.push(`meta_${field}`, tag);
|
|
667
|
+
args.push(`meta_${field}`, encodeTag(value));
|
|
452
668
|
}
|
|
453
669
|
}
|
|
454
670
|
}
|
|
455
671
|
await this.client.sendCommand(args);
|
|
456
672
|
}
|
|
457
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Single-document deletion, by `SCAN … MATCH`.
|
|
676
|
+
*
|
|
677
|
+
* This deliberately stays a keyspace scan even though the rest of this class no longer needs one.
|
|
678
|
+
* The obvious optimisation is to stamp each chunk with its document id as an implicit filterable
|
|
679
|
+
* TAG at {@link RedisVectorStore.upsert} and turn `remove` into an `FT.SEARCH` — and that is exactly
|
|
680
|
+
* the change that must not ship, because chunks written *before* it carry no such field. `FT.SEARCH`
|
|
681
|
+
* would find zero of them, `remove` would return happily, and a document that has been "deleted"
|
|
682
|
+
* would go on being retrieved. A delete that silently stops deleting is worse than a slow one, and
|
|
683
|
+
* the library cannot tell a fully-migrated corpus from an unmigrated one: both look like "this
|
|
684
|
+
* document has no indexed chunks". Nor is a search-then-fall-back-to-scan hybrid safe — a document
|
|
685
|
+
* re-ingested after the upgrade has *some* stamped chunks, so the search finds hits, the fallback
|
|
686
|
+
* never runs, and the unstamped tail survives. That is the same silent failure with a smaller blast
|
|
687
|
+
* radius, which makes it harder to notice rather than less wrong.
|
|
688
|
+
*
|
|
689
|
+
* The consumer pain this was measured against — deleting a whole collection, one `remove` per
|
|
690
|
+
* document — is answered instead by {@link RedisVectorStore.removeWhere} (one filtered query, no
|
|
691
|
+
* keyspace scan at all) and {@link RedisVectorStore.removeMany} (one scan for N documents rather
|
|
692
|
+
* than N scans). Neither depends on a field that older chunks lack, so neither has a migration.
|
|
693
|
+
*/
|
|
458
694
|
async remove(documentId) {
|
|
459
|
-
const keys =
|
|
460
|
-
|
|
695
|
+
const keys = await this.chunkKeys(documentId);
|
|
696
|
+
if (keys.length > 0) {
|
|
697
|
+
await this.client.sendCommand([
|
|
698
|
+
"DEL",
|
|
699
|
+
...keys
|
|
700
|
+
]);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Rewrite the metadata of every chunk of `documentId` in place, leaving `text` and `embedding`
|
|
705
|
+
* alone. See {@link VectorStore.updateMetadata} for the merge semantics and the return value.
|
|
706
|
+
*
|
|
707
|
+
* The care this needs is entirely about the fact that a chunk stores its metadata **twice**: as the
|
|
708
|
+
* `metadata_json` blob (what {@link RedisVectorStore.search} reads back onto a `Passage`) and as
|
|
709
|
+
* `meta_<field>` TAG fields (what RediSearch actually filters on, and only for the declared
|
|
710
|
+
* `filterableFields`). Move one without the other and the store lies to itself — a chunk that
|
|
711
|
+
* filters as the old value but reports the new one, or the reverse — so this method rewrites both
|
|
712
|
+
* from the same merged object:
|
|
713
|
+
*
|
|
714
|
+
* - a patched key that is **not** filterable lands in `metadata_json` only, and no stray `meta_*`
|
|
715
|
+
* field is invented for it (it would be dead weight the index does not know about);
|
|
716
|
+
* - a patched key that **is** filterable also rewrites its TAG, using the identical encoding
|
|
717
|
+
* `upsert` uses, so an updated chunk is indistinguishable from a re-ingested one;
|
|
718
|
+
* - a filterable key the patch **removes** has its TAG `HDEL`-ed, not left behind as a tag matching
|
|
719
|
+
* a value the document no longer carries.
|
|
720
|
+
*
|
|
721
|
+
* A plain `HSET` is all the index needs: RediSearch re-indexes a hash the moment it is written, so
|
|
722
|
+
* the very next `FT.SEARCH` sees the new TAG and no longer the old one — no reindex command, and
|
|
723
|
+
* no need to rewrite the (untouched, and expensive) vector to nudge it. `HDEL` on an indexed field
|
|
724
|
+
* likewise drops it from the index. Verified against a real Redis Stack in
|
|
725
|
+
* `redis-vector-store.db.spec.ts`, not assumed.
|
|
726
|
+
*
|
|
727
|
+
* Removals are issued **before** the write, so the only window a concurrent reader can observe is
|
|
728
|
+
* "TAG already gone, `metadata_json` not yet updated" — which fails *closed* on a filter carrying
|
|
729
|
+
* an ACL. The reverse order would briefly leave a document tagged with a capability its metadata no
|
|
730
|
+
* longer claims. Read-modify-write across chunks is not transactional; concurrent writers to the
|
|
731
|
+
* same chunk are last-writer-wins, exactly as two concurrent `upsert`s already are.
|
|
732
|
+
*/
|
|
733
|
+
async updateMetadata(documentId, patch) {
|
|
734
|
+
if (isEmptyMetadataPatch(patch)) {
|
|
735
|
+
return 0;
|
|
736
|
+
}
|
|
737
|
+
const keys = await this.chunkKeys(documentId);
|
|
738
|
+
let updated = 0;
|
|
739
|
+
for (const key of keys) {
|
|
740
|
+
const raw = await this.client.sendCommand([
|
|
741
|
+
"HGET",
|
|
742
|
+
key,
|
|
743
|
+
"metadata_json"
|
|
744
|
+
]);
|
|
745
|
+
const current = parseMetadata(raw === null || raw === void 0 ? void 0 : toStr(raw));
|
|
746
|
+
const next = applyMetadataPatch(current.metadata, patch);
|
|
747
|
+
const args = [
|
|
748
|
+
"HSET",
|
|
749
|
+
key,
|
|
750
|
+
"metadata_json",
|
|
751
|
+
JSON.stringify(next)
|
|
752
|
+
];
|
|
753
|
+
const stale = [];
|
|
754
|
+
for (const field of this.filterableFields) {
|
|
755
|
+
const value = next[field];
|
|
756
|
+
if (value !== void 0) {
|
|
757
|
+
args.push(`meta_${field}`, encodeTag(value));
|
|
758
|
+
} else if (current.metadata?.[field] !== void 0) {
|
|
759
|
+
stale.push(`meta_${field}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (stale.length > 0) {
|
|
763
|
+
await this.client.sendCommand([
|
|
764
|
+
"HDEL",
|
|
765
|
+
key,
|
|
766
|
+
...stale
|
|
767
|
+
]);
|
|
768
|
+
}
|
|
769
|
+
await this.client.sendCommand(args);
|
|
770
|
+
updated += 1;
|
|
771
|
+
}
|
|
772
|
+
return updated;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Every chunk key that currently EXISTS for `documentId` — the bare `${prefix}${documentId}` plus
|
|
776
|
+
* the `${documentId}#<n>` chunks found by `SCAN`. One definition of "chunk belongs to document",
|
|
777
|
+
* shared by `remove` and `updateMetadata`, so the two can never disagree about which keys a
|
|
778
|
+
* document owns. The glob is escaped so `gone` does not reach into `goner#0`.
|
|
779
|
+
*/
|
|
780
|
+
async chunkKeys(documentId) {
|
|
781
|
+
const keys = /* @__PURE__ */ new Set();
|
|
782
|
+
const bare = `${this.prefix}${documentId}`;
|
|
783
|
+
const exists = await this.client.sendCommand([
|
|
784
|
+
"EXISTS",
|
|
785
|
+
bare
|
|
461
786
|
]);
|
|
787
|
+
if (Number(toStr(exists)) > 0) {
|
|
788
|
+
keys.add(bare);
|
|
789
|
+
}
|
|
462
790
|
const pattern = `${this.prefix}${escapeGlob(documentId)}#*`;
|
|
463
791
|
let cursor = "0";
|
|
464
792
|
do {
|
|
@@ -476,12 +804,9 @@ var RedisVectorStore = class {
|
|
|
476
804
|
keys.add(key);
|
|
477
805
|
}
|
|
478
806
|
} while (cursor !== "0");
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
...keys
|
|
483
|
-
]);
|
|
484
|
-
}
|
|
807
|
+
return [
|
|
808
|
+
...keys
|
|
809
|
+
];
|
|
485
810
|
}
|
|
486
811
|
async listDocuments(filter) {
|
|
487
812
|
if (filterMatchesNothing(filter)) {
|
|
@@ -526,6 +851,186 @@ var RedisVectorStore = class {
|
|
|
526
851
|
...documents.values()
|
|
527
852
|
];
|
|
528
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* The id-only half of {@link RedisVectorStore.listDocuments}, served by `FT.SEARCH … NOCONTENT`:
|
|
856
|
+
* RediSearch returns bare keys, so nothing carries `metadata_json` over the wire and nothing is
|
|
857
|
+
* JSON-parsed. `listDocuments` has to parse one metadata blob per *chunk* — thousands of parses to
|
|
858
|
+
* produce a few hundred ids it then throws the metadata away from — and this does none of it.
|
|
859
|
+
*
|
|
860
|
+
* The collapse to document ids still happens client-side. RediSearch can group server-side
|
|
861
|
+
* (`FT.AGGREGATE … GROUPBY`), but only over an indexed *field*, and a chunk's document id lives in
|
|
862
|
+
* its key, not in a field — deriving it would mean stamping one at write time, which is the
|
|
863
|
+
* migration {@link RedisVectorStore.remove} explains this class refuses. Deduplicating a page of
|
|
864
|
+
* keys in JS is free by comparison.
|
|
865
|
+
*/
|
|
866
|
+
async listDocumentIds(filter) {
|
|
867
|
+
if (filterMatchesNothing(filter)) {
|
|
868
|
+
return [];
|
|
869
|
+
}
|
|
870
|
+
const query = buildFilter(filter);
|
|
871
|
+
const documents = /* @__PURE__ */ new Set();
|
|
872
|
+
let offset = 0;
|
|
873
|
+
for (; ; ) {
|
|
874
|
+
const reply = await this.client.sendCommand([
|
|
875
|
+
"FT.SEARCH",
|
|
876
|
+
this.index,
|
|
877
|
+
query,
|
|
878
|
+
"NOCONTENT",
|
|
879
|
+
"LIMIT",
|
|
880
|
+
String(offset),
|
|
881
|
+
String(SCAN_PAGE),
|
|
882
|
+
"DIALECT",
|
|
883
|
+
"2"
|
|
884
|
+
]);
|
|
885
|
+
const keys = parseSearchKeys(reply);
|
|
886
|
+
for (const key of keys) {
|
|
887
|
+
documents.add(documentIdOf(stripPrefix(key, this.prefix)));
|
|
888
|
+
}
|
|
889
|
+
if (keys.length < SCAN_PAGE) {
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
offset += SCAN_PAGE;
|
|
893
|
+
}
|
|
894
|
+
return [
|
|
895
|
+
...documents
|
|
896
|
+
];
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Bulk deletion by document id — **one** keyspace scan for N documents, where N calls to
|
|
900
|
+
* {@link RedisVectorStore.remove} would be N of them. The scan is `MATCH ${prefix}*`, so Redis still
|
|
901
|
+
* filters to this store's own keys server-side; membership is then decided in JS against a `Set`,
|
|
902
|
+
* which is what lets a single pass serve any number of ids.
|
|
903
|
+
*
|
|
904
|
+
* It deletes by key, exactly as `remove` does, so it is correct on chunks written by any version of
|
|
905
|
+
* this library — there is no field it needs the corpus to already carry.
|
|
906
|
+
*/
|
|
907
|
+
async removeMany(documentIds) {
|
|
908
|
+
if (documentIds.length === 0) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const wanted = new Set(documentIds);
|
|
912
|
+
let pending = documentIds.map((id) => `${this.prefix}${id}`);
|
|
913
|
+
let cursor = "0";
|
|
914
|
+
do {
|
|
915
|
+
const reply = await this.client.sendCommand([
|
|
916
|
+
"SCAN",
|
|
917
|
+
cursor,
|
|
918
|
+
"MATCH",
|
|
919
|
+
`${escapeGlob(this.prefix)}*`,
|
|
920
|
+
"COUNT",
|
|
921
|
+
String(SCAN_PAGE)
|
|
922
|
+
]);
|
|
923
|
+
const page = parseScanReply(reply);
|
|
924
|
+
cursor = page.cursor;
|
|
925
|
+
for (const key of page.keys) {
|
|
926
|
+
if (wanted.has(documentIdOf(stripPrefix(key, this.prefix)))) {
|
|
927
|
+
pending.push(key);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (pending.length >= DELETE_BATCH) {
|
|
931
|
+
await this.client.sendCommand([
|
|
932
|
+
"DEL",
|
|
933
|
+
...pending
|
|
934
|
+
]);
|
|
935
|
+
pending = [];
|
|
936
|
+
}
|
|
937
|
+
} while (cursor !== "0");
|
|
938
|
+
if (pending.length > 0) {
|
|
939
|
+
await this.client.sendCommand([
|
|
940
|
+
"DEL",
|
|
941
|
+
...pending
|
|
942
|
+
]);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Delete every chunk matching `filter`, via `FT.SEARCH … NOCONTENT` + `DEL` — no keyspace scan at
|
|
947
|
+
* all, so dropping a collection costs one indexed query per page instead of one full pass per
|
|
948
|
+
* document. Resolves the number of chunk keys actually deleted.
|
|
949
|
+
*
|
|
950
|
+
* The two refusals, in this order, before any query is issued:
|
|
951
|
+
*
|
|
952
|
+
* 1. **An empty filter object throws** {@link UnsafeRemovalError}. `buildFilter({})` is `*`, and `*`
|
|
953
|
+
* here would mean "delete the corpus" — far too destructive an outcome for the shape a filter
|
|
954
|
+
* degenerates to when it is built wrong.
|
|
955
|
+
* 2. **An empty-array value deletes nothing** and resolves `0`. This is the package's deny primitive
|
|
956
|
+
* (an actor with no capability tokens), and it is the one that has to be gated *explicitly*: a
|
|
957
|
+
* plausible-looking "normalise the filter first" refactor that drops empty arrays turns a deny
|
|
958
|
+
* into `*` and wipes everything the deny existed to protect.
|
|
959
|
+
*
|
|
960
|
+
* Then one more, which is specific to RediSearch: every filter key must be a declared
|
|
961
|
+
* `filterableFields` entry. `search` lets the engine reject an unknown field, which is fine for a
|
|
962
|
+
* read; for a delete the check is worth making up front and by name.
|
|
963
|
+
*
|
|
964
|
+
* Deletion happens page by page, always re-querying from offset 0 — the rows just deleted are gone
|
|
965
|
+
* from the index, so paging forward would skip their replacements. If a page comes back non-empty
|
|
966
|
+
* but `DEL` reports nothing removed (keys expired between query and delete, or the index is stale),
|
|
967
|
+
* the loop stops rather than spinning on results it can never consume.
|
|
968
|
+
*/
|
|
969
|
+
async removeWhere(filter) {
|
|
970
|
+
const keys = Object.keys(filter);
|
|
971
|
+
if (keys.length === 0) {
|
|
972
|
+
throw new UnsafeRemovalError("empty-filter", "removeWhere() refuses an empty filter: it would delete every chunk in the index. Pass a filter that scopes the removal, or delete deliberately with removeMany(await store.listDocumentIds()).");
|
|
973
|
+
}
|
|
974
|
+
const unindexed = keys.filter((key) => !this.filterableFields.includes(key));
|
|
975
|
+
if (unindexed.length > 0) {
|
|
976
|
+
const named = unindexed.map((key) => `"${key}"`).join(", ");
|
|
977
|
+
const declared = this.filterableFields.join(", ") || "none";
|
|
978
|
+
throw new UnsafeRemovalError("unindexed-field", `removeWhere() cannot scope on ${named}: not declared in this store's filterableFields (${declared}), so RediSearch has no TAG to match on.`);
|
|
979
|
+
}
|
|
980
|
+
if (filterMatchesNothing(filter)) {
|
|
981
|
+
return 0;
|
|
982
|
+
}
|
|
983
|
+
const query = buildFilter(filter);
|
|
984
|
+
let removed = 0;
|
|
985
|
+
for (; ; ) {
|
|
986
|
+
const reply = await this.client.sendCommand([
|
|
987
|
+
"FT.SEARCH",
|
|
988
|
+
this.index,
|
|
989
|
+
query,
|
|
990
|
+
"NOCONTENT",
|
|
991
|
+
"LIMIT",
|
|
992
|
+
"0",
|
|
993
|
+
String(DELETE_BATCH),
|
|
994
|
+
"DIALECT",
|
|
995
|
+
"2"
|
|
996
|
+
]);
|
|
997
|
+
const page = parseSearchKeys(reply);
|
|
998
|
+
if (page.length === 0) {
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
const deleted = Number(await this.client.sendCommand([
|
|
1002
|
+
"DEL",
|
|
1003
|
+
...page
|
|
1004
|
+
]));
|
|
1005
|
+
if (!Number.isFinite(deleted) || deleted === 0) {
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
removed += deleted;
|
|
1009
|
+
}
|
|
1010
|
+
return removed;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Count matching chunks with `FT.SEARCH … LIMIT 0 0` — RediSearch returns the total and no
|
|
1014
|
+
* documents, so the cost is the query, not the corpus. An empty-array filter value denies and
|
|
1015
|
+
* yields `0`, same as every other filtered path here.
|
|
1016
|
+
*/
|
|
1017
|
+
async countChunks(filter) {
|
|
1018
|
+
if (filterMatchesNothing(filter)) {
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
const reply = await this.client.sendCommand([
|
|
1022
|
+
"FT.SEARCH",
|
|
1023
|
+
this.index,
|
|
1024
|
+
buildFilter(filter),
|
|
1025
|
+
"NOCONTENT",
|
|
1026
|
+
"LIMIT",
|
|
1027
|
+
"0",
|
|
1028
|
+
"0",
|
|
1029
|
+
"DIALECT",
|
|
1030
|
+
"2"
|
|
1031
|
+
]);
|
|
1032
|
+
return parseSearchTotal(reply);
|
|
1033
|
+
}
|
|
529
1034
|
async search(embedding, options) {
|
|
530
1035
|
if (filterMatchesNothing(options.filter)) {
|
|
531
1036
|
return [];
|
|
@@ -556,7 +1061,77 @@ var RedisVectorStore = class {
|
|
|
556
1061
|
]);
|
|
557
1062
|
return parseSearchReply(reply, this.prefix);
|
|
558
1063
|
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Lexical (BM25) search over the same index `search` uses — the {@link LexicalVectorStore} half of
|
|
1066
|
+
* hybrid retrieval, served by RediSearch's own keyword index rather than an in-process copy of the
|
|
1067
|
+
* corpus. `options.filter` is ANDed onto the text clause with the identical TAG semantics as
|
|
1068
|
+
* `search` (array = OR, empty array = deny), so a lexical hit can never escape a filter a vector
|
|
1069
|
+
* hit would have respected.
|
|
1070
|
+
*
|
|
1071
|
+
* A query with no searchable terms — empty, whitespace, or pure punctuation — returns `[]`. That is
|
|
1072
|
+
* deliberate: RediSearch's `*` would match the entire corpus, which is the opposite of what a
|
|
1073
|
+
* relevance query with nothing to be relevant to should do.
|
|
1074
|
+
*/
|
|
1075
|
+
async searchText(query, options) {
|
|
1076
|
+
if (filterMatchesNothing(options.filter)) {
|
|
1077
|
+
return [];
|
|
1078
|
+
}
|
|
1079
|
+
const terms = tokenizeQuery(query);
|
|
1080
|
+
if (terms.length === 0) {
|
|
1081
|
+
return [];
|
|
1082
|
+
}
|
|
1083
|
+
const filterQuery = buildFilter(options.filter);
|
|
1084
|
+
const textQuery = `@text:(${terms.join("|")})`;
|
|
1085
|
+
const reply = await this.client.sendCommand([
|
|
1086
|
+
"FT.SEARCH",
|
|
1087
|
+
this.index,
|
|
1088
|
+
filterQuery === "*" ? textQuery : `(${filterQuery} ${textQuery})`,
|
|
1089
|
+
"SCORER",
|
|
1090
|
+
this.lexicalScorer,
|
|
1091
|
+
"WITHSCORES",
|
|
1092
|
+
"RETURN",
|
|
1093
|
+
"3",
|
|
1094
|
+
"text",
|
|
1095
|
+
"source",
|
|
1096
|
+
"metadata_json",
|
|
1097
|
+
"DIALECT",
|
|
1098
|
+
"2",
|
|
1099
|
+
"LIMIT",
|
|
1100
|
+
"0",
|
|
1101
|
+
String(options.topK)
|
|
1102
|
+
]);
|
|
1103
|
+
return parseSearchReply(reply, this.prefix, "lexical");
|
|
1104
|
+
}
|
|
559
1105
|
};
|
|
1106
|
+
var SCAN_PAGE = 1e3;
|
|
1107
|
+
var DELETE_BATCH = 512;
|
|
1108
|
+
function stripPrefix(key, prefix) {
|
|
1109
|
+
return key.startsWith(prefix) ? key.slice(prefix.length) : key;
|
|
1110
|
+
}
|
|
1111
|
+
__name(stripPrefix, "stripPrefix");
|
|
1112
|
+
function parseSearchKeys(reply) {
|
|
1113
|
+
if (Array.isArray(reply)) {
|
|
1114
|
+
return reply.slice(1).map(toStr);
|
|
1115
|
+
}
|
|
1116
|
+
if (typeof reply === "object" && reply !== null && "results" in reply) {
|
|
1117
|
+
const results = reply.results;
|
|
1118
|
+
return Array.isArray(results) ? results.map(readId) : [];
|
|
1119
|
+
}
|
|
1120
|
+
return [];
|
|
1121
|
+
}
|
|
1122
|
+
__name(parseSearchKeys, "parseSearchKeys");
|
|
1123
|
+
function parseSearchTotal(reply) {
|
|
1124
|
+
if (Array.isArray(reply)) {
|
|
1125
|
+
const total = Number(toStr(reply[0]));
|
|
1126
|
+
return Number.isFinite(total) ? total : 0;
|
|
1127
|
+
}
|
|
1128
|
+
if (typeof reply === "object" && reply !== null && "total_results" in reply) {
|
|
1129
|
+
const total = Number(reply.total_results);
|
|
1130
|
+
return Number.isFinite(total) ? total : 0;
|
|
1131
|
+
}
|
|
1132
|
+
return 0;
|
|
1133
|
+
}
|
|
1134
|
+
__name(parseSearchTotal, "parseSearchTotal");
|
|
560
1135
|
function parseIndexAttributes(reply) {
|
|
561
1136
|
const attributes = /* @__PURE__ */ new Map();
|
|
562
1137
|
for (const entry of readInfoField(reply, "attributes")) {
|
|
@@ -627,13 +1202,14 @@ function buildFilter(filter) {
|
|
|
627
1202
|
return `(${clauses.join(" ")})`;
|
|
628
1203
|
}
|
|
629
1204
|
__name(buildFilter, "buildFilter");
|
|
630
|
-
function
|
|
631
|
-
|
|
632
|
-
return false;
|
|
633
|
-
}
|
|
634
|
-
return Object.values(filter).some((value) => Array.isArray(value) && value.length === 0);
|
|
1205
|
+
function tokenizeQuery(query) {
|
|
1206
|
+
return query.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
635
1207
|
}
|
|
636
|
-
__name(
|
|
1208
|
+
__name(tokenizeQuery, "tokenizeQuery");
|
|
1209
|
+
function encodeTag(value) {
|
|
1210
|
+
return Array.isArray(value) ? value.map(String).join(",") : String(value);
|
|
1211
|
+
}
|
|
1212
|
+
__name(encodeTag, "encodeTag");
|
|
637
1213
|
function escapeTag(value) {
|
|
638
1214
|
return value.replace(/[^a-zA-Z0-9_]/g, (char) => `\\${char}`);
|
|
639
1215
|
}
|
|
@@ -670,17 +1246,19 @@ function toStr(value) {
|
|
|
670
1246
|
return Buffer.isBuffer(value) ? value.toString("utf8") : String(value);
|
|
671
1247
|
}
|
|
672
1248
|
__name(toStr, "toStr");
|
|
673
|
-
function parseSearchReply(reply, prefix) {
|
|
1249
|
+
function parseSearchReply(reply, prefix, scoreSource = "vector") {
|
|
1250
|
+
const stride = scoreSource === "lexical" ? 3 : 2;
|
|
674
1251
|
if (Array.isArray(reply)) {
|
|
675
1252
|
const passages = [];
|
|
676
|
-
for (let index = 1; index + 1 < reply.length; index +=
|
|
677
|
-
const rawFields = reply[index + 1];
|
|
1253
|
+
for (let index = 1; index + stride - 1 < reply.length; index += stride) {
|
|
1254
|
+
const rawFields = reply[index + stride - 1];
|
|
678
1255
|
if (Array.isArray(rawFields)) {
|
|
679
1256
|
const attrs = {};
|
|
680
1257
|
for (let field = 0; field + 1 < rawFields.length; field += 2) {
|
|
681
1258
|
attrs[toStr(rawFields[field])] = toStr(rawFields[field + 1]);
|
|
682
1259
|
}
|
|
683
|
-
|
|
1260
|
+
const inlineScore = scoreSource === "lexical" ? toStr(reply[index + 1]) : void 0;
|
|
1261
|
+
passages.push(toPassage(toStr(reply[index]), attrs, prefix, scoreSource, inlineScore));
|
|
684
1262
|
}
|
|
685
1263
|
}
|
|
686
1264
|
return passages;
|
|
@@ -688,12 +1266,19 @@ function parseSearchReply(reply, prefix) {
|
|
|
688
1266
|
if (typeof reply === "object" && reply !== null && "results" in reply) {
|
|
689
1267
|
const results = reply.results;
|
|
690
1268
|
if (Array.isArray(results)) {
|
|
691
|
-
return results.map((result) => toPassage(readId(result), readAttributes(result), prefix));
|
|
1269
|
+
return results.map((result) => toPassage(readId(result), readAttributes(result), prefix, scoreSource, scoreSource === "lexical" ? readScore(result) : void 0));
|
|
692
1270
|
}
|
|
693
1271
|
}
|
|
694
1272
|
return [];
|
|
695
1273
|
}
|
|
696
1274
|
__name(parseSearchReply, "parseSearchReply");
|
|
1275
|
+
function readScore(result) {
|
|
1276
|
+
if (typeof result === "object" && result !== null && "score" in result) {
|
|
1277
|
+
return toStr(result.score);
|
|
1278
|
+
}
|
|
1279
|
+
return void 0;
|
|
1280
|
+
}
|
|
1281
|
+
__name(readScore, "readScore");
|
|
697
1282
|
function readId(result) {
|
|
698
1283
|
if (typeof result === "object" && result !== null && "id" in result) {
|
|
699
1284
|
return toStr(result.id);
|
|
@@ -714,13 +1299,13 @@ function readAttributes(result) {
|
|
|
714
1299
|
return attrs;
|
|
715
1300
|
}
|
|
716
1301
|
__name(readAttributes, "readAttributes");
|
|
717
|
-
function toPassage(rawId, attrs, prefix) {
|
|
718
|
-
const
|
|
1302
|
+
function toPassage(rawId, attrs, prefix, scoreSource = "vector", inlineScore) {
|
|
1303
|
+
const score = scoreSource === "lexical" ? Number(inlineScore ?? "0") : 1 - Number(attrs.vector_score ?? "1");
|
|
719
1304
|
const source = attrs.source;
|
|
720
1305
|
return {
|
|
721
1306
|
id: rawId.startsWith(prefix) ? rawId.slice(prefix.length) : rawId,
|
|
722
1307
|
text: attrs.text ?? "",
|
|
723
|
-
score:
|
|
1308
|
+
score: Number.isFinite(score) ? score : 0,
|
|
724
1309
|
...source !== void 0 ? {
|
|
725
1310
|
source
|
|
726
1311
|
} : {},
|
|
@@ -894,6 +1479,25 @@ function tokenize(text) {
|
|
|
894
1479
|
}
|
|
895
1480
|
__name(tokenize, "tokenize");
|
|
896
1481
|
|
|
1482
|
+
// src/lexical-retriever.ts
|
|
1483
|
+
var LexicalRetriever = class {
|
|
1484
|
+
static {
|
|
1485
|
+
__name(this, "LexicalRetriever");
|
|
1486
|
+
}
|
|
1487
|
+
store;
|
|
1488
|
+
constructor(store) {
|
|
1489
|
+
this.store = store;
|
|
1490
|
+
}
|
|
1491
|
+
async retrieve(query, options = {}) {
|
|
1492
|
+
return this.store.searchText(query, {
|
|
1493
|
+
topK: options.topK ?? 5,
|
|
1494
|
+
...options.filter !== void 0 ? {
|
|
1495
|
+
filter: options.filter
|
|
1496
|
+
} : {}
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
|
|
897
1501
|
// src/hybrid-retriever.ts
|
|
898
1502
|
var HybridRetriever = class {
|
|
899
1503
|
static {
|
|
@@ -1081,16 +1685,20 @@ __name(createRetrievalTool, "createRetrievalTool");
|
|
|
1081
1685
|
FilteredRetriever,
|
|
1082
1686
|
HybridRetriever,
|
|
1083
1687
|
KeywordRetriever,
|
|
1688
|
+
LexicalRetriever,
|
|
1084
1689
|
MemoryVectorStore,
|
|
1085
1690
|
PgVectorStore,
|
|
1086
1691
|
RedisVectorSchemaMismatchError,
|
|
1087
1692
|
RedisVectorStore,
|
|
1088
1693
|
RerankingRetriever,
|
|
1694
|
+
UnsafeRemovalError,
|
|
1695
|
+
applyMetadataPatch,
|
|
1089
1696
|
chunkDocuments,
|
|
1090
1697
|
chunkText,
|
|
1091
1698
|
createRetrievalTool,
|
|
1092
1699
|
documentIdOf,
|
|
1093
1700
|
ingestChunks,
|
|
1094
|
-
ingestDocuments
|
|
1701
|
+
ingestDocuments,
|
|
1702
|
+
isLexicalVectorStore
|
|
1095
1703
|
});
|
|
1096
1704
|
//# sourceMappingURL=index.cjs.map
|