RAG领域的ElastiCache——如何存储与查询嵌入向量
用于 RAG 的 ElastiCache:从 Node.js 和 TypeScript 中在 Redis 中存储和查询嵌入向量,搭建集群,创建向量索引并执行相似度搜索。
来源: ElastiCache for RAG: How to Store and Query Embeddings in Redis with Node.js and TypeScript
可将此内容视为《ElastiCache for RAG:如何使用 Node.js 和 TypeScript 在 Redis 中存储和查询嵌入向量》一文中理念的面向操作员的重构版本:清晰的阶段划分、有序的代码模块以及便于交接时参考的恢复说明。将概览视为可度量的框架使用效果最佳,在扩大范围之前,先记录一份标准案例、一个故障场景以及回滚说明。应将此阶段视为输入与经过验证的输出之间的契约,为相关成果命名、明确成功标准,并杜绝默许的半完成状态。
为何 Redis 可以作为向量存储
在修改代码之前,为“为何 Redis 可作为向量存储”这一主题明确输入数据、各步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。除了功能结果外,还需记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。必须引用实际支撑答案的段落;没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。
搭建 ElastiCache Serverless 集群
在搭建 ElastiCache Serverless 集群时,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏的状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,这样操作人员无需查看整个系统结构即可进行审核。 需注明实际作为答案依据的段落。如果没有引用,操作人员就无法区分是虚假信息还是索引缺失导致的错误。
1. 创建集群
对于第1点:在修改代码之前,需先创建集群、明确输入参数、指定该步骤的负责人以及设定退出条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。需同时记录正常流程与故障恢复流程。重试机制、人工审核环节以及错误处理方式都是产品本身的组成部分,而非后续需要补充的内容。必须引用那些真正作为答案依据的段落;没有引用的话,操作人员就无法区分是虚假信息还是索引缺失导致的错误。
aws elasticache create-serverless-cache \
--engine redis \
--cache-name rag-cache \
--security-group-ids sg-0123456789abcdef0 \
--subnet-group-name my-subnet-group \
--description "Redis for RAG embeddings" \
--cluster-mode-enabled # <-- critical for multi‑key Lua scripts
对于第1点:在修改代码之前,需先创建集群、明确输入参数、指定该步骤的负责人以及设定退出条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。应将这一阶段视为输入与经过验证的输出之间的契约,为相关成果命名、明确成功标准,并杜绝默默完成部分任务的情况。
2. 注意潜在问题
在处理“2. 注意潜在问题”时,首先写下契约内容:所需的输入参数、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免从演示环境过渡到共享环境时出现意外费用。 在调整提示词之前,先使用固定的问题集测试召回率。仅仅更换提示词往往无法改善较差的检索效果。
使用satisfies运算符编写类型安全的Lambda代码
在使用 satisfies 运算符处理类型安全的 Lambda 代码时,首先需明确契约内容:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。
应将配置信息置于应用程序代码之外。环境文件、密钥存储以及功能开关应集中存放,这样运算符无需查看整个系统结构即可进行审计。
在调整提示词之前,先在固定的问题集上测试召回率。仅仅更换提示词很难改善较差的检索效果。
1. 安装所需包
在处理1.安装所需包时,首先写下相关规范:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误消息处理都是产品本身的组成部分,而非后续需要补充的功能。 在调整提示词之前,先使用固定的问题集来衡量检索的准确率。仅仅更换提示词很难解决检索效果不佳的问题。
npm install @aws-sdk/client-elasticache @aws-sdk/client-lambda @aws-sdk/client-redis @aws-sdk/lib-dynamodb
npm install redis@4 ioredis@5 # ioredis supports cluster mode and Lua
npm install openai # for embedding API (optional)
2. Lambda框架结构
在处理“2. Lambda框架”时,首先需明确接口规范:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 建议采用小型、可测试的单元而非冗长的脚本。当某个步骤出错时,错误应指向单一责任模块,而非复杂的流程链。 在调整提示词之前,先使用固定的问题集测试召回率。仅仅更换提示词很难改善较差的信息检索效果。
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { ElastiCacheClient, DescribeCacheClustersCommand } from "@aws-sdk/client-elasticache";
import Redis from "ioredis";
// ---- 1️⃣ Type for the shape of our Redis JSON document ----
interface DocumentRecord {
id: string; // unique identifier for the source chunk
text: string; // original text
embedding: number[]; // vector of floats from OpenAI
}
// The `satisfies` operator guarantees that the constant below conforms exactly to DocumentRecord
const sampleDoc = {
id: "doc-001",
text: "Redis can store vectors.",
embedding: [0.12, -0.34, 0.56],
} satisfies DocumentRecord;
// ---- 2️⃣ Helper to get the Redis endpoint from ElastiCache ----
async function getRedisEndpoint(): Promise<string> {
const client = new ElastiCacheClient({});
const cmd = new DescribeCacheClustersCommand({ ShowCacheNodeInfo: true });
const resp = await client.send(cmd);
// Assume only one cluster exists for simplicity
const node = resp.CacheClusters?.[0].CacheNodes?.[0];
if (!node?.Endpoint?.Address) throw new Error("Redis endpoint not found");
return `${node.Endpoint.Address}:${node.Endpoint.Port}`;
}
// ---- 3️⃣ Lambda entry point ----
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
// Parse the incoming query string
const query = JSON.parse(event.body ?? "{}").question;
if (!query) {
return { statusCode: 400, body: JSON.stringify({ error: "Missing question" }) };
}
// 3️⃣️⃣️⃣ Get an embedding (replace with real OpenAI call)
const embedding = await fakeEmbedding(query); // returns number[]
// 4️⃣ Connect to Redis (cluster mode)
const redisEndpoint = await getRedisEndpoint();
const redis = new Redis(redisEndpoint, {
// If your cluster uses TLS (recommended) add:
// tls: {}
});
// 5️⃣ Store the embedding using RedisJSON
const docId = `doc:${Date.now()}`; // simple unique key
await redis.call(
"JSON.SET",
docId,
".",
JSON.stringify({
id: docId,
text: query,
embedding,
})
);
// 6️⃣ Run the KNN Lua script (defined later) to fetch top‑3 nearest vectors
const topK = 3;
const neighbors = await redis.eval(
KNN_SCRIPT,
0, // number of KEYS we pass (none, we use SCAN inside the script)
docId,
topK
);
// 7️⃣ Build a mock answer (in reality you would feed the neighbor texts to the LLM)
const answer = `Found ${neighbors.length} similar chunks.`;
await redis.quit();
return {
statusCode: 200,
body: JSON.stringify({ answer, neighbors }),
};
};
// ---- 4️⃣ Fake embedding generator (replace with real OpenAI call) ----
async function fakeEmbedding(text: string): Promise<number[]> {
// Simple deterministic pseudo‑embedding for demo purposes
const hash = Array.from(text).reduce((a, c) => a + c.charCodeAt(0), 0);
return Array.from({ length: 1536 }, (_, i) => ((hash + i) % 100) / 100);
}
使用RedisJSON存储嵌入向量
在研究使用RedisJSON存储嵌入向量时,首先需明确相关规范:所需的输入参数、成功标志,以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改始终符合要求。 将这一阶段视为输入与经过验证的输出之间的契约。为相关对象命名,定义成功判定标准,杜绝无声的半完成状态。 在调整提示词之前,先在固定的问题集上测试召回率。仅仅更换提示词很难改善较差的检索效果。
1. 为何选择JSON而非原始字符串?
在研究“为何选择 JSON 而非原始字符串?”这一主题时,首先需明确相关规范:所需的输入参数、成功信号,以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 在调整提示词之前,先使用固定的问题集测试召回率。仅仅更换提示词往往无法改善较差的检索效果。
2. 编写文档所需的最少代码量
在学习“2. 编写文档的最小代码量”这一内容时,首先需明确相关约定:所需的输入参数、成功信号,以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 应将配置信息与应用程序代码分开存放。环境文件、密钥存储以及功能开关应集中管理,这样操作人员无需查看整个系统结构即可进行审计。 在调整提示词之前,先使用固定的问题集来测试召回率。仅仅更换提示词往往无法解决检索效果不佳的问题。
// Assume `redis` is an ioredis instance connected to the cluster
const docKey = "doc:12345";
const payload: DocumentRecord = {
id: docKey,
text: "Why Redis can store vectors",
embedding: [0.01, 0.23, -0.45, /* … up to 1536 dims … */],
};
// Store the whole object as JSON under the root path "."
await redis.call(
"JSON.SET",
docKey,
".",
JSON.stringify(payload) // serialize to string before sending
);
3. 仅读取嵌入向量以节省带宽
在处理第3步时,为节省带宽仅读取嵌入向量,首先需明确相关约定:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 同时记录正常流程与异常恢复流程。重试机制、人工审核环节以及死信处理都是产品功能的一部分,而非后续需要补充的内容。 在调整提示词之前,需先用固定的问题集来衡量检索效果。仅仅更换提示词往往无法解决检索能力薄弱的问题。
// Retrieve only the `.embedding` field
const rawEmbedding = await redis.call("JSON.GET", docKey, ".embedding");
// Convert the JSON string back to a number array
const embedding: number[] = JSON.parse(rawEmbedding as string);
在处理第3步时,为节省带宽仅读取嵌入向量,首先需明确相关约定:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 应将此阶段视为输入与经过验证的输出之间的契约。为相关输出命名,定义成功判定标准,并杜绝无声的半完成状态。
4. ElastiCache上使用RedisJSON的注意事项
- 在ElastiCache上使用RedisJSON时,若能将其视为可度量的对象,将会更易发现潜在问题。在扩大应用范围之前,先记录一份最佳运行案例、一个故障实例以及回滚说明。同时将功能测试结果与执行时间、令牌消耗或查询成本一并记录下来。提前了解成本情况,可避免从演示环境过渡到共享环境时出现意外费用。此外,应将分块策略与检索策略分开处理——当质量指标发生变化时,修改其中一项不应强制要求重新编写另一项的代码。
实时查询与向量评分
将“实时查询与向量评分”视为可度量的表面来处理时,其效果最佳。在扩大范围之前,先收集一份理想的转录样本、一个失败案例以及回滚说明。 将配置置于应用程序代码之外。环境文件、密钥存储和功能标志应集中存放于一个位置,以便操作人员无需查看整个系统结构即可进行审计。 将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。
1. 相似度指标
- 将相似度指标视为可测量的对象时,其效果最佳。在扩大范围之前,需记录一份理想的处理结果、一个故障案例以及回滚说明。同时将正常流程与恢复流程记录下来。重试机制、人工审核环节以及错误处理都属于产品本身的功能,而非后续需要补充的内容。应将分块策略与检索策略分开,当质量指标发生变化时,修改其中一项不应迫使另一项也必须重新编写。
- 将相似度指标视为可测量的对象时,其效果最佳。在扩大范围之前,需记录一份理想的处理结果、一个故障案例以及回滚说明。应将此阶段视为输入数据与经过验证的输出结果之间的契约,为相关文档命名,明确成功标准,杜绝默默完成部分任务的情况。
cosine(a, b) = (a · b) / (|a| * |b|)
2. Lua脚本
对于第2点:在修改Lua脚本之前,需先定义输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。应在功能结果旁记录执行时间以及令牌或查询成本。提前显示成本信息,可避免在从演示环境切换到共享环境时出现意外费用。需注明实际作为答案依据的段落;没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。
-- KNN_SCRIPT: Find top‑K nearest vectors to a query embedding stored at `queryKey`.
-- Arguments:
-- KEYS[1] = (unused) – we pass 0 keys, the script scans the keyspace itself.
-- ARGV[1] = queryKey (the key that holds the fresh embedding)
-- ARGV[2] = k (how many neighbors to return)
local queryKey = ARGV[1]
local k = tonumber(ARGV[2])
-- 1️⃣ Load the query embedding from JSON
local queryEmbeddingJson = redis.call('JSON.GET', queryKey, '.embedding')
local queryEmbedding = cjson.decode(queryEmbeddingJson)
-- Helper: compute dot product
local function dot(a, b)
local sum = 0
for i = 1, #a do
sum = sum + a[i] * b[i]
end
return sum
end
-- Helper: compute vector length
local function norm(v)
return math.sqrt(dot(v, v))
end
local queryNorm = norm(queryEmbedding)
-- 2️⃣ Scan all keys that match the pattern "doc:*"
local cursor = "0"
local results = {} -- will hold {id, score, text}
repeat
local scan = redis.call('SCAN', cursor, 'MATCH', 'doc:*', 'COUNT', 1000)
cursor = scan[1]
local keys = scan[2]
for _, key in ipairs(keys) do
-- Skip the query key itself
if key ~= queryKey then
local docJson = redis.call('JSON.GET', key, '.')
local doc = cjson.decode(docJson)
local sim = dot(queryEmbedding, doc.embedding) / (queryNorm * norm(doc.embedding))
table.insert(results, { id = doc.id, score = sim, text = doc.text })
end
end
until cursor == "0"
-- 3️⃣ Sort by descending similarity and keep top‑k
table.sort(results, function(a, b) return a.score > b.score end)
local top = {}
for i = 1, math.min(k, #results) do
table.insert(top, results[i])
end
return cjson.encode(top)
3. 从TypeScript运行脚本
3. 从 TypeScript 运行脚本时,应在修改代码之前定义输入参数、步骤负责人以及退出条件。操作员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,这样操作员无需查看整个流程即可进行审核。 需注明实际作为答案依据的段落。如果没有引用,操作员就无法区分是幻觉内容还是索引缺失导致的错误。
// The script is stored as a string constant in our code
const KNN_SCRIPT = `...lua code from above...`;
// Execute the script
const topK = 5;
const rawResult = await redis.eval(KNN_SCRIPT, 0, docId, topK);
const neighbors: Array<{ id: string; score: number; text: string }> = JSON.parse(
rawResult as string
);
// `neighbors` now contains the most similar chunks, ready for the LLM.
4. 为何我们选择 Lua 而非客户端循环
对于第4点——为何选择Lua而非客户端循环——在修改代码之前,需明确输入参数、该步骤的负责人以及终止条件。操作员应能够从已知的检查点重新执行该步骤,而无需猜测隐藏状态。 需同时记录正常流程与异常恢复路径。重试机制、人工审核环节以及错误处理都是产品本身的组成部分,而非后续需要补充的功能。 必须引用实际作为答案依据的段落。没有引用的话,操作员就无法区分是虚假信息还是索引缺失导致的错误。 对于第4点——为何选择Lua而非客户端循环——在修改代码之前,需明确输入参数、该步骤的负责人以及终止条件。操作员应能够从已知的检查点重新执行该步骤,而无需猜测隐藏状态。 应将此阶段视为输入与验证后输出之间的契约。为相关成果命名,定义成功判定标准,并拒绝默许部分完成的状态。
n.
核心要点
在处理核心要点时,首先列出相关约定:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。
在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免从演示环境过渡到共享环境时出现意外费用。
在调整提示词之前,先使用固定的问题集测试系统的召回率。仅仅更换提示词往往无法解决检索效果不佳的问题。
操作检查清单
在处理操作检查清单时,同样要先列出相关约定:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。
优先选择小型、可测试的单元,而非庞大的脚本。当某个步骤出现故障时,故障应能指向具体的责任模块,而非复杂的流程链。
在调整提示词之前,先使用固定的问题集测试召回率。仅仅更换提示词很难改善较差的检索效果。
尽量降低渲染成本,只有在经过测试后才能将耗时的计算过程交由缓存处理。过早使用缓存可能会掩盖过时的属性错误。
只要预算允许,就应在持续集成过程中使用测试用例来检测关键路径,而非依赖真实的付费 API。
将配置信息与应用程序代码分开。环境文件、密钥存储以及功能开关应集中存放,以便运维人员无需查看整个系统结构即可进行审计。
在升级技术栈之前,先锁定版本,为关键路径记录标准操作流程,并明确回滚步骤。共享环境需要设置速率限制、租户验证机制,以及负责密钥轮换的明确责任人。与其追求华丽的临时演示,不如注重扎实的可靠性。