Inicio / Artículos

ElastiCache para RAG: Cómo almacenar y consultar embeddings en

ElastiCache para RAG: almacene y consulte embeddings en Redis desde Node.js y TypeScript, configure el clúster, cree el índice vectorial y ejecute búsquedas de similitud.

Editor: REACTAPP.TOP13 min de lectura2918 palabras

Fuentes: ElastiCache for RAG: How to Store and Query Embeddings in Redis with Node.js and TypeScript

Úselo como una versión reestructurada dirigida a operadores de las ideas presentadas en “ElastiCache para RAG: Cómo almacenar y consultar embeddings en Redis con Node.js y TypeScript”: etapas claras, secciones de código ordenadas y notas de recuperación que perduran tras la transferencia de responsabilidades. La visión general funciona mejor cuando se considera como una superficie medible. Capture una transcripción clave, un caso de fallo y la nota de reversión antes de ampliar el alcance. Considere esta etapa como un contrato entre las entradas y los resultados validados. Asigne nombres a los artefactos, defina comprobaciones de éxito y rechace completaciones parciales silenciosas.

Por qué Redis puede ser un almacén de vectores

Para entender por qué Redis puede ser un almacén vectorial, se deben definir las entradas, el responsable de cada paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido, sin tener que adivinar el estado oculto. Se deben registrar los tiempos de ejecución y el costo de tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando el proceso pasa de entornos de demostración a entornos compartidos. Es necesario citar los pasajes que realmente sustentan la respuesta; sin ellas, los operadores no pueden distinguir entre alucinaciones y brechas en el indexado.

Configuración de un clúster Serverless de ElastiCache

Para configurar un clúster Serverless de ElastiCache, defina los parámetros de entrada, el responsable de la tarea y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la tarea a partir de un punto de control conocido, sin tener que adivinar el estado oculto. Guarde la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de secretos y las banderas de funcionalidad deben encontrarse en un único lugar que los operadores puedan auditar sin necesidad de leer todo el sistema. Mencione las secciones del texto que sirvieron como base para la respuesta. Sin citaciones, los operadores no podrán distinguir entre una alucinación y una laguna en el indexado.

1. Crear el clúster

Para el punto 1: cree el clúster, defina las entradas, el responsable de la tarea y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la tarea a partir de un punto de control conocido sin tener que adivinar el estado oculto. Documente tanto la ruta óptima como la ruta de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores. Cite los pasajes que realmente sustentan la respuesta; sin citas, los operadores no podrán distinguir entre alucinaciones y fallos en el indexado.

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

Para el punto 1: cree el clúster, defina las entradas, el responsable de la tarea y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la tarea a partir de un punto de control conocido sin tener que adivinar el estado oculto. Considere esta etapa como un contrato entre las entradas y los resultados validados. Asigne nombres a los artefactos, defina verificaciones de éxito y rechace las completaciones parciales silenciosas.

2. Tenga en cuenta los puntos críticos

Al trabajar en la sección 2. Tenga en cuenta los puntos críticos, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación ayuda a mantener honestos los cambios posteriores en el código. Registre los tiempos y el costo de tokens o consultas junto con los resultados funcionales. Ver la información sobre costos desde el principio evita facturas inesperadas cuando se pasa de entornos de demostración a entornos compartidos. Mida el rendimiento en un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

Código Lambda seguro con el operador satisfies

Al trabajar con código Lambda seguro de tipo utilizando el operador satisfies, primero escribe el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Mantén la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de secretos y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin tener que leer todo el sistema. Mide la tasa de recuperación en un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

1. Instala los paquetes necesarios

Al trabajar en el paso 1: Instale los paquetes necesarios, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Documente junto con ello el camino óptimo y el camino de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores. Mida la precisión con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

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. El esqueleto de Lambda

Al trabajar en 2. El esqueleto de Lambda, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Prefiera unidades pequeñas y probables sobre scripts extensos. Cuando un paso falla, el fallo debe apuntar a una única responsabilidad y no a un proceso complicado. Mida la capacidad de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

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);
}

Almacenar embeddings con RedisJSON

Al trabajar en el almacenamiento de embeddings con RedisJSON, primero escribe el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Considera esta etapa como un contrato entre las entradas y las salidas validadas. Nombra los artefactos, define las verificaciones de éxito y rechaza las completaciones parciales silenciosas. Mide la capacidad de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

1. ¿Por qué JSON en lugar de una cadena raw?

Al trabajar en la sección 1. ¿Por qué JSON en lugar de una cadena raw?, anote primero el contrato: entradas requeridas, señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Registre los tiempos y el costo de tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando se pasa de entornos de demostración a entornos compartidos. Mida el rendimiento en un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

2. Código mínimo para escribir un documento

Al trabajar en la sección 2: Código mínimo para escribir un documento, primero anote el contrato: entradas requeridas, señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Guarde la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de secretos y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin tener que leer todo el sistema. Mida la tasa de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

// 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. Leer solo el embedding (para ahorrar ancho de banda)

Al trabajar en el punto 3, al leer solo la representación vectorial (para ahorrar ancho de banda), anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación garantiza que los cambios posteriores en el código sean transparentes. Documente tanto la ruta de éxito como la ruta de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores. Mida el rendimiento de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

