@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/README.md
CHANGED
|
@@ -481,8 +481,10 @@ array is refused so a request can't widen a search. A corpus shared by everyone
|
|
|
481
481
|
|
|
482
482
|
**Saving doesn't index.** Embedding is slow and paid for, so call `index` from a job or wherever the
|
|
483
483
|
text changed. It embeds first, so a provider failure keeps the old chunks. On `PgVectorStore` the swap is
|
|
484
|
-
one transaction. On Redis,
|
|
485
|
-
|
|
484
|
+
one transaction. On Redis, it is one script, and concurrent calls wait for each other. A Redis document
|
|
485
|
+
of more than 256 chunks, counting old and new, is written in steps so it doesn't block the server: a
|
|
486
|
+
search can briefly see both versions, but concurrent calls still can't leave a mix behind. On stores
|
|
487
|
+
without `replaceDocument`, it is two statements.
|
|
486
488
|
|
|
487
489
|
**Deleting.** With Drizzle, use `.returning()` and call `remove`. With MikroORM, add
|
|
488
490
|
`provideRagDeleteSync(ProjectDocument, DocumentsRag)` from `@dudousxd/nestjs-agent-store-mikro-orm` to
|
package/dist/index.cjs
CHANGED
|
@@ -279,6 +279,7 @@ var MemoryVectorStore = class {
|
|
|
279
279
|
__name(this, "MemoryVectorStore");
|
|
280
280
|
}
|
|
281
281
|
records = /* @__PURE__ */ new Map();
|
|
282
|
+
replacing = /* @__PURE__ */ new Map();
|
|
282
283
|
/**
|
|
283
284
|
* Telemetry self-description. No `collection`: there is no namespace to name — the corpus IS this
|
|
284
285
|
* instance, and two `MemoryVectorStore`s in one process are two corpora with nothing to tell them
|
|
@@ -294,7 +295,30 @@ var MemoryVectorStore = class {
|
|
|
294
295
|
this.records.set(record.id, record);
|
|
295
296
|
}
|
|
296
297
|
}
|
|
298
|
+
/** A replace and a removal of one document take turns, in the order they were called. */
|
|
299
|
+
async replaceDocument(documentId, records) {
|
|
300
|
+
await this.inTurn(documentId, async () => {
|
|
301
|
+
this.deleteChunks(documentId);
|
|
302
|
+
await this.upsert(records);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
297
305
|
async remove(documentId) {
|
|
306
|
+
await this.inTurn(documentId, async () => this.deleteChunks(documentId));
|
|
307
|
+
}
|
|
308
|
+
async inTurn(documentId, work) {
|
|
309
|
+
const previous = this.replacing.get(documentId) ?? Promise.resolve();
|
|
310
|
+
const run = previous.catch(() => {
|
|
311
|
+
}).then(work);
|
|
312
|
+
this.replacing.set(documentId, run);
|
|
313
|
+
try {
|
|
314
|
+
await run;
|
|
315
|
+
} finally {
|
|
316
|
+
if (this.replacing.get(documentId) === run) {
|
|
317
|
+
this.replacing.delete(documentId);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
deleteChunks(documentId) {
|
|
298
322
|
for (const id of this.records.keys()) {
|
|
299
323
|
if (documentIdOf(id) === documentId) {
|
|
300
324
|
this.records.delete(id);
|
|
@@ -391,12 +415,9 @@ var MemoryVectorStore = class {
|
|
|
391
415
|
if (documentIds.length === 0) {
|
|
392
416
|
return;
|
|
393
417
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.records.delete(id);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
418
|
+
await Promise.all([
|
|
419
|
+
...new Set(documentIds)
|
|
420
|
+
].map((documentId) => this.remove(documentId)));
|
|
400
421
|
}
|
|
401
422
|
/**
|
|
402
423
|
* See {@link VectorStore.removeWhere}. The empty-array short-circuit below is redundant
|
|
@@ -415,6 +436,7 @@ var MemoryVectorStore = class {
|
|
|
415
436
|
if (filterMatchesNothing(filter)) {
|
|
416
437
|
return 0;
|
|
417
438
|
}
|
|
439
|
+
await Promise.allSettled(this.replacing.values());
|
|
418
440
|
let removed = 0;
|
|
419
441
|
for (const [id, record] of this.records) {
|
|
420
442
|
if (matchesFilter(record.metadata, filter)) {
|
|
@@ -1209,6 +1231,19 @@ var REPLACE_LOCK_TTL_MS = 3e4;
|
|
|
1209
1231
|
var REPLACE_LOCK_WAIT_MS = 12e4;
|
|
1210
1232
|
var RENEW_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) end return 0";
|
|
1211
1233
|
var RELEASE_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0";
|
|
1234
|
+
var SWAP_CHUNKS_SCRIPT = `
|
|
1235
|
+
if redis.call('GET', KEYS[1]) ~= ARGV[1] then return 0 end
|
|
1236
|
+
local stale = tonumber(ARGV[2])
|
|
1237
|
+
for i = 2, stale + 1 do redis.call('DEL', KEYS[i]) end
|
|
1238
|
+
local arg = 3
|
|
1239
|
+
for i = stale + 2, #KEYS do
|
|
1240
|
+
local fields = tonumber(ARGV[arg])
|
|
1241
|
+
redis.call('DEL', KEYS[i])
|
|
1242
|
+
redis.call('HSET', KEYS[i], unpack(ARGV, arg + 1, arg + fields))
|
|
1243
|
+
arg = arg + fields + 1
|
|
1244
|
+
end
|
|
1245
|
+
return 1`;
|
|
1246
|
+
var SWAP_MAX_CHUNKS = 256;
|
|
1212
1247
|
var RedisVectorSchemaMismatchError = class extends Error {
|
|
1213
1248
|
static {
|
|
1214
1249
|
__name(this, "RedisVectorSchemaMismatchError");
|
|
@@ -1346,42 +1381,56 @@ var RedisVectorStore = class {
|
|
|
1346
1381
|
}
|
|
1347
1382
|
async upsert(records) {
|
|
1348
1383
|
for (const record of records) {
|
|
1349
|
-
|
|
1384
|
+
await this.client.sendCommand([
|
|
1350
1385
|
"HSET",
|
|
1351
1386
|
`${this.prefix}${record.id}`,
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1387
|
+
...this.chunkFields(record)
|
|
1388
|
+
]);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
chunkFields(record) {
|
|
1392
|
+
const fields = [
|
|
1393
|
+
"text",
|
|
1394
|
+
record.text,
|
|
1395
|
+
"embedding",
|
|
1396
|
+
encodeVector(record.embedding)
|
|
1397
|
+
];
|
|
1398
|
+
if (record.source !== void 0) {
|
|
1399
|
+
fields.push("source", record.source);
|
|
1400
|
+
}
|
|
1401
|
+
if (record.metadata !== void 0) {
|
|
1402
|
+
fields.push("metadata_json", JSON.stringify(record.metadata));
|
|
1403
|
+
for (const field of this.filterableFields) {
|
|
1404
|
+
const value = record.metadata[field];
|
|
1405
|
+
if (value !== void 0) {
|
|
1406
|
+
fields.push(`meta_${field}`, encodeTag(value));
|
|
1367
1407
|
}
|
|
1368
1408
|
}
|
|
1369
|
-
await this.client.sendCommand(args);
|
|
1370
1409
|
}
|
|
1410
|
+
return fields;
|
|
1371
1411
|
}
|
|
1372
1412
|
/**
|
|
1373
|
-
* Replaces a document's chunks, one writer at a time per document.
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1413
|
+
* Replaces a document's chunks, one writer at a time per document. The swap is one script, so a search
|
|
1414
|
+
* sees the old version or the new one, and a crash leaves the old one. The keys to drop are found
|
|
1415
|
+
* first, under the lock, because a script can't scan the keyspace.
|
|
1416
|
+
*
|
|
1417
|
+
* A document past {@link SWAP_MAX_CHUNKS} would block Redis for too long that way. Its new chunks go in
|
|
1418
|
+
* first and the old ones come out after, so a search may briefly see both, and a crash leaves the
|
|
1419
|
+
* document searchable for the next call to repair.
|
|
1377
1420
|
*/
|
|
1378
1421
|
async replaceDocument(documentId, records) {
|
|
1379
|
-
await this.
|
|
1380
|
-
|
|
1381
|
-
|
|
1422
|
+
await this.withDocumentLocks([
|
|
1423
|
+
documentId
|
|
1424
|
+
], async (assertHeld, token) => {
|
|
1382
1425
|
const current = new Set(records.map((record) => `${this.prefix}${record.id}`));
|
|
1383
1426
|
const stale = (await this.chunkKeys(documentId)).filter((key) => !current.has(key));
|
|
1384
1427
|
assertHeld();
|
|
1428
|
+
if (records.length + stale.length <= SWAP_MAX_CHUNKS) {
|
|
1429
|
+
await this.swapChunks(documentId, token, stale, records);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
await this.upsert(records);
|
|
1433
|
+
assertHeld();
|
|
1385
1434
|
if (stale.length > 0) {
|
|
1386
1435
|
await this.client.sendCommand([
|
|
1387
1436
|
"DEL",
|
|
@@ -1390,69 +1439,110 @@ var RedisVectorStore = class {
|
|
|
1390
1439
|
}
|
|
1391
1440
|
});
|
|
1392
1441
|
}
|
|
1442
|
+
async swapChunks(documentId, token, stale, records) {
|
|
1443
|
+
const keys = [
|
|
1444
|
+
this.lockKey(documentId),
|
|
1445
|
+
...stale,
|
|
1446
|
+
...records.map((record) => `${this.prefix}${record.id}`)
|
|
1447
|
+
];
|
|
1448
|
+
const args = [
|
|
1449
|
+
token,
|
|
1450
|
+
String(stale.length)
|
|
1451
|
+
];
|
|
1452
|
+
for (const record of records) {
|
|
1453
|
+
const fields = this.chunkFields(record);
|
|
1454
|
+
args.push(String(fields.length), ...fields);
|
|
1455
|
+
}
|
|
1456
|
+
const swapped = await this.client.sendCommand([
|
|
1457
|
+
"EVAL",
|
|
1458
|
+
SWAP_CHUNKS_SCRIPT,
|
|
1459
|
+
String(keys.length),
|
|
1460
|
+
...keys,
|
|
1461
|
+
...args
|
|
1462
|
+
]);
|
|
1463
|
+
if (Number(swapped) === 0) {
|
|
1464
|
+
throw lostLockError(documentId);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
lockKey(documentId) {
|
|
1468
|
+
return `${this.index}:replace-lock:${documentId}`;
|
|
1469
|
+
}
|
|
1393
1470
|
/**
|
|
1394
|
-
* A lease, so a crashed writer can't block
|
|
1395
|
-
* which keeps a large document from outliving
|
|
1471
|
+
* A lease on each document, so a crashed writer can't block one forever. They are renewed while the
|
|
1472
|
+
* work runs, which keeps a large document from outliving them.
|
|
1473
|
+
*
|
|
1474
|
+
* Several documents are locked in sorted order, without duplicates. Every other caller either holds
|
|
1475
|
+
* one lock or takes them in that same order, so no two calls can wait on each other.
|
|
1396
1476
|
*/
|
|
1397
|
-
async
|
|
1398
|
-
const key = `${this.index}:replace-lock:${documentId}`;
|
|
1477
|
+
async withDocumentLocks(documentIds, work) {
|
|
1399
1478
|
const token = (0, import_node_crypto.randomUUID)();
|
|
1400
|
-
const
|
|
1401
|
-
let
|
|
1402
|
-
while (await this.client.sendCommand([
|
|
1403
|
-
"SET",
|
|
1404
|
-
key,
|
|
1405
|
-
token,
|
|
1406
|
-
"NX",
|
|
1407
|
-
"PX",
|
|
1408
|
-
String(REPLACE_LOCK_TTL_MS)
|
|
1409
|
-
]) === null) {
|
|
1410
|
-
if (Date.now() >= deadline) {
|
|
1411
|
-
throw new Error(`RedisVectorStore: timed out after ${REPLACE_LOCK_WAIT_MS}ms waiting to replace document "${documentId}"`);
|
|
1412
|
-
}
|
|
1413
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1414
|
-
delay = Math.min(delay * 2, 250);
|
|
1415
|
-
}
|
|
1416
|
-
let lost = false;
|
|
1479
|
+
const held = [];
|
|
1480
|
+
let lostDocument;
|
|
1417
1481
|
const assertHeld = /* @__PURE__ */ __name(() => {
|
|
1418
|
-
if (
|
|
1419
|
-
throw
|
|
1482
|
+
if (lostDocument !== void 0) {
|
|
1483
|
+
throw lostLockError(lostDocument);
|
|
1420
1484
|
}
|
|
1421
1485
|
}, "assertHeld");
|
|
1422
1486
|
const renewal = setInterval(() => {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
(
|
|
1432
|
-
|
|
1433
|
-
|
|
1487
|
+
for (const key of held) {
|
|
1488
|
+
void Promise.resolve(this.client.sendCommand([
|
|
1489
|
+
"EVAL",
|
|
1490
|
+
RENEW_LOCK_SCRIPT,
|
|
1491
|
+
"1",
|
|
1492
|
+
key,
|
|
1493
|
+
token,
|
|
1494
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1495
|
+
])).then(
|
|
1496
|
+
(renewed) => {
|
|
1497
|
+
if (Number(renewed) === 0) {
|
|
1498
|
+
lostDocument = key.slice(this.lockKey("").length);
|
|
1499
|
+
}
|
|
1500
|
+
},
|
|
1501
|
+
// A failed renewal isn't a lost lock yet: the lease may still have time left.
|
|
1502
|
+
() => {
|
|
1434
1503
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
() => {
|
|
1438
|
-
}
|
|
1439
|
-
);
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1440
1506
|
}, REPLACE_LOCK_TTL_MS / 3);
|
|
1441
1507
|
renewal.unref?.();
|
|
1442
1508
|
try {
|
|
1443
|
-
const
|
|
1509
|
+
for (const documentId of [
|
|
1510
|
+
...new Set(documentIds)
|
|
1511
|
+
].sort()) {
|
|
1512
|
+
await this.acquireLock(documentId, token);
|
|
1513
|
+
held.push(this.lockKey(documentId));
|
|
1514
|
+
}
|
|
1515
|
+
const result = await work(assertHeld, token);
|
|
1444
1516
|
assertHeld();
|
|
1445
1517
|
return result;
|
|
1446
1518
|
} finally {
|
|
1447
1519
|
clearInterval(renewal);
|
|
1448
|
-
await Promise.resolve(this.client.sendCommand([
|
|
1520
|
+
await Promise.all(held.map((key) => Promise.resolve(this.client.sendCommand([
|
|
1449
1521
|
"EVAL",
|
|
1450
1522
|
RELEASE_LOCK_SCRIPT,
|
|
1451
1523
|
"1",
|
|
1452
1524
|
key,
|
|
1453
1525
|
token
|
|
1454
1526
|
])).catch(() => {
|
|
1455
|
-
});
|
|
1527
|
+
})));
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
async acquireLock(documentId, token) {
|
|
1531
|
+
const deadline = Date.now() + REPLACE_LOCK_WAIT_MS;
|
|
1532
|
+
let delay = 10;
|
|
1533
|
+
while (await this.client.sendCommand([
|
|
1534
|
+
"SET",
|
|
1535
|
+
this.lockKey(documentId),
|
|
1536
|
+
token,
|
|
1537
|
+
"NX",
|
|
1538
|
+
"PX",
|
|
1539
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1540
|
+
]) === null) {
|
|
1541
|
+
if (Date.now() >= deadline) {
|
|
1542
|
+
throw new Error(`RedisVectorStore: timed out after ${REPLACE_LOCK_WAIT_MS}ms waiting to change document "${documentId}"`);
|
|
1543
|
+
}
|
|
1544
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1545
|
+
delay = Math.min(delay * 2, 250);
|
|
1456
1546
|
}
|
|
1457
1547
|
}
|
|
1458
1548
|
/**
|
|
@@ -1476,13 +1566,17 @@ var RedisVectorStore = class {
|
|
|
1476
1566
|
* than N scans). Neither depends on a field that older chunks lack, so neither has a migration.
|
|
1477
1567
|
*/
|
|
1478
1568
|
async remove(documentId) {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1569
|
+
await this.withDocumentLocks([
|
|
1570
|
+
documentId
|
|
1571
|
+
], async () => {
|
|
1572
|
+
const keys = await this.chunkKeys(documentId);
|
|
1573
|
+
if (keys.length > 0) {
|
|
1574
|
+
await this.client.sendCommand([
|
|
1575
|
+
"DEL",
|
|
1576
|
+
...keys
|
|
1577
|
+
]);
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1486
1580
|
}
|
|
1487
1581
|
/**
|
|
1488
1582
|
* Rewrite the metadata of every chunk of `documentId` in place, leaving `text` and `embedding`
|
|
@@ -1739,44 +1833,51 @@ var RedisVectorStore = class {
|
|
|
1739
1833
|
*
|
|
1740
1834
|
* It deletes by key, exactly as `remove` does, so it is correct on chunks written by any version of
|
|
1741
1835
|
* this library — there is no field it needs the corpus to already carry.
|
|
1836
|
+
*
|
|
1837
|
+
* It holds every document's lock for the whole scan, so it waits for a `replaceDocument` in progress
|
|
1838
|
+
* and one can't start partway through. That adds two commands per document to the one scan.
|
|
1742
1839
|
*/
|
|
1743
1840
|
async removeMany(documentIds) {
|
|
1744
1841
|
if (documentIds.length === 0) {
|
|
1745
1842
|
return;
|
|
1746
1843
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1844
|
+
await this.withDocumentLocks(documentIds, async (assertHeld) => {
|
|
1845
|
+
const wanted = new Set(documentIds);
|
|
1846
|
+
let pending = documentIds.map((id) => `${this.prefix}${id}`);
|
|
1847
|
+
let cursor = "0";
|
|
1848
|
+
do {
|
|
1849
|
+
const reply = await this.client.sendCommand([
|
|
1850
|
+
"SCAN",
|
|
1851
|
+
cursor,
|
|
1852
|
+
"MATCH",
|
|
1853
|
+
`${escapeGlob(this.prefix)}*`,
|
|
1854
|
+
"COUNT",
|
|
1855
|
+
String(SCAN_PAGE)
|
|
1856
|
+
]);
|
|
1857
|
+
const page = parseScanReply(reply);
|
|
1858
|
+
cursor = page.cursor;
|
|
1859
|
+
for (const key of page.keys) {
|
|
1860
|
+
if (wanted.has(documentIdOf(stripPrefix(key, this.prefix)))) {
|
|
1861
|
+
pending.push(key);
|
|
1862
|
+
}
|
|
1764
1863
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1864
|
+
if (pending.length >= DELETE_BATCH) {
|
|
1865
|
+
assertHeld();
|
|
1866
|
+
await this.client.sendCommand([
|
|
1867
|
+
"DEL",
|
|
1868
|
+
...pending
|
|
1869
|
+
]);
|
|
1870
|
+
pending = [];
|
|
1871
|
+
}
|
|
1872
|
+
} while (cursor !== "0");
|
|
1873
|
+
if (pending.length > 0) {
|
|
1874
|
+
assertHeld();
|
|
1767
1875
|
await this.client.sendCommand([
|
|
1768
1876
|
"DEL",
|
|
1769
1877
|
...pending
|
|
1770
1878
|
]);
|
|
1771
|
-
pending = [];
|
|
1772
1879
|
}
|
|
1773
|
-
}
|
|
1774
|
-
if (pending.length > 0) {
|
|
1775
|
-
await this.client.sendCommand([
|
|
1776
|
-
"DEL",
|
|
1777
|
-
...pending
|
|
1778
|
-
]);
|
|
1779
|
-
}
|
|
1880
|
+
});
|
|
1780
1881
|
}
|
|
1781
1882
|
/**
|
|
1782
1883
|
* Delete every chunk matching `filter`, via `FT.SEARCH … NOCONTENT` + `DEL` — no keyspace scan at
|
|
@@ -1801,6 +1902,10 @@ var RedisVectorStore = class {
|
|
|
1801
1902
|
* from the index, so paging forward would skip their replacements. If a page comes back non-empty
|
|
1802
1903
|
* but `DEL` reports nothing removed (keys expired between query and delete, or the index is stale),
|
|
1803
1904
|
* the loop stops rather than spinning on results it can never consume.
|
|
1905
|
+
*
|
|
1906
|
+
* The documents aren't known until a page comes back, so each page locks the documents it touches, one
|
|
1907
|
+
* page at a time, and asks again with `INKEYS` once it holds them. A `replaceDocument` that finished in
|
|
1908
|
+
* between may have dropped those chunks, or rewritten them to no longer match.
|
|
1804
1909
|
*/
|
|
1805
1910
|
async removeWhere(filter) {
|
|
1806
1911
|
const keys = Object.keys(filter);
|
|
@@ -1822,6 +1927,7 @@ var RedisVectorStore = class {
|
|
|
1822
1927
|
}
|
|
1823
1928
|
const query = buildFilter(filter);
|
|
1824
1929
|
let removed = 0;
|
|
1930
|
+
let emptyPageBefore = false;
|
|
1825
1931
|
for (; ; ) {
|
|
1826
1932
|
const reply = await this.client.sendCommand([
|
|
1827
1933
|
"FT.SEARCH",
|
|
@@ -1838,14 +1944,46 @@ var RedisVectorStore = class {
|
|
|
1838
1944
|
if (page.length === 0) {
|
|
1839
1945
|
break;
|
|
1840
1946
|
}
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1947
|
+
const documents = page.map((key) => documentIdOf(stripPrefix(key, this.prefix)));
|
|
1948
|
+
const outcome = await this.withDocumentLocks(documents, async (assertHeld) => {
|
|
1949
|
+
const confirmed = parseSearchKeys(await this.client.sendCommand([
|
|
1950
|
+
"FT.SEARCH",
|
|
1951
|
+
this.index,
|
|
1952
|
+
query,
|
|
1953
|
+
"NOCONTENT",
|
|
1954
|
+
"INKEYS",
|
|
1955
|
+
String(page.length),
|
|
1956
|
+
...page,
|
|
1957
|
+
"LIMIT",
|
|
1958
|
+
"0",
|
|
1959
|
+
String(DELETE_BATCH),
|
|
1960
|
+
"DIALECT",
|
|
1961
|
+
"2"
|
|
1962
|
+
]));
|
|
1963
|
+
assertHeld();
|
|
1964
|
+
return confirmed.length === 0 ? {
|
|
1965
|
+
confirmed: 0,
|
|
1966
|
+
deleted: 0
|
|
1967
|
+
} : {
|
|
1968
|
+
confirmed: confirmed.length,
|
|
1969
|
+
deleted: Number(await this.client.sendCommand([
|
|
1970
|
+
"DEL",
|
|
1971
|
+
...confirmed
|
|
1972
|
+
]))
|
|
1973
|
+
};
|
|
1974
|
+
});
|
|
1975
|
+
if (outcome.confirmed === 0) {
|
|
1976
|
+
if (emptyPageBefore) {
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
emptyPageBefore = true;
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
emptyPageBefore = false;
|
|
1983
|
+
if (!Number.isFinite(outcome.deleted) || outcome.deleted === 0) {
|
|
1846
1984
|
break;
|
|
1847
1985
|
}
|
|
1848
|
-
removed += deleted;
|
|
1986
|
+
removed += outcome.deleted;
|
|
1849
1987
|
}
|
|
1850
1988
|
return removed;
|
|
1851
1989
|
}
|
|
@@ -1943,6 +2081,10 @@ var RedisVectorStore = class {
|
|
|
1943
2081
|
return parseSearchReply(reply, this.prefix, "lexical");
|
|
1944
2082
|
}
|
|
1945
2083
|
};
|
|
2084
|
+
function lostLockError(documentId) {
|
|
2085
|
+
return new Error(`RedisVectorStore: lost the lock while changing document "${documentId}"; call again to repair it`);
|
|
2086
|
+
}
|
|
2087
|
+
__name(lostLockError, "lostLockError");
|
|
1946
2088
|
var SCAN_PAGE = 1e3;
|
|
1947
2089
|
var DELETE_BATCH = 512;
|
|
1948
2090
|
function stripPrefix(key, prefix) {
|
|
@@ -3009,9 +3151,10 @@ var RagIndex = class {
|
|
|
3009
3151
|
}
|
|
3010
3152
|
/**
|
|
3011
3153
|
* Replaces the row's chunks and returns how many were written. Embeddings come first: if the
|
|
3012
|
-
* provider fails, the previous chunks are still there. The swap is one transaction on Postgres
|
|
3013
|
-
* adds the new chunks before dropping the old, so
|
|
3014
|
-
* `replaceDocument`, a crash mid-swap loses the
|
|
3154
|
+
* provider fails, the previous chunks are still there. The swap is one transaction on Postgres and one
|
|
3155
|
+
* script on Redis, where a document too large for that adds the new chunks before dropping the old, so
|
|
3156
|
+
* a search can briefly see both. On a store without `replaceDocument`, a crash mid-swap loses the
|
|
3157
|
+
* document until the next `index`.
|
|
3015
3158
|
*/
|
|
3016
3159
|
async index(row) {
|
|
3017
3160
|
const { definition, deps } = this;
|