Accueil / Articles

ElastiCache pour RAG : Comment stocker et interroger des embeddings dans

ElastiCache pour RAG : stocker et interroger des embeddings dans Redis depuis Node.js et TypeScript, configurer le cluster, créer l’index vectoriel et effectuer des recherches de similarité.

Éditeur : REACTAPP.TOP13 min de lecture2918 mots

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

Utilisez ceci comme une version révisée destinée aux opérateurs des idées présentées dans « ElastiCache pour RAG : Comment stocker et interroger des embeddings dans Redis avec Node.js et TypeScript » : étapes claires, sections de code ordonnées, ainsi que des notes de récupération permettant de gérer les transferts de responsabilités. L’aperçu fonctionne le mieux lorsqu’il est considéré comme une surface mesurable. Capturez un enregistrement exemplaire, un cas d’échec et les notes de réversion avant d’élargir le périmètre. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définez des vérifications de succès, et refusez toute complétion partielle silencieuse.

Pourquoi Redis peut être un stockage vectoriel

Pour comprendre pourquoi Redis peut servir de stockage vectoriel, il convient de définir les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu, sans avoir à deviner l’état caché. Enregistrer les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts permet d’éviter des factures inattendues lorsque le processus passe de l’environnement de démonstration à des environnements partagés. Citer les passages qui fondent réellement la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

Mettre en place un cluster ElastiCache Serverless

Pour configurer un cluster Serverless d’ElastiCache, définissez les paramètres d’entrée, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans avoir à lire l’ensemble du système. Citez les passages qui servent de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

1. Créer le cluster

Pour le point 1 : créez d’abord le cluster, définites les entrées, l’ responsable de l’étape ainsi que les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez conjointement le parcours normal et les procédures de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations ultérieures. Citez les passages qui justifient réellement la réponse ; sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

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

Pour le point 1 : créez d’abord le cluster, définites les entrées, l’ responsable de l’étape ainsi que les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des vérifications de succès et refusez les terminations partielles silencieuses.

2. Notez les pièges à éviter

Lorsque vous travaillez sur la section 2. Notez les pièges à éviter, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le système passe d’un environnement de démonstration à des environnements partagés. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

Code Lambda sécurisé grâce à l’opérateur satisfies

Lorsque vous travaillez avec du code Lambda sécurisé par type à l’aide de l’opérateur satisfies, notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Gardez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

1. Installer les paquets requis

Lors de l’étape 1. Installer les paquets requis, notez d’abord le contrat : les entrées nécessaires, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Documentez ensemble le parcours normal et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations apportées ultérieurement. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

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. Le squelette Lambda

Lorsque vous travaillez sur la section 2. Le squelette Lambda, notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Préférez des unités petites et testables à des scripts volumineux. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un processus complexe et embrouillé. Évaluez le taux de rappel sur un ensemble de questions fixe avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

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

Stockage des embeddings avec RedisJSON

Lorsque vous travaillez sur le stockage d’embeddings avec RedisJSON, notez d’abord les exigences : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux éléments générés, définez des vérifications de succès, et refusez les terminations partielles silencieuses. Évaluez le taux de rappel sur un ensemble de questions fixe avant d’ajuster les prompts. Le simple changement de prompts ne résout que rarement un système de récupération insuffisant.

1. Pourquoi JSON plutôt qu’une chaîne brute ?

Lorsque vous travaillez sur la section 1. Pourquoi JSON plutôt qu’une chaîne brute ?, notez d’abord les exigences : entrées requises, signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le système passe d’un environnement de démonstration à des environnements partagés. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Changer fréquemment les prompts ne résout que rarement un système de récupération insuffisant.

2. Code minimal pour écrire un document

Lorsque vous travaillez sur la section 2 – Code minimal pour écrire un document – notez d’abord les exigences : entrées requises, signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les administrateurs peuvent auditer sans devoir lire l’ensemble du système. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

// 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. Ne lire que l’embedding (pour économiser la bande passante)

