Valkey: Vector search en RAG

Semantisch zoeken, aanbevelingen en retrieval-augmented generation met Valkey

👋 Welkom bij de Stackhero-documentatie!

Stackhero biedt een kant-en-klare Valkey cloud oplossing met tal van voordelen, waaronder:

  • Valkey Admin webinterface inbegrepen.
  • Onbeperkte berichtgrootte en overdrachten.
  • Updates eenvoudig met één klik.
  • Optimale prestaties en sterke beveiliging dankzij een privé, dedicated infrastructuur.

Bespaar tijd en maak uw leven eenvoudiger: het kost slechts 5 minuten om de Valkey cloud hosting oplossing van Stackhero uit te proberen!

Als u werkt aan semantisch zoeken, een aanbevelingsengine of een retrieval-augmented generation (RAG) pipeline, moet u embeddings opslaan en snel de dichtstbijzijnde matches voor een query vinden. Stackhero voor Valkey ondersteunt dit direct: de search-module indexeert vectoren en beantwoordt gelijkenisqueries binnen milliseconden. U hoeft geen aparte vectordatabase op te zetten naast de Valkey-service die u al gebruikt.

Twee eigenschappen maken deze aanpak praktisch en efficiënt:

  • Hybride queries. U kunt vector-gelijkeniszoekopdrachten uitvoeren en reguliere filters toepassen (zoals numerieke bereiken, tags of tekst) in één enkele aanvraag. U kunt bijvoorbeeld "de 5 segmenten die het dichtst bij deze vraag liggen, maar alleen uit documenten die deze gebruiker mag lezen" ophalen met één query, niet twee.
  • Minder losse componenten. Uw embeddings worden samen met uw cache- en sessiedata opgeslagen, met dezelfde inloggegevens, back-ups en monitoringtools. Dit houdt uw stack eenvoudiger en beter beheersbaar.

Activeer de module Search (en JSON als u documenten als JSON wilt opslaan) in het gedeelte Modules van uw serviceconfiguratie op het Stackhero dashboard. Zie voor meer details de search en JSON handleiding.

Definieer een vectorveld door de dimensie en de afstandsmaat op te geven. De dimensie moet overeenkomen met uw model: bijvoorbeeld 1536 voor OpenAI text-embedding-3-small, of 768 voor veel open source modellen.

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 maakt een graafindex aan, waarmee snelle benaderende zoekopdrachten mogelijk zijn, zelfs als uw collectie groot wordt. Dit wordt aanbevolen voor collecties groter dan enkele duizenden vectoren. Voor kleine collecties kunt u FLAT gebruiken voor een exacte, brute-force zoekopdracht.

COSINE is doorgaans de beste afstandsmaat voor tekstembeddings. Ook L2 en IP worden ondersteund.

Een K-nearest neighbors (KNN) query geeft de vector als parameter door, als ruwe little-endian float32 bytes:

FT.SEARCH chunksIndex "*=>[KNN 5 @embedding $queryVector]"
  PARAMS 2 queryVector "<rawBytes>"

Om vector search te combineren met filters, vervangt u eenvoudigweg de * door uw filterexpressie:

FT.SEARCH chunksIndex "@documentId:{doc42}=>[KNN 5 @embedding $queryVector]"
  PARAMS 2 queryVector "<rawBytes>"

Valkey gebruikt hetzelfde protocol als Redis, dus u kunt elke Redis-compatibele client gebruiken.

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. Maak de index één keer aan bij het opstarten
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. Indexeer uw segmenten (embedding komt van uw modelprovider)
async function indexChunk({ id, documentId, content, embedding }) {
  await client.hSet(`chunk:${id}`, {
    content,
    documentId,
    embedding: toBytes(embedding)
  });
}

// 3. Haal de segmenten op die het dichtst bij een vraag liggen, beperkt tot één 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. Geef deze door aan uw model als context
const context = await retrieve({ questionEmbedding, documentId: 'doc42' });
const prompt = `Beantwoord uitsluitend met deze context:\n\n${context.join('\n\n')}\n\nVraag: ${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_VALKEY_URL_TLS'])

# Maak de index één keer aan bij het opstarten
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]

Vectoren worden in het geheugen opgeslagen, dus het is belangrijk om rekening te houden met hun geheugenverbruik. De ruwe schatting voor float32-vectoren is:

numberOfVectors x dimensions x 4 bytes, plus ongeveer 30 tot 50 procent extra voor de HNSW-graaf.

Bijvoorbeeld: één miljoen vectoren van 1536 dimensies vereisen ongeveer 6 GB voor de ruwe vectoren. Een 20 GB-abonnement is een comfortabele start. U kunt het geheugengebruik verminderen door een model met minder dimensies te kiezen: een model met 768 dimensies gebruikt slechts de helft van het geheugen van een 1536-dimensie model.

U kunt het daadwerkelijke geheugengebruik controleren met FT.INFO chunksIndex en met de used_memory-metriek in uw Prometheus monitoring. Als u meer geheugen nodig heeft, kunt u uw abonnement op elk moment upgraden via uw dashboard.

  • Indexeren van bestaande keys gebeurt op de achtergrond. U kunt de voortgang volgen met FT.INFO. Zoekopdrachten geven gedeeltelijke resultaten terug totdat het backfill-proces is voltooid.
  • De search-module implementeert een gerichte set van FT.*-commando's. Voor vector search zijn alle essentiële functies aanwezig: HNSW en exacte KNN, hybride filtering en FT.AGGREGATE.

Zie voor een volledige referentie de Valkey search documentatie.