next-semantic-cache 1.0.0 → 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/README.md +188 -303
- package/dist/redis-vector-adapter.d.ts +135 -0
- package/dist/redis-vector-adapter.js +323 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,303 +1,188 @@
|
|
|
1
|
-
# next-semantic-cache
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const deleted = await adapter.flush();
|
|
190
|
-
console.log(`Removed ${deleted} entries from the semantic cache.`);
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
### `VectorStoreAdapter` interface
|
|
196
|
-
|
|
197
|
-
Bring your own storage backend by implementing two methods:
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
export interface VectorStoreAdapter {
|
|
201
|
-
/**
|
|
202
|
-
* Return the cached response string if a similar prompt exists,
|
|
203
|
-
* or null if no match exceeds the threshold.
|
|
204
|
-
*/
|
|
205
|
-
search(vector: number[], threshold: number): Promise<string | null>;
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Persist the prompt embedding alongside its LLM response.
|
|
209
|
-
*/
|
|
210
|
-
save(promptVector: number[], response: string): Promise<void>;
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
#### Example — in-memory adapter (for unit testing)
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
import type { VectorStoreAdapter } from "next-semantic-cache";
|
|
218
|
-
|
|
219
|
-
class InMemoryAdapter implements VectorStoreAdapter {
|
|
220
|
-
private store: Array<{ vector: number[]; response: string }> = [];
|
|
221
|
-
|
|
222
|
-
private cosineSimilarity(a: number[], b: number[]): number {
|
|
223
|
-
const dot = a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
224
|
-
const magA = Math.sqrt(a.reduce((s, v) => s + v * v, 0));
|
|
225
|
-
const magB = Math.sqrt(b.reduce((s, v) => s + v * v, 0));
|
|
226
|
-
return dot / (magA * magB);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async search(vector: number[], threshold: number) {
|
|
230
|
-
for (const entry of this.store) {
|
|
231
|
-
if (this.cosineSimilarity(vector, entry.vector) >= threshold) {
|
|
232
|
-
return entry.response;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async save(promptVector: number[], response: string) {
|
|
239
|
-
this.store.push({ vector: promptVector, response });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
## Architecture notes
|
|
247
|
-
|
|
248
|
-
### Singleton embedding model
|
|
249
|
-
|
|
250
|
-
`@huggingface/transformers` loads `Xenova/all-MiniLM-L6-v2` (quantised q8 ONNX, ~23 MB) **once per process**. The pipeline instance is held in a module-level variable and reused on every subsequent request. Cold-start cost: ~400–800 ms. Warm embedding cost: ~2–5 ms.
|
|
251
|
-
|
|
252
|
-
### Middleware interception flow
|
|
253
|
-
|
|
254
|
-
The middleware implements two hooks from the Vercel AI SDK's `LanguageModelV1Middleware` spec:
|
|
255
|
-
|
|
256
|
-
- **`transformParams`** — runs synchronously *before* the LLM call. Embeds the prompt and attaches a `__semanticCacheHit` sentinel to `providerMetadata` on a hit.
|
|
257
|
-
- **`wrapGenerate`** — reads the sentinel. If present, returns the cached result and **never calls `doGenerate()`**. On a miss it calls `doGenerate()` and fire-and-forgets the `save()`.
|
|
258
|
-
|
|
259
|
-
### Failure isolation
|
|
260
|
-
|
|
261
|
-
If Redis is unreachable or times out:
|
|
262
|
-
- `search()` returns `null` → the middleware calls the real LLM transparently.
|
|
263
|
-
- `save()` logs the error and returns — the LLM response has already been streamed to the user.
|
|
264
|
-
|
|
265
|
-
**Redis failures are completely invisible to your users.**
|
|
266
|
-
|
|
267
|
-
---
|
|
268
|
-
|
|
269
|
-
## Threshold tuning guide
|
|
270
|
-
|
|
271
|
-
| Threshold | Behaviour | Use-case |
|
|
272
|
-
|---|---|---|
|
|
273
|
-
| **0.98** | Near-identical phrasing only | Deduplicate exact retries |
|
|
274
|
-
| **0.92** *(default)* | Catches paraphrases & minor rewording | General-purpose FAQ / support bots |
|
|
275
|
-
| **0.88** | Broad semantic matching | High-traffic apps where some imprecision is acceptable |
|
|
276
|
-
| **0.80** | Very aggressive | Only for tightly constrained, single-domain prompts |
|
|
277
|
-
|
|
278
|
-
---
|
|
279
|
-
|
|
280
|
-
## FAQ
|
|
281
|
-
|
|
282
|
-
**Does it work in the Vercel Edge Runtime?**
|
|
283
|
-
Yes. `@huggingface/transformers` with ONNX backend runs in Edge (V8 isolate) environments. The `@upstash/redis` client uses the REST API so there are no raw TCP sockets.
|
|
284
|
-
|
|
285
|
-
**Can I use a different LLM — Anthropic Claude, Google Gemini, etc.?**
|
|
286
|
-
Yes. `withSemanticCache` accepts any `LanguageModelV1`-compatible model. Swap `openai(...)` for `anthropic(...)`, `google(...)`, or any community provider.
|
|
287
|
-
|
|
288
|
-
**Does it support streaming?**
|
|
289
|
-
Cache misses stream normally via the underlying model. Cache hits return a non-streaming synthetic result — the text is complete and the SDK delivers it as a resolved promise. To surface hits as streams, extend `buildCachedGenerateResult` to use `wrapStream` instead.
|
|
290
|
-
|
|
291
|
-
**Does it work with `generateObject` / structured output?**
|
|
292
|
-
The middleware hooks into `wrapGenerate`, which also underlies `generateObject`. Cached responses are raw text; if your object-generation prompt is highly templated and structurally identical across calls, hits will work correctly. For variable schema inputs, raise `threshold` to ≥ 0.97.
|
|
293
|
-
|
|
294
|
-
**Will it slow down my first request on a cold start?**
|
|
295
|
-
The embedding model loads in ~400–800 ms on the first invocation. Vercel pre-warms functions in production, reducing the impact. You can also explicitly warm the pipeline by calling `getEmbeddingPipeline()` from a `warmup` route.
|
|
296
|
-
|
|
297
|
-
---
|
|
298
|
-
|
|
299
|
-
## License
|
|
300
|
-
|
|
301
|
-
MIT © 2026 — contributions welcome.
|
|
302
|
-
|
|
303
|
-
> **Install deps:** `npm i next-semantic-cache @upstash/redis uuid ai`
|
|
1
|
+
# next-semantic-cache
|
|
2
|
+
|
|
3
|
+
> Semantic caching middleware for the [Vercel AI SDK](https://sdk.vercel.ai) — short-circuit LLM calls with vector similarity lookups.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/next-semantic-cache)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
`next-semantic-cache` is a drop-in `LanguageModelV4Middleware` that intercepts prompts, embeds them locally, and serves cached responses when a semantically similar prompt has been seen before. It ships with a pluggable `VectorStoreAdapter` interface and a production-ready Redis (RediSearch / Upstash) implementation.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why?
|
|
13
|
+
|
|
14
|
+
LLM calls are **slow** and **expensive**. Traditional caching only matches *exact* strings — but users phrase the same question in countless ways. `next-semantic-cache` matches on **meaning**, not exact text:
|
|
15
|
+
|
|
16
|
+
| Prompt A | Prompt B | Exact cache | Semantic cache |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| "How do I reset my password?" | "What's the steps to change my password?" | ❌ Miss | ✅ Hit |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- 🧠 **Semantic matching** — embeds prompts with `all-MiniLM-L6-v2` and matches on cosine similarity.
|
|
25
|
+
- 🔌 **Pluggable storage** — implement `VectorStoreAdapter` for any vector DB (Pinecone, pgvector, Weaviate, in-memory, …).
|
|
26
|
+
- 🚀 **Redis adapter included** — works with **Upstash Redis** (serverless) or self-hosted **Redis Stack**.
|
|
27
|
+
- 🛡️ **Defensive by design** — every cache operation is wrapped in try/catch. If the store is down, you transparently fall back to the live LLM. **A cache failure never breaks your app.**
|
|
28
|
+
- ⏱️ **Configurable TTL** — auto-expire entries via Redis `EXPIRE`.
|
|
29
|
+
- 📦 **Zero-config embeddings** — the embedding model runs locally; no extra API keys required.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install next-semantic-cache ai @huggingface/transformers
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For the Redis adapter, also install:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @upstash/redis uuid
|
|
43
|
+
npm install -D @types/uuid
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { openai } from "@ai-sdk/openai";
|
|
52
|
+
import { wrapLanguageModel, generateText } from "ai";
|
|
53
|
+
import { SemanticCacheMiddleware } from "next-semantic-cache";
|
|
54
|
+
import { RedisVectorAdapter } from "next-semantic-cache/redis-vector-adapter";
|
|
55
|
+
|
|
56
|
+
// 1. Create a vector store adapter.
|
|
57
|
+
const vectorStore = new RedisVectorAdapter({
|
|
58
|
+
redisUrl: process.env.UPSTASH_REDIS_REST_URL!,
|
|
59
|
+
redisToken: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
60
|
+
ttlSeconds: 60 * 60 * 24, // 24-hour cache expiry
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 2. Wrap your model with the middleware.
|
|
64
|
+
const model = wrapLanguageModel({
|
|
65
|
+
model: openai("gpt-4o"),
|
|
66
|
+
middleware: SemanticCacheMiddleware({
|
|
67
|
+
vectorStore,
|
|
68
|
+
similarityThreshold: 0.92, // 0–1; higher = stricter match
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 3. Use it exactly like any AI SDK model.
|
|
73
|
+
const { text } = await generateText({
|
|
74
|
+
model,
|
|
75
|
+
prompt: "Explain quantum entanglement in one sentence.",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(text);
|
|
79
|
+
// First call → hits the LLM, caches the result.
|
|
80
|
+
// Later calls with a similar prompt → served instantly from cache.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
### `SemanticCacheMiddleware(options)`
|
|
88
|
+
|
|
89
|
+
| Option | Type | Default | Description |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| `vectorStore` | `VectorStoreAdapter` | **required** | The persistence backend for embeddings and responses. |
|
|
92
|
+
| `similarityThreshold` | `number` | `0.92` | Minimum cosine similarity (0–1) for a cache hit. Higher = fewer, more accurate hits. |
|
|
93
|
+
|
|
94
|
+
### `RedisVectorAdapter(options)`
|
|
95
|
+
|
|
96
|
+
| Option | Type | Default | Description |
|
|
97
|
+
| --- | --- | --- | --- |
|
|
98
|
+
| `client` | `Redis` | — | A pre-configured `@upstash/redis` client. Mutually exclusive with `redisUrl` + `redisToken`. |
|
|
99
|
+
| `redisUrl` | `string` | — | Upstash REST endpoint. Required if `client` is omitted. |
|
|
100
|
+
| `redisToken` | `string` | — | Upstash REST token. Required if `client` is omitted. |
|
|
101
|
+
| `ttlSeconds` | `number` | `0` | Auto-expire entries after N seconds. `0` = keep indefinitely. |
|
|
102
|
+
| `connectionTimeoutMs` | `number` | `2000` | Timeout per Redis command before falling back to the LLM. |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Writing a Custom Adapter
|
|
107
|
+
|
|
108
|
+
Implement the `VectorStoreAdapter` interface to plug in any backend:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import type { VectorStoreAdapter } from "next-semantic-cache";
|
|
112
|
+
|
|
113
|
+
export class MyVectorAdapter implements VectorStoreAdapter {
|
|
114
|
+
/**
|
|
115
|
+
* Return the cached response if a stored vector is within `threshold`
|
|
116
|
+
* cosine similarity of `vector`, otherwise `null`.
|
|
117
|
+
*/
|
|
118
|
+
async search(vector: number[], threshold: number): Promise<string | null> {
|
|
119
|
+
// ... query your vector DB ...
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Persist a prompt embedding and its LLM response.
|
|
125
|
+
* Failures should be swallowed — caching is best-effort.
|
|
126
|
+
*/
|
|
127
|
+
async save(promptVector: number[], response: string): Promise<void> {
|
|
128
|
+
// ... upsert into your vector DB ...
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## How It Works
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
┌──────────────────────────────┐
|
|
139
|
+
prompt ──────────► │ transformParams │
|
|
140
|
+
│ • embed prompt (MiniLM-L6) │
|
|
141
|
+
│ • vectorStore.search(...) │
|
|
142
|
+
└───────────┬──────────────────┘
|
|
143
|
+
│
|
|
144
|
+
┌────────────────┴─────────────────┐
|
|
145
|
+
▼ ▼
|
|
146
|
+
Cache HIT Cache MISS
|
|
147
|
+
┌──────────────────────┐ ┌──────────────────────────┐
|
|
148
|
+
│ return synthetic │ │ wrapGenerate: │
|
|
149
|
+
│ result — LLM is │ │ • call real model │
|
|
150
|
+
│ NEVER invoked │ │ • vectorStore.save(...) │
|
|
151
|
+
└──────────────────────┘ │ (fire-and-forget) │
|
|
152
|
+
└──────────────────────────┘
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
1. **`transformParams`** embeds the incoming prompt and queries the vector store.
|
|
156
|
+
2. **Cache hit** → a synthetic `LanguageModelV4GenerateResult` is returned immediately; the real model is short-circuited.
|
|
157
|
+
3. **Cache miss** → the request falls through to the real model, and the response is saved to the store asynchronously (never blocking the response path).
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Redis Storage Layout
|
|
162
|
+
|
|
163
|
+
The `RedisVectorAdapter` stores each entry as a Redis `HASH` and creates an HNSW vector index over them:
|
|
164
|
+
|
|
165
|
+
| Component | Value |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| Key prefix | `sc:<uuid>` |
|
|
168
|
+
| Fields | `response`, `vector` (Float32 binary), `createdAt` (ISO-8601) |
|
|
169
|
+
| Index | `FT.CREATE` — HNSW, `FLOAT32`, `DIM 384`, `COSINE` distance |
|
|
170
|
+
|
|
171
|
+
The index is created **lazily** on first `save()`/`search()` and is idempotent.
|
|
172
|
+
|
|
173
|
+
> ⚠️ **Note on Upstash + binary vectors:** `@upstash/redis` transports commands as JSON over HTTP. Binary vector payloads may not round-trip reliably through the REST layer. If you encounter KNN query errors, use a **Redis Stack** instance with a native `ioredis` client for full binary support.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Requirements
|
|
178
|
+
|
|
179
|
+
- **Node.js** ≥ 18
|
|
180
|
+
- **Vercel AI SDK** (`ai`) v4+
|
|
181
|
+
- A vector store (Redis Stack / Upstash, or your own adapter)
|
|
182
|
+
- Embedding model: `all-MiniLM-L6-v2` (384-dim) via `@huggingface/transformers`
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT © [Prateek Chaturvedi](https://github.com/chaturvedi-prateek)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* redis-vector-adapter.ts
|
|
3
|
+
*
|
|
4
|
+
* Concrete implementation of VectorStoreAdapter using Upstash Redis
|
|
5
|
+
* (compatible with any Redis instance that has the RediSearch / Redis Stack
|
|
6
|
+
* module enabled — the same FT.* command surface is used in both cases).
|
|
7
|
+
*
|
|
8
|
+
* Storage layout
|
|
9
|
+
* ─────────────
|
|
10
|
+
* Key prefix : sc:<uuid> (Redis HASH)
|
|
11
|
+
* Fields : response — raw LLM response text
|
|
12
|
+
* vector — Float32 embedding encoded as a binary string
|
|
13
|
+
* createdAt — ISO-8601 timestamp for observability
|
|
14
|
+
*
|
|
15
|
+
* Index : FT.CREATE on the HASH prefix using an HNSW VECTOR field
|
|
16
|
+
* with COSINE distance. Created lazily on first save/search.
|
|
17
|
+
*
|
|
18
|
+
* Failure policy (defensive-first)
|
|
19
|
+
* ──────────────────────────────────
|
|
20
|
+
* Every Redis operation is wrapped in try/catch. If Redis is unreachable,
|
|
21
|
+
* times out, or returns an unexpected payload:
|
|
22
|
+
* • search() returns null → middleware falls back to the real LLM.
|
|
23
|
+
* • save() is a no-op → the miss is simply not cached; no crash.
|
|
24
|
+
*
|
|
25
|
+
* Dependencies:
|
|
26
|
+
* npm i @upstash/redis uuid
|
|
27
|
+
* npm i -D @types/node @types/uuid
|
|
28
|
+
*/
|
|
29
|
+
import { Redis } from "@upstash/redis";
|
|
30
|
+
import type { VectorStoreAdapter } from "./semantic-cache-middleware";
|
|
31
|
+
export interface RedisVectorAdapterOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Pre-configured @upstash/redis client instance.
|
|
34
|
+
* Pass this if you want to share a client across your application.
|
|
35
|
+
* Mutually exclusive with `redisUrl` + `redisToken`.
|
|
36
|
+
*/
|
|
37
|
+
client?: Redis;
|
|
38
|
+
/**
|
|
39
|
+
* Upstash Redis REST endpoint URL.
|
|
40
|
+
* Required when `client` is not provided.
|
|
41
|
+
* Example: "https://your-db.upstash.io"
|
|
42
|
+
*/
|
|
43
|
+
redisUrl?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Upstash Redis REST API token.
|
|
46
|
+
* Required when `client` is not provided.
|
|
47
|
+
*/
|
|
48
|
+
redisToken?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Optional TTL in seconds. When set, each cache entry will automatically
|
|
51
|
+
* expire after this many seconds via Redis EXPIRE.
|
|
52
|
+
* Omit (or set to 0) to keep entries indefinitely.
|
|
53
|
+
*/
|
|
54
|
+
ttlSeconds?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Maximum number of milliseconds to wait for a Redis command before
|
|
57
|
+
* treating the operation as a failure and falling back to the LLM.
|
|
58
|
+
* @default 2000
|
|
59
|
+
*/
|
|
60
|
+
connectionTimeoutMs?: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A {@link VectorStoreAdapter} backed by Redis with the RediSearch (Redis
|
|
64
|
+
* Stack) vector similarity extension.
|
|
65
|
+
*
|
|
66
|
+
* Compatible with:
|
|
67
|
+
* • Upstash Redis (recommended — serverless, REST-based)
|
|
68
|
+
* • Redis Stack (self-hosted or Redis Cloud with Search enabled)
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const adapter = new RedisVectorAdapter({
|
|
73
|
+
* redisUrl: process.env.UPSTASH_REDIS_REST_URL!,
|
|
74
|
+
* redisToken: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
75
|
+
* ttlSeconds: 60 * 60 * 24, // 24-hour cache expiry
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* const model = wrapLanguageModel({
|
|
79
|
+
* model: openai("gpt-4o"),
|
|
80
|
+
* middleware: SemanticCacheMiddleware({
|
|
81
|
+
* vectorStore: adapter,
|
|
82
|
+
* similarityThreshold: 0.92,
|
|
83
|
+
* }),
|
|
84
|
+
* });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare class RedisVectorAdapter implements VectorStoreAdapter {
|
|
88
|
+
private readonly redis;
|
|
89
|
+
private readonly ttlSeconds;
|
|
90
|
+
private readonly timeoutMs;
|
|
91
|
+
/**
|
|
92
|
+
* Whether the RediSearch HNSW index has been confirmed to exist this
|
|
93
|
+
* process lifetime. Avoids issuing FT.CREATE on every request.
|
|
94
|
+
*/
|
|
95
|
+
private indexReady;
|
|
96
|
+
constructor(options: RedisVectorAdapterOptions);
|
|
97
|
+
/**
|
|
98
|
+
* Lazily creates the RediSearch HNSW index on first use.
|
|
99
|
+
* The operation is idempotent — if the index already exists Redis returns
|
|
100
|
+
* an error that we swallow silently.
|
|
101
|
+
*/
|
|
102
|
+
private ensureIndex;
|
|
103
|
+
/**
|
|
104
|
+
* Performs a KNN-1 cosine similarity search against the HNSW index.
|
|
105
|
+
*
|
|
106
|
+
* Cosine *distance* (returned by Redis) = 1 − cosine *similarity*.
|
|
107
|
+
* Therefore the `threshold` parameter (expressed as similarity, e.g. 0.92)
|
|
108
|
+
* is converted: `maxDistance = 1 − threshold` (e.g. 0.08).
|
|
109
|
+
*
|
|
110
|
+
* If Redis is unavailable or returns an error, `null` is returned and the
|
|
111
|
+
* middleware gracefully falls through to a live LLM call.
|
|
112
|
+
*
|
|
113
|
+
* @param vector - The float32 embedding of the incoming prompt.
|
|
114
|
+
* @param threshold - Minimum cosine similarity score in [0, 1].
|
|
115
|
+
* @returns The cached LLM response string, or `null` on a miss / error.
|
|
116
|
+
*/
|
|
117
|
+
search(vector: number[], threshold: number): Promise<string | null>;
|
|
118
|
+
/**
|
|
119
|
+
* Stores a prompt embedding and its LLM response as a Redis HASH.
|
|
120
|
+
*
|
|
121
|
+
* Failures are logged but never re-thrown — caching is best-effort.
|
|
122
|
+
*
|
|
123
|
+
* @param promptVector - The float32 embedding of the prompt.
|
|
124
|
+
* @param response - The LLM-generated text to cache.
|
|
125
|
+
*/
|
|
126
|
+
save(promptVector: number[], response: string): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Deletes ALL semantic cache entries from Redis.
|
|
129
|
+
* Useful for testing or manual cache invalidation.
|
|
130
|
+
*
|
|
131
|
+
* This scans the keyspace — avoid on large production databases.
|
|
132
|
+
* Prefer TTL-based expiry for automatic eviction in production.
|
|
133
|
+
*/
|
|
134
|
+
flush(): Promise<number>;
|
|
135
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* redis-vector-adapter.ts
|
|
4
|
+
*
|
|
5
|
+
* Concrete implementation of VectorStoreAdapter using Upstash Redis
|
|
6
|
+
* (compatible with any Redis instance that has the RediSearch / Redis Stack
|
|
7
|
+
* module enabled — the same FT.* command surface is used in both cases).
|
|
8
|
+
*
|
|
9
|
+
* Storage layout
|
|
10
|
+
* ─────────────
|
|
11
|
+
* Key prefix : sc:<uuid> (Redis HASH)
|
|
12
|
+
* Fields : response — raw LLM response text
|
|
13
|
+
* vector — Float32 embedding encoded as a binary string
|
|
14
|
+
* createdAt — ISO-8601 timestamp for observability
|
|
15
|
+
*
|
|
16
|
+
* Index : FT.CREATE on the HASH prefix using an HNSW VECTOR field
|
|
17
|
+
* with COSINE distance. Created lazily on first save/search.
|
|
18
|
+
*
|
|
19
|
+
* Failure policy (defensive-first)
|
|
20
|
+
* ──────────────────────────────────
|
|
21
|
+
* Every Redis operation is wrapped in try/catch. If Redis is unreachable,
|
|
22
|
+
* times out, or returns an unexpected payload:
|
|
23
|
+
* • search() returns null → middleware falls back to the real LLM.
|
|
24
|
+
* • save() is a no-op → the miss is simply not cached; no crash.
|
|
25
|
+
*
|
|
26
|
+
* Dependencies:
|
|
27
|
+
* npm i @upstash/redis uuid
|
|
28
|
+
* npm i -D @types/node @types/uuid
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.RedisVectorAdapter = void 0;
|
|
32
|
+
const redis_1 = require("@upstash/redis");
|
|
33
|
+
const uuid_1 = require("uuid");
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Constants
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
/** Prefix for every cache entry stored as a Redis HASH. */
|
|
38
|
+
const KEY_PREFIX = "sc:";
|
|
39
|
+
/** Name of the RediSearch index created over the cache entries. */
|
|
40
|
+
const INDEX_NAME = "semantic_cache_idx";
|
|
41
|
+
/**
|
|
42
|
+
* Dimensionality of the all-MiniLM-L6-v2 embedding space.
|
|
43
|
+
* Must match the model used in the middleware (384 for MiniLM-L6).
|
|
44
|
+
*/
|
|
45
|
+
const VECTOR_DIM = 384;
|
|
46
|
+
/**
|
|
47
|
+
* Number of HNSW graph neighbours to explore at query time.
|
|
48
|
+
* Higher = more accurate recall, higher latency. 32 is a safe default.
|
|
49
|
+
*/
|
|
50
|
+
const HNSW_EF_RUNTIME = 32;
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Helpers — raw command dispatch
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/**
|
|
55
|
+
* Dispatches a raw Redis command through the @upstash/redis client.
|
|
56
|
+
*
|
|
57
|
+
* The @upstash/redis SDK does not expose a public `sendCommand` /
|
|
58
|
+
* `executeCommand` method for RediSearch (FT.*) or other low-level commands.
|
|
59
|
+
* Internally every client forwards a command array via its private request
|
|
60
|
+
* pipeline. We funnel ALL raw commands through this single helper so the
|
|
61
|
+
* cast to the internal shape is isolated to one place rather than scattered
|
|
62
|
+
* across the class.
|
|
63
|
+
*
|
|
64
|
+
* @param redis - The @upstash/redis client instance.
|
|
65
|
+
* @param args - The command name followed by its arguments, as a flat array.
|
|
66
|
+
* @returns The decoded command result, typed by the caller via `T`.
|
|
67
|
+
*/
|
|
68
|
+
async function rawCommand(redis, args) {
|
|
69
|
+
// The internal `request` method accepts `{ command: [...] }` and returns
|
|
70
|
+
// `{ result }`. It is not part of the public type surface, so we cast here
|
|
71
|
+
// deliberately and keep the cast confined to this helper.
|
|
72
|
+
const client = redis;
|
|
73
|
+
const { result } = await client.request({ command: args });
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Helpers — vector encoding / decoding
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Encodes a JS number[] into a Buffer of raw Float32 bytes.
|
|
81
|
+
* Redis vector fields require the embedding as a binary blob.
|
|
82
|
+
*/
|
|
83
|
+
function encodeVector(vector) {
|
|
84
|
+
const float32 = new Float32Array(vector);
|
|
85
|
+
return Buffer.from(float32.buffer);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Decodes a Base64 string back into a Buffer containing raw Float32 bytes.
|
|
89
|
+
* Used when converting the stored value back for a KNN query blob.
|
|
90
|
+
*/
|
|
91
|
+
function base64ToBuffer(b64) {
|
|
92
|
+
return Buffer.from(b64, "base64");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Wraps a promise with a timeout.
|
|
96
|
+
* Rejects with a timeout error if the promise does not settle within
|
|
97
|
+
* `ms` milliseconds.
|
|
98
|
+
*/
|
|
99
|
+
function withTimeout(promise, ms) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const timer = setTimeout(() => reject(new Error(`Redis operation timed out after ${ms}ms`)), ms);
|
|
102
|
+
promise.then((value) => { clearTimeout(timer); resolve(value); }, (err) => { clearTimeout(timer); reject(err); });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A {@link VectorStoreAdapter} backed by Redis with the RediSearch (Redis
|
|
107
|
+
* Stack) vector similarity extension.
|
|
108
|
+
*
|
|
109
|
+
* Compatible with:
|
|
110
|
+
* • Upstash Redis (recommended — serverless, REST-based)
|
|
111
|
+
* • Redis Stack (self-hosted or Redis Cloud with Search enabled)
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const adapter = new RedisVectorAdapter({
|
|
116
|
+
* redisUrl: process.env.UPSTASH_REDIS_REST_URL!,
|
|
117
|
+
* redisToken: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
118
|
+
* ttlSeconds: 60 * 60 * 24, // 24-hour cache expiry
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* const model = wrapLanguageModel({
|
|
122
|
+
* model: openai("gpt-4o"),
|
|
123
|
+
* middleware: SemanticCacheMiddleware({
|
|
124
|
+
* vectorStore: adapter,
|
|
125
|
+
* similarityThreshold: 0.92,
|
|
126
|
+
* }),
|
|
127
|
+
* });
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
class RedisVectorAdapter {
|
|
131
|
+
redis;
|
|
132
|
+
ttlSeconds;
|
|
133
|
+
timeoutMs;
|
|
134
|
+
/**
|
|
135
|
+
* Whether the RediSearch HNSW index has been confirmed to exist this
|
|
136
|
+
* process lifetime. Avoids issuing FT.CREATE on every request.
|
|
137
|
+
*/
|
|
138
|
+
indexReady = false;
|
|
139
|
+
constructor(options) {
|
|
140
|
+
const { client, redisUrl, redisToken, ttlSeconds = 0, connectionTimeoutMs = 2000 } = options;
|
|
141
|
+
if (client) {
|
|
142
|
+
this.redis = client;
|
|
143
|
+
}
|
|
144
|
+
else if (redisUrl && redisToken) {
|
|
145
|
+
this.redis = new redis_1.Redis({
|
|
146
|
+
url: redisUrl,
|
|
147
|
+
token: redisToken,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
throw new Error("RedisVectorAdapter: supply either `client` or both `redisUrl` and `redisToken`.");
|
|
152
|
+
}
|
|
153
|
+
this.ttlSeconds = ttlSeconds;
|
|
154
|
+
this.timeoutMs = connectionTimeoutMs;
|
|
155
|
+
}
|
|
156
|
+
// =========================================================================
|
|
157
|
+
// Index bootstrap
|
|
158
|
+
// =========================================================================
|
|
159
|
+
/**
|
|
160
|
+
* Lazily creates the RediSearch HNSW index on first use.
|
|
161
|
+
* The operation is idempotent — if the index already exists Redis returns
|
|
162
|
+
* an error that we swallow silently.
|
|
163
|
+
*/
|
|
164
|
+
async ensureIndex() {
|
|
165
|
+
if (this.indexReady)
|
|
166
|
+
return;
|
|
167
|
+
try {
|
|
168
|
+
await withTimeout(rawCommand(this.redis, [
|
|
169
|
+
"FT.CREATE", INDEX_NAME,
|
|
170
|
+
"ON", "HASH",
|
|
171
|
+
"PREFIX", "1", KEY_PREFIX,
|
|
172
|
+
"SCHEMA",
|
|
173
|
+
// ── Full-text field (for human inspection / future hybrid search) ──
|
|
174
|
+
"response", "TEXT",
|
|
175
|
+
"createdAt", "TEXT",
|
|
176
|
+
// ── Vector field — HNSW with Cosine distance ──────────────────────
|
|
177
|
+
"vector", "VECTOR", "HNSW",
|
|
178
|
+
"6", // number of attribute/value pairs below
|
|
179
|
+
"TYPE", "FLOAT32",
|
|
180
|
+
"DIM", String(VECTOR_DIM),
|
|
181
|
+
"DISTANCE_METRIC", "COSINE",
|
|
182
|
+
]), this.timeoutMs);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
// Redis returns "Index already exists" as an error string; swallow it.
|
|
186
|
+
// Any other error is also swallowed here — if the index genuinely fails
|
|
187
|
+
// to create, FT.SEARCH will error too and be caught in search().
|
|
188
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
189
|
+
if (!message.includes("already exists") && !message.includes("Index already exists")) {
|
|
190
|
+
console.warn("[RedisVectorAdapter] FT.CREATE warning (non-fatal):", message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Mark as ready regardless — subsequent failures will be caught per-call.
|
|
194
|
+
this.indexReady = true;
|
|
195
|
+
}
|
|
196
|
+
// =========================================================================
|
|
197
|
+
// VectorStoreAdapter — search
|
|
198
|
+
// =========================================================================
|
|
199
|
+
/**
|
|
200
|
+
* Performs a KNN-1 cosine similarity search against the HNSW index.
|
|
201
|
+
*
|
|
202
|
+
* Cosine *distance* (returned by Redis) = 1 − cosine *similarity*.
|
|
203
|
+
* Therefore the `threshold` parameter (expressed as similarity, e.g. 0.92)
|
|
204
|
+
* is converted: `maxDistance = 1 − threshold` (e.g. 0.08).
|
|
205
|
+
*
|
|
206
|
+
* If Redis is unavailable or returns an error, `null` is returned and the
|
|
207
|
+
* middleware gracefully falls through to a live LLM call.
|
|
208
|
+
*
|
|
209
|
+
* @param vector - The float32 embedding of the incoming prompt.
|
|
210
|
+
* @param threshold - Minimum cosine similarity score in [0, 1].
|
|
211
|
+
* @returns The cached LLM response string, or `null` on a miss / error.
|
|
212
|
+
*/
|
|
213
|
+
async search(vector, threshold) {
|
|
214
|
+
try {
|
|
215
|
+
await this.ensureIndex();
|
|
216
|
+
// Convert similarity threshold → cosine distance upper bound.
|
|
217
|
+
const maxDistance = 1 - threshold;
|
|
218
|
+
// Encode the query vector as a binary buffer for the KNN parameter.
|
|
219
|
+
const queryBlob = encodeVector(vector);
|
|
220
|
+
// ── FT.SEARCH KNN query ──────────────────────────────────────────────
|
|
221
|
+
const rawResult = await withTimeout(rawCommand(this.redis, [
|
|
222
|
+
"FT.SEARCH", INDEX_NAME,
|
|
223
|
+
`*=>[KNN 1 @vector $vec AS __score]`,
|
|
224
|
+
"PARAMS", "2",
|
|
225
|
+
"vec", queryBlob.toString("binary"), // binary string for the REST layer
|
|
226
|
+
"RETURN", "2", "__score", "response",
|
|
227
|
+
"SORTBY", "__score", "ASC",
|
|
228
|
+
"DIALECT", "2",
|
|
229
|
+
]), this.timeoutMs);
|
|
230
|
+
// rawResult layout (RESP2): [ totalCount, key1, [field, value, ...], ... ]
|
|
231
|
+
const totalCount = rawResult[0];
|
|
232
|
+
if (totalCount === 0) {
|
|
233
|
+
return null; // no entries in the index yet
|
|
234
|
+
}
|
|
235
|
+
// The first result's field-value array is at index 2.
|
|
236
|
+
const fields = rawResult[2];
|
|
237
|
+
// Parse the flat field-value array into a map.
|
|
238
|
+
const fieldMap = {};
|
|
239
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
240
|
+
fieldMap[fields[i]] = fields[i + 1];
|
|
241
|
+
}
|
|
242
|
+
const score = parseFloat(fieldMap["__score"] ?? "1");
|
|
243
|
+
const response = fieldMap["response"] ?? null;
|
|
244
|
+
// `score` is cosine distance; reject if it exceeds the max allowed distance.
|
|
245
|
+
if (score > maxDistance || response === null) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
return response;
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
// ── Defensive fallback ────────────────────────────────────────────────
|
|
252
|
+
console.error("[RedisVectorAdapter] search() failed (falling back to LLM):", err instanceof Error ? err.message : err);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// =========================================================================
|
|
257
|
+
// VectorStoreAdapter — save
|
|
258
|
+
// =========================================================================
|
|
259
|
+
/**
|
|
260
|
+
* Stores a prompt embedding and its LLM response as a Redis HASH.
|
|
261
|
+
*
|
|
262
|
+
* Failures are logged but never re-thrown — caching is best-effort.
|
|
263
|
+
*
|
|
264
|
+
* @param promptVector - The float32 embedding of the prompt.
|
|
265
|
+
* @param response - The LLM-generated text to cache.
|
|
266
|
+
*/
|
|
267
|
+
async save(promptVector, response) {
|
|
268
|
+
try {
|
|
269
|
+
await this.ensureIndex();
|
|
270
|
+
const key = `${KEY_PREFIX}${(0, uuid_1.v4)()}`;
|
|
271
|
+
const vectorBuf = encodeVector(promptVector);
|
|
272
|
+
// Store the vector as a binary string field so RediSearch can index it.
|
|
273
|
+
await withTimeout(rawCommand(this.redis, [
|
|
274
|
+
"HSET", key,
|
|
275
|
+
"vector", vectorBuf.toString("binary"),
|
|
276
|
+
"response", response,
|
|
277
|
+
"createdAt", new Date().toISOString(),
|
|
278
|
+
]), this.timeoutMs);
|
|
279
|
+
// Apply optional TTL — Redis will remove the key after `ttlSeconds`.
|
|
280
|
+
if (this.ttlSeconds > 0) {
|
|
281
|
+
await withTimeout(rawCommand(this.redis, ["EXPIRE", key, String(this.ttlSeconds)]), this.timeoutMs);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
// ── Defensive fallback ────────────────────────────────────────────────
|
|
286
|
+
// A failed cache write must never degrade the primary LLM response
|
|
287
|
+
// that was already returned to the user.
|
|
288
|
+
console.error("[RedisVectorAdapter] save() failed (cache write skipped):", err instanceof Error ? err.message : err);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// =========================================================================
|
|
292
|
+
// Utility — explicit teardown (optional, for graceful shutdown)
|
|
293
|
+
// =========================================================================
|
|
294
|
+
/**
|
|
295
|
+
* Deletes ALL semantic cache entries from Redis.
|
|
296
|
+
* Useful for testing or manual cache invalidation.
|
|
297
|
+
*
|
|
298
|
+
* This scans the keyspace — avoid on large production databases.
|
|
299
|
+
* Prefer TTL-based expiry for automatic eviction in production.
|
|
300
|
+
*/
|
|
301
|
+
async flush() {
|
|
302
|
+
try {
|
|
303
|
+
let cursor = "0";
|
|
304
|
+
let deletedCount = 0;
|
|
305
|
+
do {
|
|
306
|
+
const [nextCursor, keys] = await withTimeout(rawCommand(this.redis, [
|
|
307
|
+
"SCAN", cursor, "MATCH", `${KEY_PREFIX}*`, "COUNT", "100"
|
|
308
|
+
]), this.timeoutMs);
|
|
309
|
+
cursor = nextCursor;
|
|
310
|
+
if (keys.length > 0) {
|
|
311
|
+
await withTimeout(rawCommand(this.redis, ["DEL", ...keys]), this.timeoutMs);
|
|
312
|
+
deletedCount += keys.length;
|
|
313
|
+
}
|
|
314
|
+
} while (cursor !== "0");
|
|
315
|
+
return deletedCount;
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
console.error("[RedisVectorAdapter] flush() failed:", err instanceof Error ? err.message : err);
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
exports.RedisVectorAdapter = RedisVectorAdapter;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-semantic-cache",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "**Cut your OpenAI / Anthropic bill. Slash latency to ~50 ms. Compute embeddings for free — locally, on every request.**",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"dist/index.js",
|
|
12
12
|
"dist/index.d.ts",
|
|
13
13
|
"dist/semantic-cache-middleware.js",
|
|
14
|
-
"dist/semantic-cache-middleware.d.ts"
|
|
14
|
+
"dist/semantic-cache-middleware.d.ts",
|
|
15
|
+
"dist/redis-vector-adapter.js",
|
|
16
|
+
"dist/redis-vector-adapter.d.ts",
|
|
17
|
+
"README.md"
|
|
15
18
|
],
|
|
16
19
|
"keywords": [],
|
|
17
20
|
"author": "Prateek Chaturvedi",
|