@dudousxd/nestjs-agent-rag 0.14.0 → 0.14.2
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 +2 -1
- package/dist/index.cjs +135 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -14
- package/dist/index.d.ts +29 -14
- package/dist/index.js +135 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -481,7 +481,8 @@ 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
|
|
484
|
+
one transaction. On Redis, a search can briefly see both versions, but concurrent calls wait for each
|
|
485
|
+
other, so they can't leave a mix behind. On stores without `replaceDocument`, it is two statements.
|
|
485
486
|
|
|
486
487
|
**Deleting.** With Drizzle, use `.returning()` and call `remove`. With MikroORM, add
|
|
487
488
|
`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,6 +295,23 @@ var MemoryVectorStore = class {
|
|
|
294
295
|
this.records.set(record.id, record);
|
|
295
296
|
}
|
|
296
297
|
}
|
|
298
|
+
/** Replaces take turns per document. A `remove` during one isn't queued, and may be undone by it. */
|
|
299
|
+
async replaceDocument(documentId, records) {
|
|
300
|
+
const previous = this.replacing.get(documentId) ?? Promise.resolve();
|
|
301
|
+
const run = previous.catch(() => {
|
|
302
|
+
}).then(async () => {
|
|
303
|
+
await this.remove(documentId);
|
|
304
|
+
await this.upsert(records);
|
|
305
|
+
});
|
|
306
|
+
this.replacing.set(documentId, run);
|
|
307
|
+
try {
|
|
308
|
+
await run;
|
|
309
|
+
} finally {
|
|
310
|
+
if (this.replacing.get(documentId) === run) {
|
|
311
|
+
this.replacing.delete(documentId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
297
315
|
async remove(documentId) {
|
|
298
316
|
for (const id of this.records.keys()) {
|
|
299
317
|
if (documentIdOf(id) === documentId) {
|
|
@@ -767,6 +785,10 @@ var PgVectorStore = class {
|
|
|
767
785
|
/**
|
|
768
786
|
* Swaps a document's chunks in one transaction, so a crash can't lose it and a search sees the old
|
|
769
787
|
* version or the new one. A client without `transaction` falls back to two statements.
|
|
788
|
+
*
|
|
789
|
+
* Concurrent swaps of one document take turns on an advisory lock. Without it the second `DELETE`
|
|
790
|
+
* can't see the rows the first just committed, so the first's tail survives next to the second's
|
|
791
|
+
* chunks, and overlapping writes can deadlock.
|
|
770
792
|
*/
|
|
771
793
|
async replaceDocument(documentId, records) {
|
|
772
794
|
const swap = /* @__PURE__ */ __name(async (client) => {
|
|
@@ -776,7 +798,16 @@ var PgVectorStore = class {
|
|
|
776
798
|
await this.upsertWith(client, records);
|
|
777
799
|
}, "swap");
|
|
778
800
|
const transaction = this.client.transaction?.bind(this.client);
|
|
779
|
-
|
|
801
|
+
if (transaction === void 0) {
|
|
802
|
+
await swap(this.client);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
await transaction(async (client) => {
|
|
806
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
|
|
807
|
+
`${this.table}:${stripNulBytes(documentId)}`
|
|
808
|
+
]);
|
|
809
|
+
await swap(client);
|
|
810
|
+
});
|
|
780
811
|
}
|
|
781
812
|
async upsertWith(client, records) {
|
|
782
813
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -1191,6 +1222,11 @@ function toVectorLiteral(embedding) {
|
|
|
1191
1222
|
__name(toVectorLiteral, "toVectorLiteral");
|
|
1192
1223
|
|
|
1193
1224
|
// src/redis-vector-store.ts
|
|
1225
|
+
var import_node_crypto = require("crypto");
|
|
1226
|
+
var REPLACE_LOCK_TTL_MS = 3e4;
|
|
1227
|
+
var REPLACE_LOCK_WAIT_MS = 12e4;
|
|
1228
|
+
var RENEW_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) end return 0";
|
|
1229
|
+
var RELEASE_LOCK_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0";
|
|
1194
1230
|
var RedisVectorSchemaMismatchError = class extends Error {
|
|
1195
1231
|
static {
|
|
1196
1232
|
__name(this, "RedisVectorSchemaMismatchError");
|
|
@@ -1352,6 +1388,92 @@ var RedisVectorStore = class {
|
|
|
1352
1388
|
}
|
|
1353
1389
|
}
|
|
1354
1390
|
/**
|
|
1391
|
+
* Replaces a document's chunks, one writer at a time per document. Redis can't swap them in one step
|
|
1392
|
+
* without scanning the keyspace inside a script, so a search may briefly see old and new chunks
|
|
1393
|
+
* together, but concurrent calls can't leave them mixed. The new chunks go in before the old ones
|
|
1394
|
+
* come out, so a crash leaves the document searchable and the next call repairs it.
|
|
1395
|
+
*/
|
|
1396
|
+
async replaceDocument(documentId, records) {
|
|
1397
|
+
await this.withDocumentLock(documentId, async (assertHeld) => {
|
|
1398
|
+
await this.upsert(records);
|
|
1399
|
+
assertHeld();
|
|
1400
|
+
const current = new Set(records.map((record) => `${this.prefix}${record.id}`));
|
|
1401
|
+
const stale = (await this.chunkKeys(documentId)).filter((key) => !current.has(key));
|
|
1402
|
+
assertHeld();
|
|
1403
|
+
if (stale.length > 0) {
|
|
1404
|
+
await this.client.sendCommand([
|
|
1405
|
+
"DEL",
|
|
1406
|
+
...stale
|
|
1407
|
+
]);
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* A lease, so a crashed writer can't block the document forever. It is renewed while the work runs,
|
|
1413
|
+
* which keeps a large document from outliving it.
|
|
1414
|
+
*/
|
|
1415
|
+
async withDocumentLock(documentId, work) {
|
|
1416
|
+
const key = `${this.index}:replace-lock:${documentId}`;
|
|
1417
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
1418
|
+
const deadline = Date.now() + REPLACE_LOCK_WAIT_MS;
|
|
1419
|
+
let delay = 10;
|
|
1420
|
+
while (await this.client.sendCommand([
|
|
1421
|
+
"SET",
|
|
1422
|
+
key,
|
|
1423
|
+
token,
|
|
1424
|
+
"NX",
|
|
1425
|
+
"PX",
|
|
1426
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1427
|
+
]) === null) {
|
|
1428
|
+
if (Date.now() >= deadline) {
|
|
1429
|
+
throw new Error(`RedisVectorStore: timed out after ${REPLACE_LOCK_WAIT_MS}ms waiting to change document "${documentId}"`);
|
|
1430
|
+
}
|
|
1431
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1432
|
+
delay = Math.min(delay * 2, 250);
|
|
1433
|
+
}
|
|
1434
|
+
let lost = false;
|
|
1435
|
+
const assertHeld = /* @__PURE__ */ __name(() => {
|
|
1436
|
+
if (lost) {
|
|
1437
|
+
throw new Error(`RedisVectorStore: lost the lock while changing document "${documentId}"; call again to repair it`);
|
|
1438
|
+
}
|
|
1439
|
+
}, "assertHeld");
|
|
1440
|
+
const renewal = setInterval(() => {
|
|
1441
|
+
void Promise.resolve(this.client.sendCommand([
|
|
1442
|
+
"EVAL",
|
|
1443
|
+
RENEW_LOCK_SCRIPT,
|
|
1444
|
+
"1",
|
|
1445
|
+
key,
|
|
1446
|
+
token,
|
|
1447
|
+
String(REPLACE_LOCK_TTL_MS)
|
|
1448
|
+
])).then(
|
|
1449
|
+
(renewed) => {
|
|
1450
|
+
if (Number(renewed) === 0) {
|
|
1451
|
+
lost = true;
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
// A failed renewal isn't a lost lock yet: the lease may still have time left.
|
|
1455
|
+
() => {
|
|
1456
|
+
}
|
|
1457
|
+
);
|
|
1458
|
+
}, REPLACE_LOCK_TTL_MS / 3);
|
|
1459
|
+
renewal.unref?.();
|
|
1460
|
+
try {
|
|
1461
|
+
const result = await work(assertHeld);
|
|
1462
|
+
assertHeld();
|
|
1463
|
+
return result;
|
|
1464
|
+
} finally {
|
|
1465
|
+
clearInterval(renewal);
|
|
1466
|
+
await Promise.resolve(this.client.sendCommand([
|
|
1467
|
+
"EVAL",
|
|
1468
|
+
RELEASE_LOCK_SCRIPT,
|
|
1469
|
+
"1",
|
|
1470
|
+
key,
|
|
1471
|
+
token
|
|
1472
|
+
])).catch(() => {
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1355
1477
|
* Single-document deletion, by `SCAN … MATCH`.
|
|
1356
1478
|
*
|
|
1357
1479
|
* This deliberately stays a keyspace scan even though the rest of this class no longer needs one.
|
|
@@ -1372,13 +1494,15 @@ var RedisVectorStore = class {
|
|
|
1372
1494
|
* than N scans). Neither depends on a field that older chunks lack, so neither has a migration.
|
|
1373
1495
|
*/
|
|
1374
1496
|
async remove(documentId) {
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1497
|
+
await this.withDocumentLock(documentId, async () => {
|
|
1498
|
+
const keys = await this.chunkKeys(documentId);
|
|
1499
|
+
if (keys.length > 0) {
|
|
1500
|
+
await this.client.sendCommand([
|
|
1501
|
+
"DEL",
|
|
1502
|
+
...keys
|
|
1503
|
+
]);
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1382
1506
|
}
|
|
1383
1507
|
/**
|
|
1384
1508
|
* Rewrite the metadata of every chunk of `documentId` in place, leaving `text` and `embedding`
|
|
@@ -2905,8 +3029,9 @@ var RagIndex = class {
|
|
|
2905
3029
|
}
|
|
2906
3030
|
/**
|
|
2907
3031
|
* Replaces the row's chunks and returns how many were written. Embeddings come first: if the
|
|
2908
|
-
* provider fails, the previous chunks are still there. The swap is one transaction on Postgres
|
|
2909
|
-
*
|
|
3032
|
+
* provider fails, the previous chunks are still there. The swap is one transaction on Postgres. Redis
|
|
3033
|
+
* adds the new chunks before dropping the old, so a search can briefly see both. On a store without
|
|
3034
|
+
* `replaceDocument`, a crash mid-swap loses the document until the next `index`.
|
|
2910
3035
|
*/
|
|
2911
3036
|
async index(row) {
|
|
2912
3037
|
const { definition, deps } = this;
|