Redis®*: Recherche vectorielle et RAG

Recherche sémantique, recommandations et retrieval-augmented generation avec Redis

👋 Bienvenue sur la documentation de Stackhero !

Stackhero propose une solution Redis cloud prête à l'emploi qui offre de nombreux avantages, notamment :

  • Interface web Redis Commander incluse.
  • Taille et transferts de messages illimités.
  • Mises à jour simplifiées en un clic.
  • Performance optimale et sécurité renforcée grâce à une infrastructure privée et dédiée.

Gagnez du temps et simplifiez-vous la vie : il suffit de 5 minutes pour essayer la solution d'hébergement Redis cloud de Stackhero !

Si vous développez une recherche sémantique, un moteur de recommandation ou un pipeline de retrieval-augmented generation (RAG), vous avez besoin d'une solution fiable pour stocker des embeddings et retrouver rapidement les correspondances les plus proches d'une requête. Votre instance Redis sur Stackhero propose cette fonctionnalité nativement : le moteur de requête indexe les vecteurs et répond aux recherches de similarité en quelques millisecondes. Il n'est pas nécessaire d'ajouter une base de données vectorielle séparée à côté de Redis : tout fonctionne au même endroit.

Cette approche est à la fois productive et efficace grâce à deux fonctionnalités clés :

  • Requêtes hybrides. Vous pouvez combiner la recherche de similarité vectorielle avec des filtres classiques comme des plages numériques, des tags ou du texte intégral, le tout en une seule requête. Par exemple : "les 5 segments les plus proches de cette question, mais uniquement issus de documents que cet utilisateur est autorisé à lire, publiés après janvier" peuvent être récupérés en une seule requête, sans enchaîner plusieurs appels.
  • Architecture simplifiée. Embeddings, cache et sessions cohabitent, partageant identifiants, sauvegardes et supervision. Cela réduit la complexité et les surcoûts.

Activez le module Search (et JSON si vous souhaitez stocker vos documents au format JSON) dans la section Modules de la configuration de votre service sur le tableau de bord Stackhero. Pour un guide détaillé étape par étape, consultez le guide Search et JSON.

Pour déclarer un champ vectoriel, indiquez sa dimension et la métrique de distance. La dimension doit correspondre à votre modèle d'embedding : par exemple, 1536 pour OpenAI text-embedding-3-small, 768 pour de nombreux modèles open source, etc.

FT.CREATE chunksIndex
  ON HASH
  PREFIX 1 chunk:
  SCHEMA
    content TEXT
    documentId TAG
    publishedAt NUMERIC
    embedding VECTOR HNSW 6
      TYPE FLOAT32
      DIM 1536
      DISTANCE_METRIC COSINE

HNSW crée un index graphe qui maintient des requêtes rapides même lorsque votre collection grandit, ce qui en fait un bon choix au-delà de quelques milliers de vecteurs. Pour de petites collections où la précision exacte est prioritaire, vous pouvez utiliser FLAT pour une recherche exhaustive.

COSINE est la métrique recommandée pour la plupart des modèles d'embedding textuels. Vous pouvez aussi choisir L2 ou IP selon votre cas d'usage.

Une requête K nearest neighbors (KNN) s'écrit ainsi. Le vecteur est fourni sous forme de bytes float32 little-endian bruts :

FT.SEARCH chunksIndex "*=>[KNN 5 @embedding $queryVector AS score]"
  PARAMS 2 queryVector "<rawBytes>"
  SORTBY score
  RETURN 2 content score
  DIALECT 2

Pour combiner recherche de similarité et filtres, remplacez simplement * par une expression de filtre :

FT.SEARCH chunksIndex "(@documentId:{doc42} @publishedAt:[1735689600 +inf])=>[KNN 5 @embedding $queryVector AS score]"
  PARAMS 2 queryVector "<rawBytes>"
  SORTBY score
  DIALECT 2

Les requêtes vectorielles nécessitent DIALECT 2. Si vous l'omettez, la requête utilise l'ancienne syntaxe et retourne une erreur.

import { createClient, SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM } from 'redis';

const client = createClient({ url: process.env.STACKHERO_REDIS_URL_TLS });
await client.connect();

const DIMENSIONS = 1536;
const toBytes = embedding => Buffer.from(new Float32Array(embedding).buffer);

// 1. Créez l'index une seule fois, au démarrage
try {
  await client.ft.create(
    'chunksIndex',
    {
      content: SCHEMA_FIELD_TYPE.TEXT,
      documentId: SCHEMA_FIELD_TYPE.TAG,
      embedding: {
        type: SCHEMA_FIELD_TYPE.VECTOR,
        ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW,
        TYPE: 'FLOAT32',
        DIM: DIMENSIONS,
        DISTANCE_METRIC: 'COSINE'
      }
    },
    { ON: 'HASH', PREFIX: 'chunk:' }
  );
}
catch (error) {
  if (!error.message.includes('Index already exists')) {
    throw error;
  }
}

