@dudousxd/nestjs-agent-rag 0.14.1 → 0.14.3
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 +4 -2
- package/dist/index.cjs +258 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -13
- package/dist/index.d.ts +36 -13
- package/dist/index.js +258 -115
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -191,6 +191,7 @@ var MemoryVectorStore = class {
|
|
|
191
191
|
__name(this, "MemoryVectorStore");
|
|
192
192
|
}
|
|
193
193
|
records = /* @__PURE__ */ new Map();
|
|
194
|
+
replacing = /* @__PURE__ */ new Map();
|
|
194
195
|
/**
|
|
195
196
|
* Telemetry self-description. No `collection`: there is no namespace to name — the corpus IS this
|
|
196
197
|
* instance, and two `MemoryVectorStore`s in one process are two corpora with nothing to tell them
|
|
@@ -206,7 +207,30 @@ var MemoryVectorStore = class {
|
|
|
206
207
|
this.records.set(record.id, record);
|
|
207
208
|
}
|
|
208
209
|
}
|
|
210
|
+
/** A replace and a removal of one document take turns, in the order they were called. */
|
|
211
|
+
async replaceDocument(documentId, records) {
|
|
212
|
+
await this.inTurn(documentId, async () => {
|
|
213
|
+
this.deleteChunks(documentId);
|
|
214
|
+
await this.upsert(records);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
209
217
|
async remove(documentId) {
|
|
218
|
+
await this.inTurn(documentId, async () => this.deleteChunks(documentId));
|
|
219
|
+
}
|
|
220
|
+
async inTurn(documentId, work) {
|
|
221
|
+
const previous = this.replacing.get(documentId) ?? Promise.resolve();
|
|
222
|
+
const run = previous.catch(() => {
|
|
223
|
+
}).then(work);
|
|
224
|
+
this.replacing.set(documentId, run);
|
|
225
|
+
try {
|
|
226
|
+
await run;
|
|
227
|
+
} finally {
|
|
228
|
+
if (this.replacing.get(documentId) === run) {
|
|
229
|
+
this.replacing.delete(documentId);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
deleteChunks(documentId) {
|
|
210
234
|
for (const id of this.records.keys()) {
|
|
211
235
|
if (documentIdOf(id) === documentId) {
|
|
212
236
|
this.records.delete(id);
|
|
@@ -303,12 +327,9 @@ var MemoryVectorStore = class {
|
|
|
303
327
|
if (documentIds.length === 0) {
|
|
304
328
|
return;
|
|
305
329
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
this.records.delete(id);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
330
|
+
await Promise.all([
|
|
331
|
+
...new Set(documentIds)
|
|
332
|
+
].map((documentId) => this.remove(documentId)));
|
|
312
333
|
}
|
|
313
334
|
/**
|
|
314
335
|
* See {@link VectorStore.removeWhere}. The empty-array short-circuit below is redundant
|
|
@@ -327,6 +348,7 @@ var MemoryVectorStore = class {
|
|
|
327
348
|
if (filterMatchesNothing(filter)) {
|
|
328
349
|
return 0;
|
|
329
350
|
}
|
|
351
|
+
await Promise.allSettled(this.replacing.values());
|
|
330
352
|
let removed = 0;
|
|
331
353
|
for (const [id, record] of this.records) {
|
|
332
354
|
if (matchesFilter(record.metadata, filter)) {
|
|
@@ -1121,6 +1143,19 @@ var REPLACE_LOCK_TTL_MS = 3e4;
|
|
|
1121
1143
|
var REPLACE_LOCK_WAIT_MS = 12e4;
|
|
1122
1144
|
var RENEW_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) end return 0";
|
|
1123
1145
|
var RELEASE_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0";
|
|
1146
|
+
var SWAP_CHUNKS_SCRIPT = `
|
|
1147
|
+
if redis.call('GET', KEYS[1]) ~= ARGV[1] then return 0 end
|
|
1148
|
+
local stale = tonumber(ARGV[2])
|
|
1149
|
+
for i = 2, stale + 1 do redis.call('DEL', KEYS[i]) end
|
|
1150
|
+
local arg = 3
|
|
1151
|
+
for i = stale + 2, #KEYS do
|
|
1152
|
+
local fields = tonumber(ARGV[arg])
|
|
1153
|
+
redis.call('DEL', KEYS[i])
|
|
1154
|
+
redis.call('HSET', KEYS[i], unpack(ARGV, arg + 1, arg + fields))
|
|
1155
|
+
arg = arg + fields + 1
|
|
1156
|
+
end
|
|
1157
|
+
return 1`;
|
|
1158
|
+
var SWAP_MAX_CHUNKS = 256;
|
|
1124
1159
|
var RedisVectorSchemaMismatchError = class extends Error {
|
|
1125
1160
|
static {
|
|
1126
1161
|
__name(this, "RedisVectorSchemaMismatchError");
|
|
@@ -1258,42 +1293,56 @@ var RedisVectorStore = class {
|
|
|
1258
1293
|
}
|
|
1259
1294
|
async upsert(records) {
|
|
1260
1295
|
for (const record of records) {
|
|
1261
|
-
|
|
1296
|
+
await this.client.sendCommand([
|
|
1262
1297
|
"HSET",
|
|
1263
1298
|
`${this.prefix}${record.id}`,
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1299
|
+
...this.chunkFields(record)
|
|
1300
|
+
]);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
chunkFields(record) {
|
|
1304
|
+
const fields = [
|
|
1305
|
+
"text",
|
|
1306
|
+
record.text,
|
|
1307
|
+
"embedding",
|
|
1308
|
+
encodeVector(record.embedding)
|
|
1309
|
+
];
|
|
1310
|
+
if (record.source !== void 0) {
|
|
1311
|
+
fields.push("source", record.source);
|
|
1312
|
+
}
|
|
1313
|
+
if (record.metadata !== void 0) {
|
|
1314
|
+
fields.push("metadata_json", JSON.stringify(record.metadata));
|
|
1315
|
+
for (const field of this.filterableFields) {
|
|
1316
|
+
const value = record.metadata[field];
|
|
1317
|
+
if (value !== void 0) {
|
|
1318
|
+
fields.push(`meta_${field}`, encodeTag(value));
|
|
1279
1319
|
}
|
|
1280
1320
|
}
|
|
1281
|
-
await this.client.sendCommand(args);
|
|
1282
1321
|
}
|
|
1322
|
+
return fields;
|
|
1283
1323
|
}
|
|
1284
1324
|
/**
|
|
1285
|
-
* Replaces a document's chunks, one writer at a time per document.
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1325
|
+
* Replaces a document's chunks, one writer at a time per document. The swap is one script, so a search
|
|
1326
|
+
* sees the old version or the new one, and a crash leaves the old one. The keys to drop are found
|
|
1327
|
+
* first, under the lock, because a script can't scan the keyspace.
|
|
1328
|
+
*
|
|
1329
|
+
* A document past {@link SWAP_MAX_CHUNKS} would block Redis for too long that way. Its new chunks go in
|
|
1330
|
+
* first and the old ones come out after, so a search may briefly see both, and a crash leaves the
|
|
1331
|
+
* document searchable for the next call to repair.
|
|
1289
1332
|
*/
|
|
1290
1333
|
async replaceDocument(documentId, records) {
|
|
1291
|
-
await this.
|
|
1292
|
-
|
|
1293
|
-
|
|
1334
|
+
await this.withDocumentLocks([
|
|
1335
|
+
documentId
|
|
1336
|
+
], async (assertHeld, token) => {
|
|
1294
1337
|
const current = new Set(records.map((record) => `${this.prefix}${record.id}`));
|
|
1295
1338
|
const stale = (await this.chunkKeys(documentId)).filter((key) => !current.has(key));
|
|
1296
1339
|
assertHeld();
|
|
1340
|
+
if (records.length + stale.length <= SWAP_MAX_CHUNKS) {
|
|
1341
|
+
await this.swapChunks(documentId, token, stale, records);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
await this.upsert(records);
|
|
1345
|
+
assertHeld();
|
|
1297
1346
|
if (stale.length > 0) {
|
|
1298
1347
|
await this.client.sendCommand([
|
|
1299
1348
|
"DEL",
|
|
@@ -1302,69 +1351,110 @@ var RedisVectorStore = class {
|
|
|
1302
1351
|
}
|
|
1303
1352
|
});
|
|
1304
1353
|
}
|
|
1354
|
+
async swapChunks(documentId, token, stale, records) {
|
|
1355
|
+
const keys = [
|
|
1356
|
+
this.lockKey(documentId),
|
|
1357
|
+
...stale,
|
|
1358
|
+
...records.map((record) => `${this.prefix}${record.id}`)
|
|
1359
|
+
];
|
|
1360
|
+
const args = [
|
|
1361
|
+
token,
|
|
1362
|
+
String(stale.length)
|
|
1363
|
+
];
|
|
1364
|
+
for (const record of records) {
|
|
1365
|
+
const fields = this.chunkFields(record);
|
|
1366
|
+
args.push(String(fields.length), ...fields);
|
|
1367
|
+
}
|
|
1368
|
+
const swapped = await this.client.sendCommand([
|
|
1369
|
+
"EVAL",
|
|
1370
|
+
SWAP_CHUNKS_SCRIPT,
|
|
1371
|
+
String(keys.length),
|
|
1372
|
+
...keys,
|
|
1373
|
+
...args
|
|
1374
|
+
]);
|
|
1375
|
+
if (Number(swapped) === 0) {
|
|
1376
|
+
throw lostLockError(documentId);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
lockKey(documentId) {
|
|
1380
|
+
return `${this.index}:replace-lock:${documentId}`;
|
|
1381
|
+
}
|
|
1305
1382
|
/**
|
|
1306
|
-
* A lease, so a crashed writer can't block
|
|
1307
|
-
* which keeps a large document from outliving
|
|
1383
|
+
* A lease on each document, so a crashed writer can't block one forever. They are renewed while the
|
|
1384
|
+
* work runs, which keeps a large document from outliving them.
|
|
1385
|
+
*
|
|
1386
|
+
* Several documents are locked in sorted order, without duplicates. Every other caller either holds
|
|
1387
|
+
* one lock or takes them in that same order, so no two calls can wait on each other.
|
|
1308
1388
|
*/
|
|
1309
|
-
async
|
|
1310
|
-
const key = `${this.index}:replace-lock:${documentId}`;
|
|
1389
|
+
async withDocumentLocks(documentIds, work) {
|
|
1311
1390
|
const token = randomUUID();
|
|
1312
|
-
const
|
|
1313
|
-
let
|
|
1314
|
-
while (await this.client.sendCommand([
|
|
1315
|
-
"SET",
|
|
1316
|
-
key,
|
|
1317
|
-
token,
|
|
1318
|
-
"NX",
|
|
1319
|
-
"PX",
|
|
1320
|
-
String(REPLACE_LOCK_TTL_MS)
|
|
1321
|
-
]) === null) {
|
|
1322
|
-
if (Date.now() >= deadline) {
|
|
1323
|
-
throw new Error(`RedisVectorStore: timed out after ${REPLACE_LOCK_WAIT_MS}ms waiting to replace document "${documentId}"`);
|
|
1324
|
-
}
|
|
1325
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1326
|
-
delay = Math.min(delay * 2, 250);
|
|
1327
|
-
}
|
|
1328
|
-
let lost = false;
|
|
1391
|
+
const held = [];
|
|
1392
|
+
let lostDocument;
|
|
1329
1393
|
const assertHeld = /* @__PURE__ */ __name(() => {
|
|
1330
|
-
if (
|
|
1331
|
-
throw
|
|
1394
|
+
if (lostDocument !== void 0) {
|
|
1395
|
+
throw lostLockError(lostDocument);
|
|
1332
1396
|
}
|
|
1333
1397
|
}, "assertHeld");
|
|
1334
1398
|
const renewal = setInterval(() => {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
(
|
|
1344
|
-
|
|
1345
|
-
|
|
1399
|
+
for (const key of held) {
|
|
1400
|
+
void Promise.resolve(this.client.sendCommand([
|
|
1401
|
+
"EVAL",
|
|
1402
|
+
RENEW_LOCK_SCRIPT,
|
|
1403
|
+
"1",
|
|
1404
|
+
key,
|
|
1405
|
+
token,
|
|
1406
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1407
|
+
])).then(
|
|
1408
|
+
(renewed) => {
|
|
1409
|
+
if (Number(renewed) === 0) {
|
|
1410
|
+
lostDocument = key.slice(this.lockKey("").length);
|
|
1411
|
+
}
|
|
1412
|
+
},
|
|
1413
|
+
// A failed renewal isn't a lost lock yet: the lease may still have time left.
|
|
1414
|
+
() => {
|
|
1346
1415
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
() => {
|
|
1350
|
-
}
|
|
1351
|
-
);
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1352
1418
|
}, REPLACE_LOCK_TTL_MS / 3);
|
|
1353
1419
|
renewal.unref?.();
|
|
1354
1420
|
try {
|
|
1355
|
-
const
|
|
1421
|
+
for (const documentId of [
|
|
1422
|
+
...new Set(documentIds)
|
|
1423
|
+
].sort()) {
|
|
1424
|
+
await this.acquireLock(documentId, token);
|
|
1425
|
+
held.push(this.lockKey(documentId));
|
|
1426
|
+
}
|
|
1427
|
+
const result = await work(assertHeld, token);
|
|
1356
1428
|
assertHeld();
|
|
1357
1429
|
return result;
|
|
1358
1430
|
} finally {
|
|
1359
1431
|
clearInterval(renewal);
|
|
1360
|
-
await Promise.resolve(this.client.sendCommand([
|
|
1432
|
+
await Promise.all(held.map((key) => Promise.resolve(this.client.sendCommand([
|
|
1361
1433
|
"EVAL",
|
|
1362
1434
|
RELEASE_LOCK_SCRIPT,
|
|
1363
1435
|
"1",
|
|
1364
1436
|
key,
|
|
1365
1437
|
token
|
|
1366
1438
|
])).catch(() => {
|
|
1367
|
-
});
|
|
1439
|
+
})));
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async acquireLock(documentId, token) {
|
|
1443
|
+
const deadline = Date.now() + REPLACE_LOCK_WAIT_MS;
|
|
1444
|
+
let delay = 10;
|
|
1445
|
+
while (await this.client.sendCommand([
|
|
1446
|
+
"SET",
|
|
1447
|
+
this.lockKey(documentId),
|
|
1448
|
+
token,
|
|
1449
|
+
"NX",
|
|
1450
|
+
"PX",
|
|
1451
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1452
|
+
]) === null) {
|
|
1453
|
+
if (Date.now() >= deadline) {
|
|
1454
|
+
throw new Error(`RedisVectorStore: timed out after ${REPLACE_LOCK_WAIT_MS}ms waiting to change document "${documentId}"`);
|
|
1455
|
+
}
|
|
1456
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1457
|
+
delay = Math.min(delay * 2, 250);
|
|
1368
1458
|
}
|
|
1369
1459
|
}
|
|
1370
1460
|
/**
|
|
@@ -1388,13 +1478,17 @@ var RedisVectorStore = class {
|
|
|
1388
1478
|
* than N scans). Neither depends on a field that older chunks lack, so neither has a migration.
|
|
1389
1479
|
*/
|
|
1390
1480
|
async remove(documentId) {
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1481
|
+
await this.withDocumentLocks([
|
|
1482
|
+
documentId
|
|
1483
|
+
], async () => {
|
|
1484
|
+
const keys = await this.chunkKeys(documentId);
|
|
1485
|
+
if (keys.length > 0) {
|
|
1486
|
+
await this.client.sendCommand([
|
|
1487
|
+
"DEL",
|
|
1488
|
+
...keys
|
|
1489
|
+
]);
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1398
1492
|
}
|
|
1399
1493
|
/**
|
|
1400
1494
|
* Rewrite the metadata of every chunk of `documentId` in place, leaving `text` and `embedding`
|
|
@@ -1651,44 +1745,51 @@ var RedisVectorStore = class {
|
|
|
1651
1745
|
*
|
|
1652
1746
|
* It deletes by key, exactly as `remove` does, so it is correct on chunks written by any version of
|
|
1653
1747
|
* this library — there is no field it needs the corpus to already carry.
|
|
1748
|
+
*
|
|
1749
|
+
* It holds every document's lock for the whole scan, so it waits for a `replaceDocument` in progress
|
|
1750
|
+
* and one can't start partway through. That adds two commands per document to the one scan.
|
|
1654
1751
|
*/
|
|
1655
1752
|
async removeMany(documentIds) {
|
|
1656
1753
|
if (documentIds.length === 0) {
|
|
1657
1754
|
return;
|
|
1658
1755
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1756
|
+
await this.withDocumentLocks(documentIds, async (assertHeld) => {
|
|
1757
|
+
const wanted = new Set(documentIds);
|
|
1758
|
+
let pending = documentIds.map((id) => `${this.prefix}${id}`);
|
|
1759
|
+
let cursor = "0";
|
|
1760
|
+
do {
|
|
1761
|
+
const reply = await this.client.sendCommand([
|
|
1762
|
+
"SCAN",
|
|
1763
|
+
cursor,
|
|
1764
|
+
"MATCH",
|
|
1765
|
+
`${escapeGlob(this.prefix)}*`,
|
|
1766
|
+
"COUNT",
|
|
1767
|
+
String(SCAN_PAGE)
|
|
1768
|
+
]);
|
|
1769
|
+
const page = parseScanReply(reply);
|
|
1770
|
+
cursor = page.cursor;
|
|
1771
|
+
for (const key of page.keys) {
|
|
1772
|
+
if (wanted.has(documentIdOf(stripPrefix(key, this.prefix)))) {
|
|
1773
|
+
pending.push(key);
|
|
1774
|
+
}
|
|
1676
1775
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1776
|
+
if (pending.length >= DELETE_BATCH) {
|
|
1777
|
+
assertHeld();
|
|
1778
|
+
await this.client.sendCommand([
|
|
1779
|
+
"DEL",
|
|
1780
|
+
...pending
|
|
1781
|
+
]);
|
|
1782
|
+
pending = [];
|
|
1783
|
+
}
|
|
1784
|
+
} while (cursor !== "0");
|
|
1785
|
+
if (pending.length > 0) {
|
|
1786
|
+
assertHeld();
|
|
1679
1787
|
await this.client.sendCommand([
|
|
1680
1788
|
"DEL",
|
|
1681
1789
|
...pending
|
|
1682
1790
|
]);
|
|
1683
|
-
pending = [];
|
|
1684
1791
|
}
|
|
1685
|
-
}
|
|
1686
|
-
if (pending.length > 0) {
|
|
1687
|
-
await this.client.sendCommand([
|
|
1688
|
-
"DEL",
|
|
1689
|
-
...pending
|
|
1690
|
-
]);
|
|
1691
|
-
}
|
|
1792
|
+
});
|
|
1692
1793
|
}
|
|
1693
1794
|
/**
|
|
1694
1795
|
* Delete every chunk matching `filter`, via `FT.SEARCH … NOCONTENT` + `DEL` — no keyspace scan at
|
|
@@ -1713,6 +1814,10 @@ var RedisVectorStore = class {
|
|
|
1713
1814
|
* from the index, so paging forward would skip their replacements. If a page comes back non-empty
|
|
1714
1815
|
* but `DEL` reports nothing removed (keys expired between query and delete, or the index is stale),
|
|
1715
1816
|
* the loop stops rather than spinning on results it can never consume.
|
|
1817
|
+
*
|
|
1818
|
+
* The documents aren't known until a page comes back, so each page locks the documents it touches, one
|
|
1819
|
+
* page at a time, and asks again with `INKEYS` once it holds them. A `replaceDocument` that finished in
|
|
1820
|
+
* between may have dropped those chunks, or rewritten them to no longer match.
|
|
1716
1821
|
*/
|
|
1717
1822
|
async removeWhere(filter) {
|
|
1718
1823
|
const keys = Object.keys(filter);
|
|
@@ -1734,6 +1839,7 @@ var RedisVectorStore = class {
|
|
|
1734
1839
|
}
|
|
1735
1840
|
const query = buildFilter(filter);
|
|
1736
1841
|
let removed = 0;
|
|
1842
|
+
let emptyPageBefore = false;
|
|
1737
1843
|
for (; ; ) {
|
|
1738
1844
|
const reply = await this.client.sendCommand([
|
|
1739
1845
|
"FT.SEARCH",
|
|
@@ -1750,14 +1856,46 @@ var RedisVectorStore = class {
|
|
|
1750
1856
|
if (page.length === 0) {
|
|
1751
1857
|
break;
|
|
1752
1858
|
}
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1859
|
+
const documents = page.map((key) => documentIdOf(stripPrefix(key, this.prefix)));
|
|
1860
|
+
const outcome = await this.withDocumentLocks(documents, async (assertHeld) => {
|
|
1861
|
+
const confirmed = parseSearchKeys(await this.client.sendCommand([
|
|
1862
|
+
"FT.SEARCH",
|
|
1863
|
+
this.index,
|
|
1864
|
+
query,
|
|
1865
|
+
"NOCONTENT",
|
|
1866
|
+
"INKEYS",
|
|
1867
|
+
String(page.length),
|
|
1868
|
+
...page,
|
|
1869
|
+
"LIMIT",
|
|
1870
|
+
"0",
|
|
1871
|
+
String(DELETE_BATCH),
|
|
1872
|
+
"DIALECT",
|
|
1873
|
+
"2"
|
|
1874
|
+
]));
|
|
1875
|
+
assertHeld();
|
|
1876
|
+
return confirmed.length === 0 ? {
|
|
1877
|
+
confirmed: 0,
|
|
1878
|
+
deleted: 0
|
|
1879
|
+
} : {
|
|
1880
|
+
confirmed: confirmed.length,
|
|
1881
|
+
deleted: Number(await this.client.sendCommand([
|
|
1882
|
+
"DEL",
|
|
1883
|
+
...confirmed
|
|
1884
|
+
]))
|
|
1885
|
+
};
|
|
1886
|
+
});
|
|
1887
|
+
if (outcome.confirmed === 0) {
|
|
1888
|
+
if (emptyPageBefore) {
|
|
1889
|
+
break;
|
|
1890
|
+
}
|
|
1891
|
+
emptyPageBefore = true;
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
emptyPageBefore = false;
|
|
1895
|
+
if (!Number.isFinite(outcome.deleted) || outcome.deleted === 0) {
|
|
1758
1896
|
break;
|
|
1759
1897
|
}
|
|
1760
|
-
removed += deleted;
|
|
1898
|
+
removed += outcome.deleted;
|
|
1761
1899
|
}
|
|
1762
1900
|
return removed;
|
|
1763
1901
|
}
|
|
@@ -1855,6 +1993,10 @@ var RedisVectorStore = class {
|
|
|
1855
1993
|
return parseSearchReply(reply, this.prefix, "lexical");
|
|
1856
1994
|
}
|
|
1857
1995
|
};
|
|
1996
|
+
function lostLockError(documentId) {
|
|
1997
|
+
return new Error(`RedisVectorStore: lost the lock while changing document "${documentId}"; call again to repair it`);
|
|
1998
|
+
}
|
|
1999
|
+
__name(lostLockError, "lostLockError");
|
|
1858
2000
|
var SCAN_PAGE = 1e3;
|
|
1859
2001
|
var DELETE_BATCH = 512;
|
|
1860
2002
|
function stripPrefix(key, prefix) {
|
|
@@ -2921,9 +3063,10 @@ var RagIndex = class {
|
|
|
2921
3063
|
}
|
|
2922
3064
|
/**
|
|
2923
3065
|
* Replaces the row's chunks and returns how many were written. Embeddings come first: if the
|
|
2924
|
-
* provider fails, the previous chunks are still there. The swap is one transaction on Postgres
|
|
2925
|
-
* adds the new chunks before dropping the old, so
|
|
2926
|
-
* `replaceDocument`, a crash mid-swap loses the
|
|
3066
|
+
* provider fails, the previous chunks are still there. The swap is one transaction on Postgres and one
|
|
3067
|
+
* script on Redis, where a document too large for that adds the new chunks before dropping the old, so
|
|
3068
|
+
* a search can briefly see both. On a store without `replaceDocument`, a crash mid-swap loses the
|
|
3069
|
+
* document until the next `index`.
|
|
2927
3070
|
*/
|
|
2928
3071
|
async index(row) {
|
|
2929
3072
|
const { definition, deps } = this;
|