Lorsque vous travaillez sur le point 3, et que vous ne lisez que l’embedding pour économiser de la bande passante, notez d’abord les conditions prévues : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Documentez à la fois le parcours normal et celui de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations apportées ultérieurement. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

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

Lorsque vous travaillez sur le point 3, et que vous ne lisez que l’embedding pour économiser de la bande passante, notez d’abord les conditions prévues : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des critères de succès, et refusez les terminations partielles silencieuses.

4. Points à considérer avec RedisJSON sur ElastiCache

  1. Les points à considérer avec RedisJSON sur ElastiCache fonctionnent le mieux lorsqu’ils sont traités comme une surface mesurable. Capturez un exemple réussi, un cas d’échec ainsi que des notes de réversion avant d’élargir le périmètre. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite des factures inattendues lorsque le système passe d’un environnement de démonstration à des environnements partagés. Séparez la politique de segmentation des données de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.

Requêtes et évaluation de vecteurs en temps réel

La consultation et l’évaluation des vecteurs en temps réel fonctionnent le mieux lorsqu’elles sont considérées comme une surface mesurable. Capturez un exemple réussi, un cas d’échec ainsi que la note de réversion avant d’élargir le périmètre. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Séparez la politique de segmentation de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.

1. La métrique de similarité

  1. La métrique de similarité fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Documentez ensemble le parcours normal et le parcours de récupération. Les tentatives répétées, les contrôles humains et le traitement des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure. Séparez la politique de segmentation de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.
  2. La métrique de similarité fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des vérifications de succès et refusez les complétions partielles silencieuses.
cosine(a, b) = (a · b) / (|a| * |b|)

2. Le script Lua

Pour le point 2, concernant le script Lua, définissez les entrées, l’auteur de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût des jetons ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce du coût permet d’éviter des factures inattendues lorsque le parcours passe de l’environnement de démonstration à des environnements partagés. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

-- 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. Exécution du script depuis TypeScript

Pour 3. Lors de l’exécution du script depuis TypeScript, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les stocks de secrets et les flags fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans avoir à lire l’ensemble du graphe. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

// 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. Pourquoi nous utilisons Lua plutôt que des boucles côté client

Pour le point 4 : pourquoi nous utilisons Lua plutôt que des boucles côté client, il faut définir les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez conjointement le parcours normal et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’une mise en forme ultérieure. Citez les passages qui justifient réellement la réponse ; sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation. Pour le point 4 : pourquoi nous utilisons Lua plutôt que des boucles côté client, il faut définir les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des vérifications de succès et refusez les completions partielles silencieuses.

n.

Les points clés

Lorsque vous travaillez sur les points clés, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût en tokens ou en requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le système passe de l’environnement de démonstration à des environnements partagés. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Changer fréquemment les prompts ne résout que rarement un système de récupération inefficace.

Liste de contrôle opérationnelle

Lorsque vous travaillez sur la liste de contrôle opérationnelle, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code.

Préférez des unités petites et testables plutôt que des scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une responsabilité précise plutôt qu’un processus embrouillé.

Évaluez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Le changement fréquent des prompts ne résout que rarement un système de récupération inefficace.

Rendez le traitement peu coûteux et reportez les calculs onéreux à la mise en mémoire uniquement après avoir effectué des mesures. Une mise en mémoire prématurée peut cacher des erreurs liées à des données obsolètes.

Ajoutez un test de base qui exerce le chemin critique dans l’environnement CI en utilisant des fixtures, et non des API payantes en ligne, chaque fois que le budget le permet.

Gardez la configuration en dehors du code de l’application. Les fichiers d’environnement, les stockages de secrets et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les administrateurs peuvent auditer sans devoir lire l’ensemble du système.

Au préalable de promouvoir la stack, figez les versions, conservez une transcription exemplaire du chemin critique et vérifiez les étapes de rollback. Les environnements partagés nécessitent des limites de débit, des contrôles d’attribution et un responsable clair pour la rotation des secrets. Préférez une fiabilité solide à des démonstrations brillantes mais ponctuelles.