@langchain/langgraph-checkpoint-redis 0.0.2 → 1.0.1
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/CHANGELOG.md +17 -0
- package/dist/_virtual/rolldown_runtime.cjs +25 -0
- package/dist/index.cjs +529 -699
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +35 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +526 -693
- package/dist/index.js.map +1 -1
- package/dist/shallow.cjs +398 -536
- package/dist/shallow.cjs.map +1 -0
- package/dist/shallow.d.cts +45 -0
- package/dist/shallow.d.cts.map +1 -0
- package/dist/shallow.d.ts +31 -25
- package/dist/shallow.d.ts.map +1 -1
- package/dist/shallow.js +396 -531
- package/dist/shallow.js.map +1 -1
- package/dist/store.cjs +616 -811
- package/dist/store.cjs.map +1 -0
- package/dist/store.d.cts +138 -0
- package/dist/store.d.cts.map +1 -0
- package/dist/store.d.ts +120 -114
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +613 -807
- package/dist/store.js.map +1 -1
- package/package.json +37 -40
- package/index.cjs +0 -1
- package/index.d.cts +0 -1
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/shallow.cjs +0 -1
- package/shallow.d.cts +0 -1
- package/shallow.d.ts +0 -1
- package/shallow.js +0 -1
- package/store.cjs +0 -1
- package/store.d.cts +0 -1
- package/store.d.ts +0 -1
- package/store.js +0 -1
package/dist/store.js
CHANGED
|
@@ -1,817 +1,623 @@
|
|
|
1
|
+
import v4_default from "./node_modules/uuid/dist/esm-node/v4.js";
|
|
2
|
+
import { InvalidNamespaceError } from "@langchain/langgraph-checkpoint";
|
|
1
3
|
import { createClient, createCluster } from "redis";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
return "value" in op && "namespace" in op && "key" in op;
|
|
4
|
+
|
|
5
|
+
//#region src/store.ts
|
|
6
|
+
function isPutOperation(op) {
|
|
7
|
+
return "value" in op && "namespace" in op && "key" in op;
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"key" in op &&
|
|
11
|
-
!("value" in op) &&
|
|
12
|
-
!("namespacePrefix" in op) &&
|
|
13
|
-
!("matchConditions" in op));
|
|
9
|
+
function isGetOperation(op) {
|
|
10
|
+
return "namespace" in op && "key" in op && !("value" in op) && !("namespacePrefix" in op) && !("matchConditions" in op);
|
|
14
11
|
}
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
function isSearchOperation(op) {
|
|
13
|
+
return "namespacePrefix" in op;
|
|
17
14
|
}
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
function isListNamespacesOperation(op) {
|
|
16
|
+
return "matchConditions" in op;
|
|
20
17
|
}
|
|
21
18
|
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
actualValue !== null &&
|
|
117
|
-
Number(actualValue) <= Number(operatorValue));
|
|
118
|
-
case "$in":
|
|
119
|
-
if (!Array.isArray(operatorValue))
|
|
120
|
-
return false;
|
|
121
|
-
return operatorValue.some((val) => this.isEqual(actualValue, val));
|
|
122
|
-
case "$nin":
|
|
123
|
-
if (!Array.isArray(operatorValue))
|
|
124
|
-
return false;
|
|
125
|
-
return !operatorValue.some((val) => this.isEqual(actualValue, val));
|
|
126
|
-
case "$exists": {
|
|
127
|
-
const exists = actualValue !== undefined;
|
|
128
|
-
return operatorValue ? exists : !exists;
|
|
129
|
-
}
|
|
130
|
-
default:
|
|
131
|
-
// Unknown operator, return false for safety
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
static isEqual(a, b) {
|
|
136
|
-
// Handle null and undefined
|
|
137
|
-
if (a === b)
|
|
138
|
-
return true;
|
|
139
|
-
if (a === null || b === null)
|
|
140
|
-
return false;
|
|
141
|
-
if (a === undefined || b === undefined)
|
|
142
|
-
return false;
|
|
143
|
-
// Handle arrays
|
|
144
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
145
|
-
if (a.length !== b.length)
|
|
146
|
-
return false;
|
|
147
|
-
return a.every((val, idx) => this.isEqual(val, b[idx]));
|
|
148
|
-
}
|
|
149
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
150
|
-
// Check if non-array value exists in array
|
|
151
|
-
const arr = Array.isArray(a) ? a : b;
|
|
152
|
-
const val = Array.isArray(a) ? b : a;
|
|
153
|
-
return arr.includes(val);
|
|
154
|
-
}
|
|
155
|
-
// Handle objects
|
|
156
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
157
|
-
const aKeys = Object.keys(a);
|
|
158
|
-
const bKeys = Object.keys(b);
|
|
159
|
-
if (aKeys.length !== bKeys.length)
|
|
160
|
-
return false;
|
|
161
|
-
return aKeys.every((key) => this.isEqual(a[key], b[key]));
|
|
162
|
-
}
|
|
163
|
-
// Primitive comparison (with type coercion for numbers)
|
|
164
|
-
return a == b;
|
|
165
|
-
}
|
|
166
|
-
static getNestedValue(obj, path) {
|
|
167
|
-
const keys = path.split(".");
|
|
168
|
-
let current = obj;
|
|
169
|
-
for (const key of keys) {
|
|
170
|
-
if (current === null || current === undefined) {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
current = current[key];
|
|
174
|
-
}
|
|
175
|
-
return current;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
19
|
+
* Internal class for evaluating filters against documents.
|
|
20
|
+
* Supports MongoDB-style query operators.
|
|
21
|
+
*/
|
|
22
|
+
var FilterBuilder = class {
|
|
23
|
+
/**
|
|
24
|
+
* Evaluates if a document matches the given filter criteria.
|
|
25
|
+
* Supports advanced operators like $gt, $lt, $in, etc.
|
|
26
|
+
*/
|
|
27
|
+
static matchesFilter(doc, filter) {
|
|
28
|
+
for (const [key, filterValue] of Object.entries(filter)) if (!this.matchesFieldFilter(doc, key, filterValue)) return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Builds a Redis Search query string from filter criteria.
|
|
33
|
+
* Note: This is limited by RediSearch capabilities and may not support all operators.
|
|
34
|
+
*/
|
|
35
|
+
static buildRedisSearchQuery(filter, prefix) {
|
|
36
|
+
let queryParts = [];
|
|
37
|
+
let useClientFilter = false;
|
|
38
|
+
if (prefix) {
|
|
39
|
+
const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);
|
|
40
|
+
if (tokens.length > 0) queryParts.push(`@prefix:(${tokens.join(" ")})`);
|
|
41
|
+
}
|
|
42
|
+
for (const [_key, value] of Object.entries(filter)) if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).some((k) => k.startsWith("$"))) {
|
|
43
|
+
useClientFilter = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (queryParts.length === 0) queryParts.push("*");
|
|
47
|
+
return {
|
|
48
|
+
query: queryParts.join(" "),
|
|
49
|
+
useClientFilter
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
static matchesFieldFilter(doc, key, filterValue) {
|
|
53
|
+
const actualValue = this.getNestedValue(doc, key);
|
|
54
|
+
if (typeof filterValue === "object" && filterValue !== null && !Array.isArray(filterValue) && Object.keys(filterValue).some((k) => k.startsWith("$"))) return this.matchesOperators(actualValue, filterValue);
|
|
55
|
+
else return this.isEqual(actualValue, filterValue);
|
|
56
|
+
}
|
|
57
|
+
static matchesOperators(actualValue, operators) {
|
|
58
|
+
for (const [operator, operatorValue] of Object.entries(operators)) if (!this.matchesOperator(actualValue, operator, operatorValue)) return false;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
static matchesOperator(actualValue, operator, operatorValue) {
|
|
62
|
+
switch (operator) {
|
|
63
|
+
case "$eq": return this.isEqual(actualValue, operatorValue);
|
|
64
|
+
case "$ne": return !this.isEqual(actualValue, operatorValue);
|
|
65
|
+
case "$gt": return actualValue !== void 0 && actualValue !== null && Number(actualValue) > Number(operatorValue);
|
|
66
|
+
case "$gte": return actualValue !== void 0 && actualValue !== null && Number(actualValue) >= Number(operatorValue);
|
|
67
|
+
case "$lt": return actualValue !== void 0 && actualValue !== null && Number(actualValue) < Number(operatorValue);
|
|
68
|
+
case "$lte": return actualValue !== void 0 && actualValue !== null && Number(actualValue) <= Number(operatorValue);
|
|
69
|
+
case "$in":
|
|
70
|
+
if (!Array.isArray(operatorValue)) return false;
|
|
71
|
+
return operatorValue.some((val) => this.isEqual(actualValue, val));
|
|
72
|
+
case "$nin":
|
|
73
|
+
if (!Array.isArray(operatorValue)) return false;
|
|
74
|
+
return !operatorValue.some((val) => this.isEqual(actualValue, val));
|
|
75
|
+
case "$exists": {
|
|
76
|
+
const exists = actualValue !== void 0;
|
|
77
|
+
return operatorValue ? exists : !exists;
|
|
78
|
+
}
|
|
79
|
+
default: return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
static isEqual(a, b) {
|
|
83
|
+
if (a === b) return true;
|
|
84
|
+
if (a === null || b === null) return false;
|
|
85
|
+
if (a === void 0 || b === void 0) return false;
|
|
86
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
87
|
+
if (a.length !== b.length) return false;
|
|
88
|
+
return a.every((val, idx) => this.isEqual(val, b[idx]));
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
91
|
+
const arr = Array.isArray(a) ? a : b;
|
|
92
|
+
const val = Array.isArray(a) ? b : a;
|
|
93
|
+
return arr.includes(val);
|
|
94
|
+
}
|
|
95
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
96
|
+
const aKeys = Object.keys(a);
|
|
97
|
+
const bKeys = Object.keys(b);
|
|
98
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
99
|
+
return aKeys.every((key) => this.isEqual(a[key], b[key]));
|
|
100
|
+
}
|
|
101
|
+
return a == b;
|
|
102
|
+
}
|
|
103
|
+
static getNestedValue(obj, path) {
|
|
104
|
+
const keys = path.split(".");
|
|
105
|
+
let current = obj;
|
|
106
|
+
for (const key of keys) {
|
|
107
|
+
if (current === null || current === void 0) return void 0;
|
|
108
|
+
current = current[key];
|
|
109
|
+
}
|
|
110
|
+
return current;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
178
113
|
const REDIS_KEY_SEPARATOR = ":";
|
|
179
114
|
const STORE_PREFIX = "store";
|
|
180
115
|
const STORE_VECTOR_PREFIX = "store_vectors";
|
|
181
|
-
const SCHEMAS = [
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
return results;
|
|
690
|
-
}
|
|
691
|
-
async close() {
|
|
692
|
-
await this.client.quit();
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Get statistics about the store.
|
|
696
|
-
* Returns document counts and other metrics.
|
|
697
|
-
*/
|
|
698
|
-
async getStatistics() {
|
|
699
|
-
const stats = {
|
|
700
|
-
totalDocuments: 0,
|
|
701
|
-
namespaceCount: 0,
|
|
702
|
-
};
|
|
703
|
-
try {
|
|
704
|
-
// Get total document count
|
|
705
|
-
const countResult = await this.client.ft.search("store", "*", {
|
|
706
|
-
LIMIT: { from: 0, size: 0 },
|
|
707
|
-
});
|
|
708
|
-
stats.totalDocuments = countResult.total || 0;
|
|
709
|
-
// Get unique namespace count
|
|
710
|
-
const namespaces = await this.listNamespaces({ limit: 1000 });
|
|
711
|
-
stats.namespaceCount = namespaces.length;
|
|
712
|
-
// Get vector document count if index is configured
|
|
713
|
-
if (this.indexConfig) {
|
|
714
|
-
try {
|
|
715
|
-
const vectorResult = await this.client.ft.search("store_vectors", "*", {
|
|
716
|
-
LIMIT: { from: 0, size: 0 },
|
|
717
|
-
});
|
|
718
|
-
stats.vectorDocuments = vectorResult.total || 0;
|
|
719
|
-
}
|
|
720
|
-
catch (error) {
|
|
721
|
-
// Vector index might not exist
|
|
722
|
-
stats.vectorDocuments = 0;
|
|
723
|
-
}
|
|
724
|
-
// Get index info
|
|
725
|
-
try {
|
|
726
|
-
stats.indexInfo = await this.client.ft.info("store");
|
|
727
|
-
}
|
|
728
|
-
catch (error) {
|
|
729
|
-
// Index info might not be available
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
catch (error) {
|
|
734
|
-
if (!error.message?.includes("no such index")) {
|
|
735
|
-
throw error;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
return stats;
|
|
739
|
-
}
|
|
740
|
-
validateNamespace(namespace) {
|
|
741
|
-
if (namespace.length === 0) {
|
|
742
|
-
throw new InvalidNamespaceError("Namespace cannot be empty.");
|
|
743
|
-
}
|
|
744
|
-
for (const label of namespace) {
|
|
745
|
-
// Runtime check for JavaScript users (TypeScript already ensures this)
|
|
746
|
-
// This check is for runtime safety when called from JavaScript
|
|
747
|
-
// noinspection SuspiciousTypeOfGuard
|
|
748
|
-
if (typeof label !== "string") {
|
|
749
|
-
throw new InvalidNamespaceError(`Invalid namespace label '${String(label)}' found in ${namespace}. Namespace labels must be strings.`);
|
|
750
|
-
}
|
|
751
|
-
if (label.includes(".")) {
|
|
752
|
-
throw new InvalidNamespaceError(`Invalid namespace label '${label}' found in ${namespace}. Namespace labels cannot contain periods ('.').`);
|
|
753
|
-
}
|
|
754
|
-
if (label === "") {
|
|
755
|
-
throw new InvalidNamespaceError(`Namespace labels cannot be empty strings. Got ${label} in ${namespace}`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
if (namespace[0] === "langgraph") {
|
|
759
|
-
throw new InvalidNamespaceError(`Root label for namespace cannot be "langgraph". Got: ${namespace}`);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
async refreshItemTTL(docId) {
|
|
763
|
-
if (this.ttlConfig?.defaultTTL) {
|
|
764
|
-
const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);
|
|
765
|
-
await this.client.expire(docId, ttlSeconds);
|
|
766
|
-
// Also refresh vector key if it exists
|
|
767
|
-
const docUuid = docId.split(":").pop();
|
|
768
|
-
const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;
|
|
769
|
-
try {
|
|
770
|
-
await this.client.expire(vectorKey, ttlSeconds);
|
|
771
|
-
}
|
|
772
|
-
catch (error) {
|
|
773
|
-
// Vector key might not exist
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
escapeTagValue(value) {
|
|
778
|
-
// For TAG fields, we need to escape special characters
|
|
779
|
-
// Based on RediSearch documentation, these characters need escaping in TAG fields
|
|
780
|
-
// when used within curly braces: , . < > { } [ ] " ' : ; ! @ # $ % ^ & * ( ) - + = ~ | \ ? /
|
|
781
|
-
// Handle empty string as a special case - use a placeholder
|
|
782
|
-
if (value === "") {
|
|
783
|
-
// Use a special placeholder for empty strings
|
|
784
|
-
return "__EMPTY_STRING__";
|
|
785
|
-
}
|
|
786
|
-
// We'll escape the most common ones that appear in keys
|
|
787
|
-
return value
|
|
788
|
-
.replace(/\\/g, "\\\\")
|
|
789
|
-
.replace(/[-\s,.:<>{}[\]"';!@#$%^&*()+=~|?/]/g, "\\$&");
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Calculate similarity score based on the distance metric.
|
|
793
|
-
* Converts raw distance to a normalized similarity score [0,1].
|
|
794
|
-
*/
|
|
795
|
-
calculateSimilarityScore(distance) {
|
|
796
|
-
const metric = this.indexConfig?.distanceType || "cosine";
|
|
797
|
-
switch (metric) {
|
|
798
|
-
case "cosine":
|
|
799
|
-
// Cosine distance is in range [0,2], convert to similarity [0,1]
|
|
800
|
-
return Math.max(0, 1 - distance / 2);
|
|
801
|
-
case "l2":
|
|
802
|
-
// L2 (Euclidean) distance, use exponential decay
|
|
803
|
-
// Similarity = e^(-distance)
|
|
804
|
-
return Math.exp(-distance);
|
|
805
|
-
case "ip":
|
|
806
|
-
// Inner product can be negative, use sigmoid function
|
|
807
|
-
// Similarity = 1 / (1 + e^(-distance))
|
|
808
|
-
return 1 / (1 + Math.exp(-distance));
|
|
809
|
-
default:
|
|
810
|
-
// Default to cosine similarity
|
|
811
|
-
return Math.max(0, 1 - distance / 2);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
// Export FilterBuilder for testing purposes
|
|
816
|
-
export { FilterBuilder };
|
|
116
|
+
const SCHEMAS = [{
|
|
117
|
+
index: "store",
|
|
118
|
+
prefix: STORE_PREFIX + REDIS_KEY_SEPARATOR,
|
|
119
|
+
schema: {
|
|
120
|
+
"$.prefix": {
|
|
121
|
+
type: "TEXT",
|
|
122
|
+
AS: "prefix"
|
|
123
|
+
},
|
|
124
|
+
"$.key": {
|
|
125
|
+
type: "TAG",
|
|
126
|
+
AS: "key"
|
|
127
|
+
},
|
|
128
|
+
"$.created_at": {
|
|
129
|
+
type: "NUMERIC",
|
|
130
|
+
AS: "created_at"
|
|
131
|
+
},
|
|
132
|
+
"$.updated_at": {
|
|
133
|
+
type: "NUMERIC",
|
|
134
|
+
AS: "updated_at"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, {
|
|
138
|
+
index: "store_vectors",
|
|
139
|
+
prefix: STORE_VECTOR_PREFIX + REDIS_KEY_SEPARATOR,
|
|
140
|
+
schema: {
|
|
141
|
+
"$.prefix": {
|
|
142
|
+
type: "TEXT",
|
|
143
|
+
AS: "prefix"
|
|
144
|
+
},
|
|
145
|
+
"$.key": {
|
|
146
|
+
type: "TAG",
|
|
147
|
+
AS: "key"
|
|
148
|
+
},
|
|
149
|
+
"$.field_name": {
|
|
150
|
+
type: "TAG",
|
|
151
|
+
AS: "field_name"
|
|
152
|
+
},
|
|
153
|
+
"$.embedding": {
|
|
154
|
+
type: "VECTOR",
|
|
155
|
+
AS: "embedding"
|
|
156
|
+
},
|
|
157
|
+
"$.created_at": {
|
|
158
|
+
type: "NUMERIC",
|
|
159
|
+
AS: "created_at"
|
|
160
|
+
},
|
|
161
|
+
"$.updated_at": {
|
|
162
|
+
type: "NUMERIC",
|
|
163
|
+
AS: "updated_at"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}];
|
|
167
|
+
var RedisStore = class RedisStore {
|
|
168
|
+
client;
|
|
169
|
+
indexConfig;
|
|
170
|
+
ttlConfig;
|
|
171
|
+
embeddings;
|
|
172
|
+
constructor(client, config) {
|
|
173
|
+
this.client = client;
|
|
174
|
+
this.indexConfig = config?.index;
|
|
175
|
+
this.ttlConfig = config?.ttl;
|
|
176
|
+
if (this.indexConfig?.embed) this.embeddings = this.indexConfig.embed;
|
|
177
|
+
}
|
|
178
|
+
static async fromConnString(connString, config) {
|
|
179
|
+
const client = createClient({ url: connString });
|
|
180
|
+
await client.connect();
|
|
181
|
+
const store = new RedisStore(client, config);
|
|
182
|
+
await store.setup();
|
|
183
|
+
return store;
|
|
184
|
+
}
|
|
185
|
+
static async fromCluster(rootNodes, config) {
|
|
186
|
+
const client = createCluster({ rootNodes });
|
|
187
|
+
await client.connect();
|
|
188
|
+
const store = new RedisStore(client, config);
|
|
189
|
+
await store.setup();
|
|
190
|
+
return store;
|
|
191
|
+
}
|
|
192
|
+
async setup() {
|
|
193
|
+
try {
|
|
194
|
+
await this.client.ft.create(SCHEMAS[0].index, SCHEMAS[0].schema, {
|
|
195
|
+
ON: "JSON",
|
|
196
|
+
PREFIX: SCHEMAS[0].prefix
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (!error.message?.includes("Index already exists")) console.error("Failed to create store index:", error.message);
|
|
200
|
+
}
|
|
201
|
+
if (this.indexConfig) {
|
|
202
|
+
const dims = this.indexConfig.dims;
|
|
203
|
+
const distanceMetric = this.indexConfig.distanceType === "cosine" ? "COSINE" : this.indexConfig.distanceType === "l2" ? "L2" : this.indexConfig.distanceType === "ip" ? "IP" : "COSINE";
|
|
204
|
+
const vectorSchema = {
|
|
205
|
+
"$.prefix": {
|
|
206
|
+
type: "TEXT",
|
|
207
|
+
AS: "prefix"
|
|
208
|
+
},
|
|
209
|
+
"$.key": {
|
|
210
|
+
type: "TAG",
|
|
211
|
+
AS: "key"
|
|
212
|
+
},
|
|
213
|
+
"$.field_name": {
|
|
214
|
+
type: "TAG",
|
|
215
|
+
AS: "field_name"
|
|
216
|
+
},
|
|
217
|
+
"$.created_at": {
|
|
218
|
+
type: "NUMERIC",
|
|
219
|
+
AS: "created_at"
|
|
220
|
+
},
|
|
221
|
+
"$.updated_at": {
|
|
222
|
+
type: "NUMERIC",
|
|
223
|
+
AS: "updated_at"
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
vectorSchema["$.embedding"] = {
|
|
227
|
+
type: "VECTOR",
|
|
228
|
+
ALGORITHM: "FLAT",
|
|
229
|
+
TYPE: "FLOAT32",
|
|
230
|
+
DIM: dims,
|
|
231
|
+
DISTANCE_METRIC: distanceMetric,
|
|
232
|
+
AS: "embedding"
|
|
233
|
+
};
|
|
234
|
+
try {
|
|
235
|
+
await this.client.ft.create(SCHEMAS[1].index, vectorSchema, {
|
|
236
|
+
ON: "JSON",
|
|
237
|
+
PREFIX: SCHEMAS[1].prefix
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!error.message?.includes("Index already exists")) console.error("Failed to create vector index:", error.message);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async get(namespace, key, options) {
|
|
245
|
+
const prefix = namespace.join(".");
|
|
246
|
+
const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);
|
|
247
|
+
const prefixQuery = tokens.length > 0 ? `@prefix:(${tokens.join(" ")})` : "*";
|
|
248
|
+
let query;
|
|
249
|
+
if (key === "") query = prefixQuery;
|
|
250
|
+
else {
|
|
251
|
+
const escapedKey = this.escapeTagValue(key);
|
|
252
|
+
query = `(${prefixQuery}) (@key:{${escapedKey}})`;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const results = await this.client.ft.search("store", query, { LIMIT: {
|
|
256
|
+
from: 0,
|
|
257
|
+
size: key === "" ? 100 : 1
|
|
258
|
+
} });
|
|
259
|
+
if (!results || !results.documents || results.documents.length === 0) return null;
|
|
260
|
+
if (key === "") {
|
|
261
|
+
for (const doc$1 of results.documents) {
|
|
262
|
+
const jsonDoc$1 = doc$1.value;
|
|
263
|
+
if (jsonDoc$1.key === "" && jsonDoc$1.prefix === prefix) {
|
|
264
|
+
const docId$1 = doc$1.id;
|
|
265
|
+
if (options?.refreshTTL) await this.refreshItemTTL(docId$1);
|
|
266
|
+
return {
|
|
267
|
+
value: jsonDoc$1.value,
|
|
268
|
+
key: jsonDoc$1.key,
|
|
269
|
+
namespace: jsonDoc$1.prefix.split("."),
|
|
270
|
+
created_at: /* @__PURE__ */ new Date(jsonDoc$1.created_at / 1e6),
|
|
271
|
+
updated_at: /* @__PURE__ */ new Date(jsonDoc$1.updated_at / 1e6)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const doc = results.documents[0];
|
|
278
|
+
const jsonDoc = doc.value;
|
|
279
|
+
const docId = doc.id;
|
|
280
|
+
if (options?.refreshTTL) await this.refreshItemTTL(docId);
|
|
281
|
+
return {
|
|
282
|
+
value: jsonDoc.value,
|
|
283
|
+
key: jsonDoc.key,
|
|
284
|
+
namespace: jsonDoc.prefix.split("."),
|
|
285
|
+
created_at: /* @__PURE__ */ new Date(jsonDoc.created_at / 1e6),
|
|
286
|
+
updated_at: /* @__PURE__ */ new Date(jsonDoc.updated_at / 1e6)
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error.message?.includes("no such index")) return null;
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async put(namespace, key, value, options) {
|
|
294
|
+
this.validateNamespace(namespace);
|
|
295
|
+
const prefix = namespace.join(".");
|
|
296
|
+
const docId = v4_default();
|
|
297
|
+
const now = Date.now() * 1e6 + Math.floor(performance.now() * 1e3);
|
|
298
|
+
let createdAt = now;
|
|
299
|
+
const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);
|
|
300
|
+
const prefixQuery = tokens.length > 0 ? `@prefix:(${tokens.join(" ")})` : "*";
|
|
301
|
+
const escapedKey = this.escapeTagValue(key);
|
|
302
|
+
const existingQuery = `(${prefixQuery}) (@key:{${escapedKey}})`;
|
|
303
|
+
try {
|
|
304
|
+
const existing = await this.client.ft.search("store", existingQuery, { LIMIT: {
|
|
305
|
+
from: 0,
|
|
306
|
+
size: 1
|
|
307
|
+
} });
|
|
308
|
+
if (existing && existing.documents && existing.documents.length > 0) {
|
|
309
|
+
const oldDocId = existing.documents[0].id;
|
|
310
|
+
const existingDoc = await this.client.json.get(oldDocId);
|
|
311
|
+
if (existingDoc && typeof existingDoc === "object" && "created_at" in existingDoc) createdAt = existingDoc.created_at;
|
|
312
|
+
await this.client.del(oldDocId);
|
|
313
|
+
if (this.indexConfig) {
|
|
314
|
+
const oldUuid = oldDocId.split(":").pop();
|
|
315
|
+
const oldVectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${oldUuid}`;
|
|
316
|
+
try {
|
|
317
|
+
await this.client.del(oldVectorKey);
|
|
318
|
+
} catch (error) {}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {}
|
|
322
|
+
if (value === null) return;
|
|
323
|
+
const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;
|
|
324
|
+
const doc = {
|
|
325
|
+
prefix,
|
|
326
|
+
key,
|
|
327
|
+
value,
|
|
328
|
+
created_at: createdAt,
|
|
329
|
+
updated_at: now
|
|
330
|
+
};
|
|
331
|
+
await this.client.json.set(storeKey, "$", doc);
|
|
332
|
+
if (this.indexConfig && this.embeddings && options?.index !== false) {
|
|
333
|
+
const fieldsToIndex = options && Array.isArray(options.index) ? options.index : this.indexConfig.fields || ["text"];
|
|
334
|
+
const textsToEmbed = [];
|
|
335
|
+
const fieldNames = [];
|
|
336
|
+
for (const field of fieldsToIndex) if (value[field]) {
|
|
337
|
+
textsToEmbed.push(value[field]);
|
|
338
|
+
fieldNames.push(field);
|
|
339
|
+
}
|
|
340
|
+
if (textsToEmbed.length > 0) {
|
|
341
|
+
const embeddings = await this.embeddings.embedDocuments(textsToEmbed);
|
|
342
|
+
for (let i = 0; i < embeddings.length; i++) {
|
|
343
|
+
const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;
|
|
344
|
+
const vectorDoc = {
|
|
345
|
+
prefix,
|
|
346
|
+
key,
|
|
347
|
+
field_name: fieldNames[i],
|
|
348
|
+
embedding: embeddings[i],
|
|
349
|
+
created_at: now,
|
|
350
|
+
updated_at: now
|
|
351
|
+
};
|
|
352
|
+
await this.client.json.set(vectorKey, "$", vectorDoc);
|
|
353
|
+
const ttlMinutes$1 = options?.ttl || this.ttlConfig?.defaultTTL;
|
|
354
|
+
if (ttlMinutes$1) {
|
|
355
|
+
const ttlSeconds = Math.floor(ttlMinutes$1 * 60);
|
|
356
|
+
await this.client.expire(vectorKey, ttlSeconds);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;
|
|
362
|
+
if (ttlMinutes) {
|
|
363
|
+
const ttlSeconds = Math.floor(ttlMinutes * 60);
|
|
364
|
+
await this.client.expire(storeKey, ttlSeconds);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async delete(namespace, key) {
|
|
368
|
+
await this.put(namespace, key, null);
|
|
369
|
+
}
|
|
370
|
+
async search(namespacePrefix, options) {
|
|
371
|
+
const prefix = namespacePrefix.join(".");
|
|
372
|
+
const limit = options?.limit || 10;
|
|
373
|
+
const offset = options?.offset || 0;
|
|
374
|
+
if (options?.query && this.indexConfig && this.embeddings) {
|
|
375
|
+
const [embedding] = await this.embeddings.embedDocuments([options.query]);
|
|
376
|
+
let queryStr$1 = prefix ? `@prefix:${prefix.split(/[.-]/)[0]}*` : "*";
|
|
377
|
+
const vectorBytes = Buffer.from(new Float32Array(embedding).buffer);
|
|
378
|
+
try {
|
|
379
|
+
const results = await this.client.ft.search("store_vectors", `(${queryStr$1})=>[KNN ${limit} @embedding $BLOB]`, {
|
|
380
|
+
PARAMS: { BLOB: vectorBytes },
|
|
381
|
+
DIALECT: 2,
|
|
382
|
+
LIMIT: {
|
|
383
|
+
from: offset,
|
|
384
|
+
size: limit
|
|
385
|
+
},
|
|
386
|
+
RETURN: [
|
|
387
|
+
"prefix",
|
|
388
|
+
"key",
|
|
389
|
+
"__embedding_score"
|
|
390
|
+
]
|
|
391
|
+
});
|
|
392
|
+
const items = [];
|
|
393
|
+
for (const doc of results.documents) {
|
|
394
|
+
const docUuid = doc.id.split(":").pop();
|
|
395
|
+
const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;
|
|
396
|
+
const storeDoc = await this.client.json.get(storeKey);
|
|
397
|
+
if (storeDoc) {
|
|
398
|
+
if (options.filter) {
|
|
399
|
+
if (!FilterBuilder.matchesFilter(storeDoc.value || {}, options.filter)) continue;
|
|
400
|
+
}
|
|
401
|
+
if (options.refreshTTL) {
|
|
402
|
+
await this.refreshItemTTL(storeKey);
|
|
403
|
+
await this.refreshItemTTL(doc.id);
|
|
404
|
+
}
|
|
405
|
+
const score = doc.value?.__embedding_score ? this.calculateSimilarityScore(parseFloat(doc.value.__embedding_score)) : 0;
|
|
406
|
+
const threshold = options.similarityThreshold ?? this.indexConfig?.similarityThreshold;
|
|
407
|
+
if (threshold !== void 0 && score < threshold) continue;
|
|
408
|
+
items.push({
|
|
409
|
+
value: storeDoc.value,
|
|
410
|
+
key: storeDoc.key,
|
|
411
|
+
namespace: storeDoc.prefix.split("."),
|
|
412
|
+
created_at: /* @__PURE__ */ new Date(storeDoc.created_at / 1e6),
|
|
413
|
+
updated_at: /* @__PURE__ */ new Date(storeDoc.updated_at / 1e6),
|
|
414
|
+
score
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return items;
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error.message?.includes("no such index")) return [];
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let queryStr = "*";
|
|
425
|
+
if (prefix) {
|
|
426
|
+
const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);
|
|
427
|
+
if (tokens.length > 0) queryStr = `@prefix:(${tokens.join(" ")})`;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
const results = await this.client.ft.search("store", queryStr, {
|
|
431
|
+
LIMIT: {
|
|
432
|
+
from: offset,
|
|
433
|
+
size: limit
|
|
434
|
+
},
|
|
435
|
+
SORTBY: {
|
|
436
|
+
BY: "created_at",
|
|
437
|
+
DIRECTION: "DESC"
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
const items = [];
|
|
441
|
+
for (const doc of results.documents) {
|
|
442
|
+
const jsonDoc = doc.value;
|
|
443
|
+
if (options?.filter) {
|
|
444
|
+
if (!FilterBuilder.matchesFilter(jsonDoc.value || {}, options.filter)) continue;
|
|
445
|
+
}
|
|
446
|
+
if (options?.refreshTTL) await this.refreshItemTTL(doc.id);
|
|
447
|
+
items.push({
|
|
448
|
+
value: jsonDoc.value,
|
|
449
|
+
key: jsonDoc.key,
|
|
450
|
+
namespace: jsonDoc.prefix.split("."),
|
|
451
|
+
created_at: /* @__PURE__ */ new Date(jsonDoc.created_at / 1e6),
|
|
452
|
+
updated_at: /* @__PURE__ */ new Date(jsonDoc.updated_at / 1e6)
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return items;
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (error.message?.includes("no such index")) return [];
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async listNamespaces(options) {
|
|
462
|
+
let query = "*";
|
|
463
|
+
try {
|
|
464
|
+
const results = await this.client.ft.search("store", query, {
|
|
465
|
+
LIMIT: {
|
|
466
|
+
from: 0,
|
|
467
|
+
size: 1e3
|
|
468
|
+
},
|
|
469
|
+
RETURN: ["prefix"]
|
|
470
|
+
});
|
|
471
|
+
const namespaceSet = /* @__PURE__ */ new Set();
|
|
472
|
+
for (const doc of results.documents) {
|
|
473
|
+
const prefix = doc.value.prefix;
|
|
474
|
+
const parts = prefix.split(".");
|
|
475
|
+
if (options?.prefix) {
|
|
476
|
+
if (parts.length < options.prefix.length) continue;
|
|
477
|
+
let matches = true;
|
|
478
|
+
for (let i = 0; i < options.prefix.length; i++) if (parts[i] !== options.prefix[i]) {
|
|
479
|
+
matches = false;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
if (!matches) continue;
|
|
483
|
+
}
|
|
484
|
+
if (options?.suffix) {
|
|
485
|
+
if (parts.length < options.suffix.length) continue;
|
|
486
|
+
let matches = true;
|
|
487
|
+
const startIdx = parts.length - options.suffix.length;
|
|
488
|
+
for (let i = 0; i < options.suffix.length; i++) if (parts[startIdx + i] !== options.suffix[i]) {
|
|
489
|
+
matches = false;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
if (!matches) continue;
|
|
493
|
+
}
|
|
494
|
+
if (options?.maxDepth) {
|
|
495
|
+
const truncated = parts.slice(0, options.maxDepth);
|
|
496
|
+
namespaceSet.add(truncated.join("."));
|
|
497
|
+
} else namespaceSet.add(prefix);
|
|
498
|
+
}
|
|
499
|
+
let namespaces = Array.from(namespaceSet).map((ns) => ns.split(".")).sort((a, b) => a.join(".").localeCompare(b.join(".")));
|
|
500
|
+
if (options?.offset || options?.limit) {
|
|
501
|
+
const offset = options.offset || 0;
|
|
502
|
+
const limit = options.limit || 10;
|
|
503
|
+
namespaces = namespaces.slice(offset, offset + limit);
|
|
504
|
+
}
|
|
505
|
+
return namespaces;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (error.message?.includes("no such index")) return [];
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async batch(ops) {
|
|
512
|
+
const results = new Array(ops.length).fill(null);
|
|
513
|
+
for (let idx = 0; idx < ops.length; idx++) {
|
|
514
|
+
const op = ops[idx];
|
|
515
|
+
if (isPutOperation(op)) {
|
|
516
|
+
await this.put(op.namespace, op.key, op.value);
|
|
517
|
+
results[idx] = null;
|
|
518
|
+
} else if (isSearchOperation(op)) results[idx] = await this.search(op.namespacePrefix, {
|
|
519
|
+
filter: op.filter,
|
|
520
|
+
query: op.query,
|
|
521
|
+
limit: op.limit,
|
|
522
|
+
offset: op.offset
|
|
523
|
+
});
|
|
524
|
+
else if (isListNamespacesOperation(op)) {
|
|
525
|
+
let prefix = void 0;
|
|
526
|
+
let suffix = void 0;
|
|
527
|
+
if (op.matchConditions) {
|
|
528
|
+
for (const condition of op.matchConditions) if (condition.matchType === "prefix") prefix = condition.path;
|
|
529
|
+
else if (condition.matchType === "suffix") suffix = condition.path;
|
|
530
|
+
}
|
|
531
|
+
results[idx] = await this.listNamespaces({
|
|
532
|
+
prefix,
|
|
533
|
+
suffix,
|
|
534
|
+
maxDepth: op.maxDepth,
|
|
535
|
+
limit: op.limit,
|
|
536
|
+
offset: op.offset
|
|
537
|
+
});
|
|
538
|
+
} else if (isGetOperation(op)) results[idx] = await this.get(op.namespace, op.key);
|
|
539
|
+
else throw new Error(`Unknown operation type: ${JSON.stringify(op)}`);
|
|
540
|
+
}
|
|
541
|
+
return results;
|
|
542
|
+
}
|
|
543
|
+
async close() {
|
|
544
|
+
await this.client.quit();
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Get statistics about the store.
|
|
548
|
+
* Returns document counts and other metrics.
|
|
549
|
+
*/
|
|
550
|
+
async getStatistics() {
|
|
551
|
+
const stats = {
|
|
552
|
+
totalDocuments: 0,
|
|
553
|
+
namespaceCount: 0
|
|
554
|
+
};
|
|
555
|
+
try {
|
|
556
|
+
const countResult = await this.client.ft.search("store", "*", { LIMIT: {
|
|
557
|
+
from: 0,
|
|
558
|
+
size: 0
|
|
559
|
+
} });
|
|
560
|
+
stats.totalDocuments = countResult.total || 0;
|
|
561
|
+
const namespaces = await this.listNamespaces({ limit: 1e3 });
|
|
562
|
+
stats.namespaceCount = namespaces.length;
|
|
563
|
+
if (this.indexConfig) {
|
|
564
|
+
try {
|
|
565
|
+
const vectorResult = await this.client.ft.search("store_vectors", "*", { LIMIT: {
|
|
566
|
+
from: 0,
|
|
567
|
+
size: 0
|
|
568
|
+
} });
|
|
569
|
+
stats.vectorDocuments = vectorResult.total || 0;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
stats.vectorDocuments = 0;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
stats.indexInfo = await this.client.ft.info("store");
|
|
575
|
+
} catch (error) {}
|
|
576
|
+
}
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (!error.message?.includes("no such index")) throw error;
|
|
579
|
+
}
|
|
580
|
+
return stats;
|
|
581
|
+
}
|
|
582
|
+
validateNamespace(namespace) {
|
|
583
|
+
if (namespace.length === 0) throw new InvalidNamespaceError("Namespace cannot be empty.");
|
|
584
|
+
for (const label of namespace) {
|
|
585
|
+
if (typeof label !== "string") throw new InvalidNamespaceError(`Invalid namespace label '${String(label)}' found in ${namespace}. Namespace labels must be strings.`);
|
|
586
|
+
if (label.includes(".")) throw new InvalidNamespaceError(`Invalid namespace label '${label}' found in ${namespace}. Namespace labels cannot contain periods ('.').`);
|
|
587
|
+
if (label === "") throw new InvalidNamespaceError(`Namespace labels cannot be empty strings. Got ${label} in ${namespace}`);
|
|
588
|
+
}
|
|
589
|
+
if (namespace[0] === "langgraph") throw new InvalidNamespaceError(`Root label for namespace cannot be "langgraph". Got: ${namespace}`);
|
|
590
|
+
}
|
|
591
|
+
async refreshItemTTL(docId) {
|
|
592
|
+
if (this.ttlConfig?.defaultTTL) {
|
|
593
|
+
const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);
|
|
594
|
+
await this.client.expire(docId, ttlSeconds);
|
|
595
|
+
const docUuid = docId.split(":").pop();
|
|
596
|
+
const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;
|
|
597
|
+
try {
|
|
598
|
+
await this.client.expire(vectorKey, ttlSeconds);
|
|
599
|
+
} catch (error) {}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
escapeTagValue(value) {
|
|
603
|
+
if (value === "") return "__EMPTY_STRING__";
|
|
604
|
+
return value.replace(/\\/g, "\\\\").replace(/[-\s,.:<>{}[\]"';!@#$%^&*()+=~|?/]/g, "\\$&");
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Calculate similarity score based on the distance metric.
|
|
608
|
+
* Converts raw distance to a normalized similarity score [0,1].
|
|
609
|
+
*/
|
|
610
|
+
calculateSimilarityScore(distance) {
|
|
611
|
+
const metric = this.indexConfig?.distanceType || "cosine";
|
|
612
|
+
switch (metric) {
|
|
613
|
+
case "cosine": return Math.max(0, 1 - distance / 2);
|
|
614
|
+
case "l2": return Math.exp(-distance);
|
|
615
|
+
case "ip": return 1 / (1 + Math.exp(-distance));
|
|
616
|
+
default: return Math.max(0, 1 - distance / 2);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
//#endregion
|
|
622
|
+
export { FilterBuilder, RedisStore, isGetOperation, isListNamespacesOperation, isPutOperation, isSearchOperation };
|
|
817
623
|
//# sourceMappingURL=store.js.map
|