// 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);

Al trabajar en el punto 3, al leer solo la representación vectorial (para ahorrar ancho de banda), anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación garantiza que los cambios posteriores en el código sean transparentes. Considere esta etapa como un contrato entre las entradas y las salidas validadas. Asigne nombres a los resultados generados, defina pruebas de éxito y evite completaciones parciales silenciosas.

4. Problemas comunes con RedisJSON en ElastiCache

  1. Los problemas comunes con RedisJSON en ElastiCache funcionan mejor cuando se consideran como un conjunto de aspectos medibles. Capture una transcripción ejemplar, un caso de fallo y las notas de reversión antes de ampliar el alcance. Registre los tiempos y el costo de tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando se pasa de entornos de demostración a entornos compartidos. Separe la política de particionamiento de la política de recuperación; cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

Consultas y puntuación de vectores en tiempo real

La consulta y puntuación de vectores en tiempo real funciona mejor cuando se trata como una superficie medible. Capture un registro exitoso, un caso de fallo y la nota de reversión antes de ampliar el alcance. Mantenga la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de datos secretos y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin tener que leer todo el sistema. Separe la política de fragmentación de la política de recuperación; cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

1. La métrica de similitud

  1. La métrica de similitud funciona mejor cuando se trata como una superficie medible. Capture una transcripción ideal, un caso de fallo y la nota de reversión antes de ampliar el alcance. Documente tanto el camino óptimo como el camino de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son ajustes realizados posteriormente. Separe la política de fragmentación de la política de recuperación. Cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.
  2. La métrica de similitud funciona mejor cuando se trata como una superficie medible. Capture una transcripción ideal, un caso de fallo y la nota de reversión antes de ampliar el alcance. Considere esta etapa como un contrato entre las entradas y las salidas validadas. Asigne nombres a los artefactos, defina verificaciones de éxito y rechace las completaciones parciales silenciosas.
cosine(a, b) = (a · b) / (|a| * |b|)

2. El script Lua

Para el punto 2: en el script de Lua, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido, sin tener que adivinar el estado oculto. Registre los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. La visibilidad temprana de los costos evita facturas inesperadas cuando el proceso pasa de entornos de demostración a entornos compartidos. Cite los pasajes que realmente sustentan la respuesta; sin citas, los operadores no pueden distinguir entre alucinaciones y brechas en el indexado.

-- 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. Ejecutar el script desde TypeScript

Para el punto 3: al ejecutar el script desde TypeScript, defina las entradas, el responsable de la tarea y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la tarea a partir de un punto de control conocido sin tener que adivinar el estado oculto. Mantenga la configuración fuera del código de la aplicación; los archivos de entorno, los almacenes de datos confidenciales y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin necesidad de leer todo el sistema.

// 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. Por qué usamos Lua en lugar de bucles del lado del cliente

Para el punto 4: Por qué utilizamos Lua en lugar de bucles del lado del cliente, se deben definir las entradas, el responsable de la etapa y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la etapa a partir de un punto de control conocido sin tener que adivinar el estado oculto. Se debe documentar tanto la ruta óptima como la ruta de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores. Se deben citar los pasajes que realmente sustentan la respuesta; sin citas, los operadores no pueden distinguir entre alucinaciones y fallos en el indexado. Para el punto 4: Por qué utilizamos Lua en lugar de bucles del lado del cliente, se deben definir las entradas, el responsable de la etapa y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la etapa a partir de un punto de control conocido sin tener que adivinar el estado oculto. Considere esta etapa como un contrato entre las entradas y los resultados validados. Asigne nombres a los artefactos, defina comprobaciones de éxito y rechace las completaciones parciales silenciosas.

n.

La conclusión

Al trabajar en la sección de La conclusión, anote primero el contrato: los datos de entrada requeridos, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación garantiza que los cambios posteriores en el código sean transparentes. Registre los tiempos de ejecución y el costo de tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando se pasa de entornos de demostración a entornos compartidos. Mida el rendimiento en un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

Lista de verificación operativa

Al trabajar en la lista de verificación operativa, anote primero el contrato: los datos de entrada requeridos, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación garantiza que los cambios posteriores en el código sean transparentes.

Prefiera unidades pequeñas y verificables en lugar de scripts extensos. Cuando un paso falla, el fallo debe indicar una única responsabilidad y no un proceso complicado.

Mida el rendimiento de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

Mantenga los procesos de generación económicos y reserve las operaciones costosas para su ejecución mediante memorización solo después de realizar mediciones. Una memorización prematura puede ocultar errores en los datos utilizados.

Añada una prueba de funcionamiento que ejerza la ruta crítica en los procesos de integración continua utilizando datos fijos, y no APIs pagadas en tiempo real, siempre que lo permitan los presupuestos.

Mantenga la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de datos confidenciales y las flags de funcionalidad deben estar en un lugar único que los administradores puedan auditar sin tener que leer todo el sistema.

Antes de promocionar la arquitectura, congele las versiones, guarde una transcripción de referencia para la ruta crítica y confirme los pasos necesarios para revertir cambios. Los entornos compartidos requieren límites de uso, verificaciones de asignación y un responsable claro para la rotación de datos confidenciales. Prefiera una fiabilidad sólida a demostraciones ingeniosas pero puntuales.