Valkey: Recherche vectorielle et RAG
Recherche sémantique, recommandations et retrieval-augmented generation avec Valkey
👋 Bienvenue dans la documentation Stackhero !
Stackhero propose une solution Valkey cloud clé en main qui offre de nombreux avantages, dont :
- Interface web Valkey Admin incluse.
- Taille et transferts de messages illimités.
- Mises à jour faciles en un seul clic.
- Performance optimale et sécurité avancée grâce à une infrastructure privée et dédiée.
Gagnez du temps et simplifiez-vous la vie : il ne vous faut que 5 minutes pour essayer l’hébergement Valkey cloud de Stackhero !
Si vous développez une recherche sémantique, un moteur de recommandations ou un pipeline de retrieval-augmented generation (RAG), vous devez stocker des embeddings et retrouver rapidement les correspondances les plus proches pour une requête. Stackhero pour Valkey gère cela nativement : le module de recherche indexe les vecteurs et répond aux requêtes de similarité en quelques millisecondes. Vous n'avez pas besoin de mettre en place une base de données vectorielle distincte en plus du service Valkey que vous utilisez déjà.
Deux fonctionnalités rendent cette approche pratique et efficace :
- Requêtes hybrides. Vous pouvez effectuer des recherches de similarité vectorielle et appliquer des filtres classiques (plages numériques, tags ou texte) en une seule requête. Par exemple, vous pouvez récupérer « les 5 segments les plus proches de cette question, mais uniquement parmi les documents que cet utilisateur est autorisé à lire » en une seule requête, pas deux.
- Un composant de moins à gérer. Vos embeddings sont stockés avec vos données de cache et de session, en utilisant les mêmes identifiants, sauvegardes et outils de monitoring. Cela simplifie votre stack et facilite la gestion.
Avant de commencer
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 plus de détails, consultez le guide search et JSON.
Création d'un index vectoriel
Déclarez un champ vectoriel en précisant sa dimension et la métrique de distance. La dimension doit correspondre à votre modèle : par exemple, 1536 pour OpenAI text-embedding-3-small, ou 768 pour de nombreux modèles open source.
FT.CREATE chunksIndex
ON HASH
PREFIX 1 chunk:
SCHEMA
content TEXT
documentId TAG
embedding VECTOR HNSW 6
TYPE FLOAT32
DIM 1536
DISTANCE_METRIC COSINE
HNSW crée un index graphique, permettant des recherches approximatives rapides même lorsque votre collection devient volumineuse. C'est recommandé pour les collections de plus de quelques milliers de vecteurs. Pour de petites collections, vous pouvez utiliser FLAT pour une recherche exacte et exhaustive.
COSINE est généralement la meilleure métrique de distance pour les embeddings de texte. Les métriques L2 et IP sont aussi supportées.
Requêtes
Une requête K-nearest neighbors (KNN) transmet le vecteur en paramètre, sous forme de bytes float32 little-endian bruts :
FT.SEARCH chunksIndex "*=>[KNN 5 @embedding $queryVector]"
PARAMS 2 queryVector "<rawBytes>"
Pour combiner la recherche vectorielle avec des filtres, il suffit de remplacer le * par votre expression de filtre :
FT.SEARCH chunksIndex "@documentId:{doc42}=>[KNN 5 @embedding $queryVector]"
PARAMS 2 queryVector "<rawBytes>"
Un pipeline RAG en Node.js
Valkey utilise le même protocole que Redis, donc vous pouvez utiliser n'importe quel client compatible Redis.
import { createClient, SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM } from 'redis';
const client = createClient({ url: process.env.STACKHERO_VALKEY_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 (embedding fourni par votre 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, restreints à un document
async function retrieve({ questionEmbedding, documentId, count = 5 }) {
const results = await client.ft.search(
'chunksIndex',
`@documentId:{${documentId}}=>[KNN ${count} @embedding $queryVector]`,
{ PARAMS: { queryVector: toBytes(questionEmbedding) } }
);
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}`;
La même chose en Python
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_VALKEY_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]')
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]
Dimensionner votre service
Les vecteurs sont stockés en mémoire, il est donc important de planifier leur empreinte. L'estimation pour des vecteurs float32 est :
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 nécessitent environ 6 Go pour les vecteurs bruts. Un forfait de 20 Go constitue un point de départ confortable. Vous pouvez réduire la consommation mémoire en choisissant un modèle avec moins de dimensions : un modèle à 768 dimensions utilise seulement la moitié de la mémoire d'un modèle à 1536 dimensions.
Vous pouvez vérifier la consommation mémoire réelle avec FT.INFO chunksIndex et avec la métrique used_memory dans votre monitoring Prometheus. Si vous avez besoin de plus de mémoire, vous pouvez à tout moment faire évoluer votre forfait depuis votre tableau de bord.
Bon à savoir
- L'indexation des clés existantes s'effectue en arrière-plan. Vous pouvez suivre la progression avec
FT.INFO. Les recherches renverront des résultats partiels tant que le remplissage n'est pas terminé. - Le module de recherche implémente un ensemble ciblé de commandes
FT.*. Pour la recherche vectorielle, il couvre l'essentiel : HNSW et KNN exact, filtrage hybride etFT.AGGREGATE.
Pour une documentation complète, consultez la documentation Valkey search.