Redis®*: Wyszukiwanie wektorowe i RAG
Wyszukiwanie semantyczne, rekomendacje i retrieval-augmented generation z Redis
👋 Witamy w dokumentacji Stackhero!
Stackhero oferuje gotowe do użycia rozwiązanie Redis cloud, które zapewnia szereg korzyści, w tym:
- Wbudowany web UI
Redis Commander.- Nieograniczona wielkość i transfer wiadomości.
- Bezproblemowe aktualizacje za pomocą jednego kliknięcia.
- Optymalna wydajność i wysoki poziom bezpieczeństwa dzięki prywatnej, dedykowanej infrastrukturze.
Oszczędzaj czas i uprość sobie życie: wystarczy 5 minut, aby przetestować rozwiązanie hostingu Redis cloud od Stackhero!
Jeśli budujesz wyszukiwanie semantyczne, silnik rekomendacji lub pipeline retrieval-augmented generation (RAG), potrzebujesz niezawodnego sposobu na przechowywanie embeddingów i szybkie znajdowanie najbliższych dopasowań do zapytania. Twoja instancja Redis na Stackhero zapewnia to natywnie: silnik zapytań indeksuje wektory i odpowiada na wyszukiwania podobieństwa w milisekundach. Nie musisz dodawać osobnej bazy danych wektorowych obok Redis – wszystko działa w jednym miejscu.
To podejście jest zarówno wydajne, jak i efektywne dzięki dwóm kluczowym cechom:
- Zapytania hybrydowe. Możesz łączyć wyszukiwanie podobieństwa wektorowego ze standardowymi filtrami, takimi jak zakresy liczbowe, tagi czy pełnotekstowe, wszystko w jednym zapytaniu. Na przykład: „5 fragmentów najbliższych temu pytaniu, ale tylko z dokumentów, do których ten użytkownik ma dostęp, opublikowanych po styczniu” – to wszystko można obsłużyć jednym zapytaniem, bez konieczności wykonywania kilku.
- Uproszczona architektura. Embeddingi, cache i sesje współistnieją, współdzieląc dane dostępowe, kopie zapasowe i monitoring. To zmniejsza złożoność i narzut administracyjny.
Zanim zaczniesz
Włącz moduł Search (oraz JSON, jeśli planujesz przechowywać dokumenty w formacie JSON) w sekcji Modules konfiguracji usługi na panelu Stackhero. Szczegółowe instrukcje znajdziesz w przewodniku Search i JSON.
Tworzenie indeksu wektorowego
Aby zadeklarować pole wektorowe, określ jego wymiar oraz metrykę odległości. Wymiar powinien odpowiadać Twojemu modelowi embeddingów: na przykład 1536 dla OpenAI text-embedding-3-small, 768 dla wielu modeli open-source itd.
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 tworzy indeks grafowy, który utrzymuje szybkie zapytania nawet przy rosnącej kolekcji, co czyni go dobrym wyborem powyżej kilku tysięcy wektorów. Dla małych kolekcji, gdzie liczy się dokładność, możesz użyć FLAT do wyszukiwania brute-force.
COSINE to zalecana metryka dla większości modeli embeddingów tekstowych. Możesz również wybrać L2 lub IP, jeśli wymaga tego Twój przypadek użycia.
Zapytania
Zapytanie K nearest neighbors (KNN) wygląda następująco. Wektor przekazywany jest jako surowe bajty float32 w formacie little-endian:
FT.SEARCH chunksIndex "*=>[KNN 5 @embedding $queryVector AS score]"
PARAMS 2 queryVector "<rawBytes>"
SORTBY score
RETURN 2 content score
DIALECT 2
Aby połączyć wyszukiwanie podobieństwa z filtrami, po prostu zamień * na wyrażenie filtrujące:
FT.SEARCH chunksIndex "(@documentId:{doc42} @publishedAt:[1735689600 +inf])=>[KNN 5 @embedding $queryVector AS score]"
PARAMS 2 queryVector "<rawBytes>"
SORTBY score
DIALECT 2
Zapytania wektorowe wymagają
DIALECT 2. Jeśli to pominiesz, zapytanie użyje starszej składni i zwróci błąd.
Pipeline RAG w Node.js
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. Utwórz indeks raz, przy starcie aplikacji
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. Indeksuj swoje fragmenty (embedding pochodzi od Twojego dostawcy modelu)
async function indexChunk({ id, documentId, content, embedding }) {
await client.hSet(`chunk:${id}`, {
content,
documentId,
embedding: toBytes(embedding)
});
}
// 3. Pobierz fragmenty najbliższe pytaniu, ograniczone do jednego dokumentu
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. Przekaż je do swojego modelu jako kontekst
const context = await retrieve({ questionEmbedding, documentId: 'doc42' });
const prompt = `Odpowiedz używając wyłącznie tego kontekstu:\n\n${context.join('\n\n')}\n\nPytanie: ${question}`;
To samo w Pythonie
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'])
# Utwórz indeks raz, przy starcie aplikacji
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]
Dobór rozmiaru usługi
Wektory są przechowywane w pamięci RAM. Możesz oszacować zapotrzebowanie na pamięć według wzoru:
numberOfVectors x dimensions x 4 bytes, plus około 30–50% dodatkowo na graf HNSW.
Na przykład milion wektorów o wymiarze 1536 zajmuje około 6 GB na surowe wektory. Plan 20 GB to komfortowy punkt wyjścia dla tej skali. Możesz zmniejszyć zużycie pamięci poprzez:
- Użycie mniejszego modelu. Na przykład model o 768 wymiarach zużywa połowę pamięci w porównaniu do modelu o 1536 wymiarach.
- Przechowywanie w
FLOAT16zamiastFLOAT32, jeśli Twój model to obsługuje, co ponownie zmniejsza zużycie o połowę.
Możesz monitorować rzeczywiste zużycie za pomocą FT.INFO chunksIndex oraz metryki used_memory w swoim monitoringu Prometheus. Jeśli Twoje potrzeby rosną, podniesienie planu jest proste i możliwe bezpośrednio z panelu.
Warto wiedzieć
- Indeksy wektorowe działają tylko na bazie danych 0, tak jak wszystkie indeksy wyszukiwania w Redis.
- Zbiory wektorowe to prostsza alternatywa bez konieczności konfiguracji. Polecenia
VADDiVSIMsą wbudowane w Redis, więc możesz korzystać z wyszukiwania podobieństwa natychmiast, bez aktywowania modułów. To wygodne rozwiązanie dla prostych scenariuszy rekomendacji na jednym zbiorze. Silnik zapytań jest lepszym wyborem, gdy potrzebujesz filtrów, paginacji lub wielu pól. - Budowanie indeksu na istniejących kluczach odbywa się w tle. Postęp możesz sprawdzić poleceniem
FT.INFO. Wyszukiwania zwracają częściowe wyniki do czasu zakończenia indeksowania.
Aby dowiedzieć się więcej o wyszukiwaniu wektorowym w Redis, zobacz oficjalną dokumentację Redis vector search.