ElastiCache для RAG: как хранить и выполнять запросы к эмбеддингам
ElastiCache для RAG: хранение и поиск эмбеддингов в Redis с использованием Node.js и TypeScript, настройка кластера, создание векторного индекса и выполнение поиска по сходству.
Источники: ElastiCache for RAG: How to Store and Query Embeddings in Redis with Node.js and TypeScript
Используйте это как структурированную версию идей из статьи «ElastiCache for RAG: Как хранить и выполнять запросы к эмбеддингам в Redis с использованием Node.js и TypeScript» для операторов: четкие этапы, упорядоченные блоки кода и записи о восстановлении, которые сохраняются при передаче задач. Обзор работает лучше всего, когда рассматривается как измеримая структура. Соберите один эталонный пример работы, один случай сбоя и записи о возврате к предыдущему состоянию перед расширением объема работ. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия элементам, определите критерии успеха и не допускайте молчаливого частичного выполнения задачи.
Почему Redis может служить хранилищем векторов
Чтобы понять, почему Redis может служить хранилищем векторов, необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рядом с функциональными результатами следует записывать время выполнения и стоимость токенов или запросов. Отображение затрат на раннем этапе предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды. Необходимо указывать конкретные фрагменты текста, на которых основан ответ. Без цитат операторы не смогут отличить галлюцинации от пробелов в индексации.
Настройка серверless-кластера ElastiCache
Для настройки безуправляемого кластера ElastiCache необходимо заранее определить входные параметры, ответственного за выполнение шага и критерии завершения перед внесением изменений в код. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии системы. Конфигурацию следует хранить отдельно от кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь кодовый граф. Указывайте те участки текста, которые легли в основу ответа. Без цитат операторы не смогут отличить вымысел от проблем с индексацией.
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. Обратите внимание на подводные камни сначала запишите условия контракта: необходимые входные данные, сигнал о успешном выполнении и что происходит при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Записывайте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат на раннем этапе предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды. Измеряйте точность воспроизведения ответов на фиксированном наборе вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска.
Код Lambda с типобезопасностью с использованием оператора satisfies
При работе с безопасным по типу кодом Lambda с использованием оператора satisfies сначала запишите условия работы: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода.
Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код.
Измеряйте степень воспроизведения ответов на фиксированном наборе вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска.
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 вместо обычной строки?
При работе над разделом «1. Почему 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. Проблемы использования RedisJSON в ElastiCache
- При работе с RedisJSON в ElastiCache наилучший результат достигается, если рассматривать его как объект с измеримыми показателями. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма исследований. Зафиксируйте время выполнения операций, стоимость токенов или запросов вместе с функциональными результатами. Отслеживание затрат на раннем этапе помогает избежать неожиданных счетов при переходе от демо-среды к общедоступным средам. Разделяйте политику разбиения данных на части и политику их извлечения; изменение одной из них не должно приводить к необходимости переписывания другой при изменении показателей качества.
Запросы и оценка векторов в реальном времени
Работа с запросами и оценкой векторов в реальном времени наилучшим образом функционирует, когда её рассматривают как измеримую поверхность. Соберите один идеальный пример обработки данных, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работ. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь граф структуры. Разделяйте политику разбиения данных на части и политику их поиска. Изменение одной из них не должно приводить к необходимости переписывания другой при изменении показателей качества.
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.
Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять без необходимости изучения всей структуры кода.
Перед внедрением новой технологической стека заморозьте версии, сохраните эталонный вариант работы критически важных компонентов и уточните шаги для возврата к предыдущей версии. В совместных средах необходимо использовать ограничения на частоту запросов, проверки принадлежности к конкретному тенанту и четко определить ответственного за обновление секретов. Лучше добиваться простой надежности, чем создавать красивые, но единоразовые демонстрации.