Redis Integration Skill
Overview
This project uses dual Redis systems for different purposes:
- Upstash Redis: Rate limiting, short-term memory (STM), general caching
- Upstash Vector: Episodic memory, semantic search with embeddings
Architecture Decision: Dual-Path Vector Search
System Selection
Project Embeddings (1,536 dimensions)
├─> Redis FT.SEARCH (Redis Stack)
│ └─> Index: project_embeddings_idx
│ └─> Storage: HASH with VECTOR field
│ └─> Use Case: Project semantic search
│
Episodic Memory (OpenAI embeddings)
├─> Upstash Vector (Native vector DB)
└─> No index name needed
└─> Storage: Native vector format
└─> Use Case: Conversation history search
```typescript
**Why Two Systems?**
- **Redis FT.SEARCH**: Already used for rate-limiting, good for structured data with metadata
- **Upstash Vector**: Purpose-built for vector operations, simpler API for pure vector search
- **No migration needed**: Each system serves its purpose optimally
## Core Files Reference
```typescript
src/lib/redis/
├── client.ts # Redis client with FT.SEARCH extensions
├── vector-client.ts # Upstash Vector client singleton
├── vector-search.ts # Dual-path KNN search routing
├── embeddings.ts # Project embedding generation & search
└── contact-storage.ts # Contact form data storage
src/lib/
├── rate-limit.ts # Rate limiting configurations
└── memory/
├── redis-memory.ts # Memory manager (STM/LTM)
├── semantic-memory.ts # Facts/preferences storage
└── types.ts # Memory type definitions
```typescript
## 1. Redis Client with FT.SEARCH Extensions
### Location
`src/lib/redis/client.ts`
### Pattern: Extended Redis Client
**Problem:** Upstash Redis SDK doesn't natively support Redis Stack commands (FT.SEARCH, FT.CREATE)
**Solution:** Extend base client with custom command execution
```typescript
import { getRedisClient } from "@/lib/redis/client";
const redis = getRedisClient();
// ✅ Extended client supports Redis Stack commands
await redis.ft.create(indexName, schema, options);
await redis.ft.search(indexName, query, options);
await redis.call("FT.INFO", indexName);
```typescript
### Key Features
**1. Singleton Pattern**
```typescript
let cachedClient: RedisStackClient | null = null;
export function getRedisClient(): RedisStackClient {
if (cachedClient) {
return cachedClient;
}
// Create and cache client
cachedClient = extendWithStackCommands(baseClient, url, token);
return cachedClient;
}
```typescript
**2. FT.SEARCH Support**
```typescript
// Create vector index
await redis.ft.create(
"project_embeddings_idx",
{
"$.slug": { type: "TEXT", AS: "slug" },
"$.embedding": {
type: "VECTOR",
AS: "embedding",
},
},
{
ON: "HASH",
PREFIX: "project:embedding:",
}
);
// Search using KNN
const results = await redis.ft.search(
"project_embeddings_idx",
"*=>[KNN 5 @embedding $BLOB AS vector_score]",
{
PARAMS: ["BLOB", embeddingBuffer],
RETURN: ["slug", "title", "vector_score"],
LIMIT: { from: 0, size: 5 },
}
);
```typescript
**3. Environment Variables Required**
```env
UPSTASH_REDIS_REST_URL=https://your-instance.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token-here
```typescript
## 2. Upstash Vector Client
### Location
`src/lib/redis/vector-client.ts`
### Pattern: Separate Vector Database Client
**Why Separate?**
- Purpose-built for vector operations
- Simpler API for embeddings
- Optimized for semantic search
- No schema management needed
```typescript
import { getVectorClient } from "@/lib/redis/vector-client";
const vectorClient = getVectorClient();
// ✅ Simple vector operations
await vectorClient.upsert({
id: "message-123",
vector: embedding,
metadata: { threadId, role, content },
});
const results = await vectorClient.query({
vector: queryEmbedding,
topK: 5,
includeMetadata: true,
});
```typescript
### Environment Variables Required
```env
UPSTASH_VECTOR_REST_URL=https://your-vector-instance.upstash.io
UPSTASH_VECTOR_REST_TOKEN=your-vector-token-here
```typescript
## 3. Dual-Path Vector Search Router
### Location
`src/lib/redis/vector-search.ts`
### Pattern: Intelligent Search Routing
### Routes searches to appropriate backend
```typescript
import { knnSearch } from "@/lib/redis/vector-search";
// ✅ Project search → Routes to Redis FT.SEARCH
const projectResults = await knnSearch(
"project_embeddings_idx", // Index name triggers Redis
queryEmbedding,
5,
["slug", "title", "description"]
);
// ✅ Memory search → Routes to Upstash Vector
const memoryResults = await knnSearch(
undefined, // No index = Upstash Vector
queryEmbedding,
3,
["threadId", "role", "content"]
);
```typescript
### Routing Logic
```typescript
export async function knnSearch(
index: string | undefined,
vector: number[],
limit: number,
returnFields: string[] = []
): Promise<VectorSearchResult[]> {
// Route to Redis FT.SEARCH for project embeddings
if (index === "project_embeddings_idx") {
return knnSearchRedis(index, vector, limit, returnFields);
}
// Route to Upstash Vector for episodic memory
return knnSearchVector(vector, limit, returnFields);
}
```typescript
### Benefits
- ✅ Single API for all vector searches
- ✅ Automatic backend selection
- ✅ Consistent result format
- ✅ Easy to add new indices
## 4. Rate Limiting Configuration
### Location
`src/lib/rate-limit.ts`
### Pattern: Sliding Window Rate Limits
### Multiple rate limiters for different endpoints
```typescript
import {
chatRateLimit,
toolsRateLimit,
contactFormRateLimit,
textEditorRateLimit,
} from "@/lib/rate-limit";
// ✅ In API route
export async function POST(request: Request) {
const ip = request.headers.get("x-forwarded-for") || "anonymous";
if (chatRateLimit) {
const result = await chatRateLimit.limit(ip);
if (!result.success) {
return new Response("Rate limit exceeded", { status: 429 });
}
}
// Process request...
}
```typescript
### Rate Limit Configurations
| Limiter | Rate | Window | Prefix | Use Case |
| ---------------------------- | ------- | -------- | ------------------------------ | ---------------- |
| `chatRateLimit` | 30 req | 1 minute | `ratelimit:chat` | OpenAI API calls |
| `toolsRateLimit` | 60 req | 1 minute | `ratelimit:tools` | Tool endpoints |
| `apiRateLimit` | 100 req | 1 minute | `ratelimit:api` | Generic APIs |
| `contactCollectionRateLimit` | 5 req | 24 hours | `ratelimit:contact-collection` | Contact sharing |
| `textEditorRateLimit` | 10 req | 1 minute | `ratelimit:text-editor` | AI text editing |
| `contactFormRateLimit` | 5 req | 24 hours | `ratelimit:contact-form` | Form submissions |
### Development Mode Fallback
```typescript
// ✅ Gracefully degrades in development
const redis =
process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN
? new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
})
: null;
export const chatRateLimit = redis
? new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(30, "1 m"),
})
: null; // ← No rate-limiting in dev without credentials
```typescript
### Usage Pattern
```typescript
// Check rate limit
if (chatRateLimit) {
const { success, reset } = await chatRateLimit.limit(identifier);
if (!success) {
return Response.json(
{
error: "Rate limit exceeded",
resetAt: new Date(reset).toISOString(),
},
{ status: 429 }
);
}
}
```typescript
## 5. Project Embeddings & Semantic Search
### Location
`src/lib/redis/embeddings.ts`
### Pattern: Generate + Store + Search
### Complete workflow for project semantic search
```typescript
import { generateProjectEmbedding, searchProjects } from "@/lib/redis/embeddings";
// ✅ Generate embedding for a project
const embedding = await generateProjectEmbedding(project);
// ✅ Search for similar projects
const results = await searchProjects(queryEmbedding, topK = 5);
```typescript
### Implementation Details
**1. Generate Embeddings**
```typescript
export async function generateProjectEmbedding(project: Project): Promise<number[]> {
const text = `${project.title} ${project.description} ${project.tags.join(" ")}`;
const embedding = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
dimensions: 1536,
});
return embedding.data[0].embedding;
}
```typescript
**2. Store in Redis with Vector Index**
```typescript
export async function storeProjectEmbedding(project: Project): Promise<void> {
const redis = getRedisClient();
const embedding = await generateProjectEmbedding(project);
// Store as HASH with VECTOR field for FT.SEARCH
await redis.hset(`project:embedding:${project.slug}`, {
slug: project.slug,
title: project.title,
embedding: Buffer.from(new Float32Array(embedding)),
});
}
```typescript
**3. Semantic Search**
```typescript
export async function searchProjects(
queryEmbedding: number[],
topK: number = 5
): Promise<Project[]> {
const redis = getRedisClient();
// KNN search via FT.SEARCH
const results = await redis.ft.search(
"project_embeddings_idx",
"*=>[KNN 5 @embedding $BLOB AS vector_score]",
{
PARAMS: ["BLOB", Buffer.from(new Float32Array(queryEmbedding))],
RETURN: ["slug", "title", "vector_score"],
LIMIT: { from: 0, size: topK },
}
);
return results.documents.map(doc => ({
slug: doc.slug,
similarity: parseFloat(doc.vector_score),
}));
}
```typescript
## Summary & Decision Matrix
| System | Use Case | Backend | Pros | Cons |
|--------|----------|---------|------|------|
| **Redis FT.SEARCH** | Project embeddings | Redis Stack | Structured + vector search, metadata support | More complex setup |
| **Upstash Vector** | Episodic memory | Vector DB | Purpose-built, simple API | Pure vectors only |
| **Ratelimit** | API protection | Redis | Sliding window, flexible | Requires redis |
**Decision: Use both systems** - Each solves a specific problem optimally without overcomplicating the other.
1---2name: redis-integration3description: Redis and Upstash Vector integration patterns for rate-limiting, vector search, embeddings, and memory systems. Use when implementing caching, rate-limiting, or semantic search features.4---5
6# Redis Integration Skill
7
8## Overview
9
10This project uses **dual Redis systems** for different purposes:
11
12- **Upstash Redis**: Rate limiting, short-term memory (STM), general caching
13- **Upstash Vector**: Episodic memory, semantic search with embeddings
14
15## Architecture Decision: Dual-Path Vector Search
16
17### System Selection
18
19```typescript
20Project Embeddings (1,536 dimensions)
21├─> Redis FT.SEARCH (Redis Stack)
22│ └─> Index: project_embeddings_idx
23│ └─> Storage: HASH with VECTOR field
24│ └─> Use Case: Project semantic search
25│
26Episodic Memory (OpenAI embeddings)
27├─> Upstash Vector (Native vector DB)
28 └─> No index name needed
29 └─> Storage: Native vector format
30 └─> Use Case: Conversation history search
31```typescript
32
33**Why Two Systems?**
34
35- **Redis FT.SEARCH**: Already used for rate-limiting, good for structured data with metadata
36- **Upstash Vector**: Purpose-built for vector operations, simpler API for pure vector search
37- **No migration needed**: Each system serves its purpose optimally
38
39## Core Files Reference
40
41```typescript
42src/lib/redis/
43├── client.ts # Redis client with FT.SEARCH extensions
44├── vector-client.ts # Upstash Vector client singleton
45├── vector-search.ts # Dual-path KNN search routing
46├── embeddings.ts # Project embedding generation & search
47└── contact-storage.ts # Contact form data storage
48
49src/lib/
50├── rate-limit.ts # Rate limiting configurations
51└── memory/
52 ├── redis-memory.ts # Memory manager (STM/LTM)
53 ├── semantic-memory.ts # Facts/preferences storage
54 └── types.ts # Memory type definitions
55```typescript
56
57## 1. Redis Client with FT.SEARCH Extensions
58
59### Location
60
61`src/lib/redis/client.ts`
62
63### Pattern: Extended Redis Client
64
65**Problem:** Upstash Redis SDK doesn't natively support Redis Stack commands (FT.SEARCH, FT.CREATE)
66
67**Solution:** Extend base client with custom command execution
68
69```typescript
70import { getRedisClient } from "@/lib/redis/client";
71
72const redis = getRedisClient();
73
74// ✅ Extended client supports Redis Stack commands
75await redis.ft.create(indexName, schema, options);
76await redis.ft.search(indexName, query, options);
77await redis.call("FT.INFO", indexName);
78```typescript
79
80### Key Features
81
82**1. Singleton Pattern**
83
84```typescript
85let cachedClient: RedisStackClient | null = null;
86
87export function getRedisClient(): RedisStackClient {
88 if (cachedClient) {
89 return cachedClient;
90 }
91 // Create and cache client
92 cachedClient = extendWithStackCommands(baseClient, url, token);
93 return cachedClient;
94}
95```typescript
96
97**2. FT.SEARCH Support**
98
99```typescript
100// Create vector index
101await redis.ft.create(
102 "project_embeddings_idx",
103 {
104 "$.slug": { type: "TEXT", AS: "slug" },
105 "$.embedding": {
106 type: "VECTOR",
107 AS: "embedding",
108 },
109 },
110 {
111 ON: "HASH",
112 PREFIX: "project:embedding:",
113 }
114);
115
116// Search using KNN
117const results = await redis.ft.search(
118 "project_embeddings_idx",
119 "*=>[KNN 5 @embedding $BLOB AS vector_score]",
120 {
121 PARAMS: ["BLOB", embeddingBuffer],
122 RETURN: ["slug", "title", "vector_score"],
123 LIMIT: { from: 0, size: 5 },
124 }
125);
126```typescript
127
128**3. Environment Variables Required**
129
130```env
131UPSTASH_REDIS_REST_URL=https://your-instance.upstash.io
132UPSTASH_REDIS_REST_TOKEN=your-token-here
133```typescript
134
135## 2. Upstash Vector Client
136
137### Location
138
139`src/lib/redis/vector-client.ts`
140
141### Pattern: Separate Vector Database Client
142
143**Why Separate?**
144
145- Purpose-built for vector operations
146- Simpler API for embeddings
147- Optimized for semantic search
148- No schema management needed
149
150```typescript
151import { getVectorClient } from "@/lib/redis/vector-client";
152
153const vectorClient = getVectorClient();
154
155// ✅ Simple vector operations
156await vectorClient.upsert({
157 id: "message-123",
158 vector: embedding,
159 metadata: { threadId, role, content },
160});
161
162const results = await vectorClient.query({
163 vector: queryEmbedding,
164 topK: 5,
165 includeMetadata: true,
166});
167```typescript
168
169### Environment Variables Required
170
171```env
172UPSTASH_VECTOR_REST_URL=https://your-vector-instance.upstash.io
173UPSTASH_VECTOR_REST_TOKEN=your-vector-token-here
174```typescript
175
176## 3. Dual-Path Vector Search Router
177
178### Location
179
180`src/lib/redis/vector-search.ts`
181
182### Pattern: Intelligent Search Routing
183
184### Routes searches to appropriate backend
185
186```typescript
187import { knnSearch } from "@/lib/redis/vector-search";
188
189// ✅ Project search → Routes to Redis FT.SEARCH
190const projectResults = await knnSearch(
191 "project_embeddings_idx", // Index name triggers Redis
192 queryEmbedding,
193 5,
194 ["slug", "title", "description"]
195);
196
197// ✅ Memory search → Routes to Upstash Vector
198const memoryResults = await knnSearch(
199 undefined, // No index = Upstash Vector
200 queryEmbedding,
201 3,
202 ["threadId", "role", "content"]
203);
204```typescript
205
206### Routing Logic
207
208```typescript
209export async function knnSearch(
210 index: string | undefined,
211 vector: number[],
212 limit: number,
213 returnFields: string[] = []
214): Promise<VectorSearchResult[]> {
215 // Route to Redis FT.SEARCH for project embeddings
216 if (index === "project_embeddings_idx") {
217 return knnSearchRedis(index, vector, limit, returnFields);
218 }
219
220 // Route to Upstash Vector for episodic memory
221 return knnSearchVector(vector, limit, returnFields);
222}
223```typescript
224
225### Benefits
226
227- ✅ Single API for all vector searches
228- ✅ Automatic backend selection
229- ✅ Consistent result format
230- ✅ Easy to add new indices
231
232## 4. Rate Limiting Configuration
233
234### Location
235
236`src/lib/rate-limit.ts`
237
238### Pattern: Sliding Window Rate Limits
239
240### Multiple rate limiters for different endpoints
241
242```typescript
243import {
244 chatRateLimit,
245 toolsRateLimit,
246 contactFormRateLimit,
247 textEditorRateLimit,
248} from "@/lib/rate-limit";
249
250// ✅ In API route
251export async function POST(request: Request) {
252 const ip = request.headers.get("x-forwarded-for") || "anonymous";
253
254 if (chatRateLimit) {
255 const result = await chatRateLimit.limit(ip);
256 if (!result.success) {
257 return new Response("Rate limit exceeded", { status: 429 });
258 }
259 }
260
261 // Process request...
262}
263```typescript
264
265### Rate Limit Configurations
266
267| Limiter | Rate | Window | Prefix | Use Case |
268| ---------------------------- | ------- | -------- | ------------------------------ | ---------------- |
269| `chatRateLimit` | 30 req | 1 minute | `ratelimit:chat` | OpenAI API calls |
270| `toolsRateLimit` | 60 req | 1 minute | `ratelimit:tools` | Tool endpoints |
271| `apiRateLimit` | 100 req | 1 minute | `ratelimit:api` | Generic APIs |
272| `contactCollectionRateLimit` | 5 req | 24 hours | `ratelimit:contact-collection` | Contact sharing |
273| `textEditorRateLimit` | 10 req | 1 minute | `ratelimit:text-editor` | AI text editing |
274| `contactFormRateLimit` | 5 req | 24 hours | `ratelimit:contact-form` | Form submissions |
275
276### Development Mode Fallback
277
278```typescript
279// ✅ Gracefully degrades in development
280const redis =
281 process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN
282 ? new Redis({
283 url: process.env.UPSTASH_REDIS_REST_URL,
284 token: process.env.UPSTASH_REDIS_REST_TOKEN,
285 })
286 : null;
287
288export const chatRateLimit = redis
289 ? new Ratelimit({
290 redis,
291 limiter: Ratelimit.slidingWindow(30, "1 m"),
292 })
293 : null; // ← No rate-limiting in dev without credentials
294```typescript
295
296### Usage Pattern
297
298```typescript
299// Check rate limit
300if (chatRateLimit) {
301 const { success, reset } = await chatRateLimit.limit(identifier);
302
303 if (!success) {
304 return Response.json(
305 {
306 error: "Rate limit exceeded",
307 resetAt: new Date(reset).toISOString(),
308 },
309 { status: 429 }
310 );
311 }
312}
313```typescript
314
315## 5. Project Embeddings & Semantic Search
316
317### Location
318
319`src/lib/redis/embeddings.ts`
320
321### Pattern: Generate + Store + Search
322
323### Complete workflow for project semantic search
324
325```typescript
326import { generateProjectEmbedding, searchProjects } from "@/lib/redis/embeddings";
327
328// ✅ Generate embedding for a project
329const embedding = await generateProjectEmbedding(project);
330
331// ✅ Search for similar projects
332const results = await searchProjects(queryEmbedding, topK = 5);
333```typescript
334
335### Implementation Details
336
337**1. Generate Embeddings**
338
339```typescript
340export async function generateProjectEmbedding(project: Project): Promise<number[]> {
341 const text = `${project.title} ${project.description} ${project.tags.join(" ")}`;
342
343 const embedding = await openai.embeddings.create({
344 model: "text-embedding-3-small",
345 input: text,
346 dimensions: 1536,
347 });
348
349 return embedding.data[0].embedding;
350}
351```typescript
352
353**2. Store in Redis with Vector Index**
354
355```typescript
356export async function storeProjectEmbedding(project: Project): Promise<void> {
357 const redis = getRedisClient();
358 const embedding = await generateProjectEmbedding(project);
359
360 // Store as HASH with VECTOR field for FT.SEARCH
361 await redis.hset(`project:embedding:${project.slug}`, {
362 slug: project.slug,
363 title: project.title,
364 embedding: Buffer.from(new Float32Array(embedding)),
365 });
366}
367```typescript
368
369**3. Semantic Search**
370
371```typescript
372export async function searchProjects(
373 queryEmbedding: number[],
374 topK: number = 5
375): Promise<Project[]> {
376 const redis = getRedisClient();
377
378 // KNN search via FT.SEARCH
379 const results = await redis.ft.search(
380 "project_embeddings_idx",
381 "*=>[KNN 5 @embedding $BLOB AS vector_score]",
382 {
383 PARAMS: ["BLOB", Buffer.from(new Float32Array(queryEmbedding))],
384 RETURN: ["slug", "title", "vector_score"],
385 LIMIT: { from: 0, size: topK },
386 }
387 );
388
389 return results.documents.map(doc => ({
390 slug: doc.slug,
391 similarity: parseFloat(doc.vector_score),
392 }));
393}
394```typescript
395
396## Summary & Decision Matrix
397
398| System | Use Case | Backend | Pros | Cons |
399|--------|----------|---------|------|------|
400| **Redis FT.SEARCH** | Project embeddings | Redis Stack | Structured + vector search, metadata support | More complex setup |
401| **Upstash Vector** | Episodic memory | Vector DB | Purpose-built, simple API | Pure vectors only |
402| **Ratelimit** | API protection | Redis | Sliding window, flexible | Requires redis |
403
404**Decision: Use both systems** - Each solves a specific problem optimally without overcomplicating the other.