// 2. Indexez vos segments (l'embedding provient de votre fournisseur de modèle)
async function indexChunk({ id, documentId, content, embedding }) {
  await client.hSet(`chunk:${id}`, {
    content,
    documentId,
    embedding: toBytes(embedding)
  });
}

// 3. Récupérez les segments les plus proches d'une question, limités à un document
async function retrieve({ questionEmbedding, documentId, count = 5 }) {
  const results = await client.ft.search(
    'chunksIndex',
    `(@documentId:{${documentId}})=>[KNN ${count} @embedding $queryVector AS score]`,
    {
      PARAMS: { queryVector: toBytes(questionEmbedding) },
      SORTBY: 'score',
      RETURN: [ 'content', 'score' ],
      DIALECT: 2
    }
  );

  return results.documents.map(({ value }) => value.content);
}

// 4. Fournissez-les à votre modèle comme contexte
const context = await retrieve({ questionEmbedding, documentId: 'doc42' });
const prompt = `Répondez uniquement en utilisant ce contexte :\n\n${context.join('\n\n')}\n\nQuestion : ${question}`;
import os
import numpy as np
import redis
from redis.commands.search.field import TextField, TagField, VectorField
from redis.commands.search.index_definition import IndexDefinition, IndexType
from redis.commands.search.query import Query

DIMENSIONS = 1536

r = redis.from_url(os.environ['STACKHERO_REDIS_URL_TLS'])

# Créez l'index une seule fois, au démarrage
try:
    r.ft('chunksIndex').create_index(
        (
            TextField('content'),
            TagField('documentId'),
            VectorField(
                'embedding',
                'HNSW',
                {'TYPE': 'FLOAT32', 'DIM': DIMENSIONS, 'DISTANCE_METRIC': 'COSINE'},
            ),
        ),
        definition=IndexDefinition(prefix=['chunk:'], index_type=IndexType.HASH),
    )
except redis.ResponseError as error:
    if 'Index already exists' not in str(error):
        raise


def index_chunk(chunk_id, document_id, content, embedding):
    r.hset(
        f'chunk:{chunk_id}',
        mapping={
            'content': content,
            'documentId': document_id,
            'embedding': np.array(embedding, dtype=np.float32).tobytes(),
        },
    )


def retrieve(question_embedding, document_id, count=5):
    query = (
        Query(f'(@documentId:{{{document_id}}})=>[KNN {count} @embedding $queryVector AS score]')
        .sort_by('score')
        .return_fields('content', 'score')
        .dialect(2)
    )
    results = r.ft('chunksIndex').search(
        query,
        query_params={'queryVector': np.array(question_embedding, dtype=np.float32).tobytes()},
    )
    return [document.content for document in results.docs]

Les vecteurs sont stockés en mémoire. Vous pouvez estimer les besoins mémoire avec cette formule :

numberOfVectors x dimensions x 4 bytes, plus environ 30 à 50 % supplémentaires pour le graphe HNSW.

Par exemple, un million de vecteurs de 1536 dimensions utilisent environ 6 Go pour les vecteurs bruts. Une offre de 20 Go est un point de départ confortable pour cette volumétrie. Vous pouvez réduire la consommation mémoire en :

  • Utilisant un modèle plus petit. Par exemple, un modèle à 768 dimensions divise par deux la mémoire utilisée par rapport à un modèle à 1536 dimensions.
  • Stockant en FLOAT16 au lieu de FLOAT32 si votre modèle le permet, ce qui réduit encore l'espace de moitié.

Vous pouvez suivre la consommation réelle avec FT.INFO chunksIndex et la métrique used_memory dans votre supervision Prometheus. Si vos besoins augmentent, il est facile de faire évoluer votre offre directement depuis votre tableau de bord.

  • Les index vectoriels ne fonctionnent que sur la base de données 0, comme tous les index de recherche dans Redis.
  • Les ensembles vectoriels offrent une alternative plus simple sans configuration. Les commandes VADD et VSIM sont intégrées à Redis, ce qui permet d'utiliser la recherche de similarité immédiatement, sans activer de modules. C'est pratique pour des scénarios de recommandation simples sur un seul ensemble. Le moteur de requête reste préférable si vous avez besoin de filtres, de pagination ou de plusieurs champs.
  • La construction d'un index sur des clés existantes s'effectue en tâche de fond. Vous pouvez suivre la progression avec FT.INFO. Les recherches retournent des résultats partiels tant que l'indexation n'est pas terminée.

Pour en savoir plus sur la recherche vectorielle dans Redis, consultez la documentation officielle Redis vector search.