Valkey: Vektorsuche und RAG

Semantische Suche, Empfehlungen und Retrieval-Augmented Generation mit Valkey

👋 Willkommen in der Stackhero-Dokumentation!

Stackhero bietet eine einsatzbereite Valkey Cloud-Lösung mit zahlreichen Vorteilen, darunter:

  • Valkey Admin Web-Oberfläche inklusive.
  • Unbegrenzte Nachrichten­größe und Übertragungen.
  • Updates ganz einfach per Klick.
  • Optimale Performance und starke Sicherheit durch eine private, dedizierte Infrastruktur.

Sparen Sie Zeit und vereinfachen Sie Ihren Alltag: Es dauert nur 5 Minuten, um die Valkey Cloud Hosting-Lösung von Stackhero auszuprobieren!

Wenn Sie eine semantische Suche, eine Empfehlungs-Engine oder eine Retrieval-Augmented Generation (RAG) Pipeline entwickeln, müssen Sie Embeddings speichern und schnell die nächsten Übereinstimmungen zu einer Anfrage finden. Stackhero für Valkey unterstützt dies nativ: Das Search-Modul indexiert Vektoren und beantwortet Ähnlichkeitsanfragen in Millisekunden. Sie müssen keine separate Vektor-Datenbank neben dem bereits genutzten Valkey-Service einrichten.

Zwei Funktionen machen diesen Ansatz praktisch und effizient:

  • Hybride Abfragen. Sie können Vektor-Ähnlichkeitssuchen durchführen und gleichzeitig reguläre Filter (wie numerische Bereiche, Tags oder Text) in einer einzigen Anfrage anwenden. Zum Beispiel können Sie „die 5 Chunks, die dieser Frage am nächsten sind, aber nur aus Dokumenten, die dieser Benutzer lesen darf“ mit einer einzigen Abfrage abrufen – nicht mit zwei.
  • Eine Komponente weniger. Ihre Embeddings werden zusammen mit Cache- und Sitzungsdaten gespeichert, unter Verwendung derselben Zugangsdaten, Backups und Monitoring-Tools. Das hält Ihren Stack schlanker und vereinfacht das Management.

Aktivieren Sie das Modul Search (und JSON, falls Sie Dokumente als JSON speichern möchten) im Bereich Modules Ihrer Service-Konfiguration auf dem Stackhero Dashboard. Weitere Details finden Sie im Search- und JSON-Guide.

Deklarieren Sie ein Vektorfeld, indem Sie die Dimension und die Distanzmetrik angeben. Die Dimension muss zu Ihrem Modell passen: Zum Beispiel 1536 für OpenAI text-embedding-3-small oder 768 für viele Open-Source-Modelle.

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 erstellt einen Graph-Index, der auch bei großen Sammlungen schnelle, approximative Suchen ermöglicht. Dies wird für Sammlungen mit mehr als einigen Tausend Vektoren empfohlen. Für kleine Sammlungen können Sie FLAT für eine exakte, brute-force Suche verwenden.

COSINE ist in der Regel die beste Distanzmetrik für Text-Embeddings. Auch die Metriken L2 und IP werden unterstützt.

Eine K-nearest neighbors (KNN) Abfrage übergibt den Vektor als Parameter, als rohe little-endian float32 Bytes:

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

Um Vektorsuche mit Filtern zu kombinieren, ersetzen Sie einfach das * durch Ihren Filterausdruck:

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

Valkey verwendet dasselbe Protokoll wie Redis, daher können Sie jeden Redis-kompatiblen Client nutzen.

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. Erstellen Sie den Index einmalig beim Start
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. Indexieren Sie Ihre Chunks (Embedding kommt von Ihrem Modell-Provider)
async function indexChunk({ id, documentId, content, embedding }) {
  await client.hSet(`chunk:${id}`, {
    content,
    documentId,
    embedding: toBytes(embedding)
  });
}

// 3. Rufen Sie die Chunks ab, die einer Frage am nächsten sind, beschränkt auf ein Dokument
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. Geben Sie diese als Kontext an Ihr Modell weiter
const context = await retrieve({ questionEmbedding, documentId: 'doc42' });
const prompt = `Antworten Sie ausschließlich mit diesem Kontext:\n\n${context.join('\n\n')}\n\nFrage: ${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'])

# Erstellen Sie den Index einmalig beim Start
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]

Vektoren werden im Arbeitsspeicher gehalten, daher ist es wichtig, deren Speicherbedarf einzuplanen. Die grobe Schätzung für float32-Vektoren lautet:

numberOfVectors x dimensions x 4 bytes, plus etwa 30 bis 50 Prozent zusätzlich für den HNSW-Graphen.

Beispielsweise benötigen eine Million Vektoren mit 1536 Dimensionen etwa 6 GB für die Rohdaten. Ein 20-GB-Plan ist ein komfortabler Ausgangspunkt. Sie können den Speicherbedarf reduzieren, indem Sie ein Modell mit weniger Dimensionen wählen: Ein Modell mit 768 Dimensionen benötigt nur halb so viel Speicher wie eines mit 1536 Dimensionen.

Sie können den tatsächlichen Speicherverbrauch mit FT.INFO chunksIndex und mit der Metrik used_memory in Ihrem Prometheus Monitoring überprüfen. Falls Sie mehr Speicher benötigen, können Sie Ihren Plan jederzeit über das Dashboard upgraden.

  • Das Indexieren bestehender Keys läuft im Hintergrund. Sie können den Fortschritt mit FT.INFO verfolgen. Suchen liefern solange Teilergebnisse, bis das Backfilling abgeschlossen ist.
  • Das Search-Modul implementiert einen fokussierten Satz an FT.*-Befehlen. Für die Vektorsuche sind alle wesentlichen Funktionen abgedeckt: HNSW und exakte KNN-Suche, hybride Filterung und FT.AGGREGATE.

Eine vollständige Referenz finden Sie in der Valkey Search Dokumentation.