Valkey: Wyszukiwanie wektorowe i RAG
Wyszukiwanie semantyczne, rekomendacje i retrieval-augmented generation z Valkey
👋 Witamy w dokumentacji Stackhero!
Stackhero oferuje gotowe do użycia rozwiązanie Valkey cloud, które zapewnia szereg korzyści, w tym:
- Wbudowany web UI
Valkey Admin.- Nieograniczony rozmiar i transfer wiadomości.
- Bezproblemowe aktualizacje jednym kliknięciem.
- 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 Valkey cloud hosting od Stackhero!
Jeśli budujesz wyszukiwanie semantyczne, silnik rekomendacji lub pipeline retrieval-augmented generation (RAG), musisz przechowywać embeddingi i szybko znajdować najbliższe dopasowania dla zapytania. Stackhero dla Valkey obsługuje to natywnie: moduł wyszukiwania indeksuje wektory i odpowiada na zapytania o podobieństwo w milisekundach. Nie musisz uruchamiać osobnej bazy danych wektorowych obok już używanego serwisu Valkey.
Dwa elementy sprawiają, że to podejście jest praktyczne i wydajne:
- Zapytania hybrydowe. Możesz wykonywać wyszukiwania podobieństwa wektorowego i stosować zwykłe filtry (takie jak zakresy liczbowe, tagi czy tekst) w jednym zapytaniu. Na przykład możesz pobrać „5 fragmentów najbliższych temu pytaniu, ale tylko z dokumentów, do których ten użytkownik ma dostęp” za pomocą jednego zapytania, a nie dwóch.
- Mniej elementów do zarządzania. Twoje embeddingi są przechowywane razem z danymi cache i sesji, korzystając z tych samych poświadczeń, kopii zapasowych i narzędzi monitorujących. Upraszcza to Twój stack i ułatwia zarządzanie.
Zanim zaczniesz
Włącz moduł Search (oraz JSON, jeśli chcesz przechowywać dokumenty jako JSON) w sekcji Modules konfiguracji usługi na dashboardzie Stackhero. Szczegóły znajdziesz w przewodniku search i JSON.
Tworzenie indeksu wektorowego
Zadeklaruj pole wektorowe, określając jego wymiar i metrykę odległości. Wymiar powinien odpowiadać Twojemu modelowi: na przykład 1536 dla OpenAI text-embedding-3-small lub 768 dla wielu modeli 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 tworzy indeks grafowy, zapewniając szybkie przybliżone wyszukiwanie nawet przy dużych kolekcjach. Jest to zalecane dla kolekcji większych niż kilka tysięcy wektorów. Dla małych kolekcji możesz użyć FLAT do dokładnego, brute-force'owego wyszukiwania.
COSINE to zazwyczaj najlepsza metryka odległości dla embeddingów tekstowych. Obsługiwane są także metryki L2 i IP.
Wyszukiwanie
Zapytanie K-nearest neighbors (KNN) przekazuje wektor jako parametr, używając surowych bajtów float32 w formacie little-endian:
FT.SEARCH chunksIndex "*=>[KNN 5 @embedding $queryVector]"
PARAMS 2 queryVector "<rawBytes>"
Aby połączyć wyszukiwanie wektorowe z filtrami, po prostu zamień * na wyrażenie filtrujące:
FT.SEARCH chunksIndex "@documentId:{doc42}=>[KNN 5 @embedding $queryVector]"
PARAMS 2 queryVector "<rawBytes>"
Pipeline RAG w Node.js
Valkey używa tego samego protokołu co Redis, więc możesz korzystać z dowolnego klienta kompatybilnego z 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. Utwórz indeks raz, przy starcie
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]`,
{ PARAMS: { queryVector: toBytes(questionEmbedding) } }
);
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_VALKEY_URL_TLS'])
# Utwórz indeks raz, przy starcie
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]
Dobór rozmiaru usługi
Wektory są przechowywane w pamięci RAM, dlatego ważne jest, aby zaplanować ich zużycie. Przybliżone oszacowanie dla wektorów float32 to:
numberOfVectors x dimensions x 4 bajty, plus około 30 do 50 procent dodatkowo na graf HNSW.
Na przykład milion wektorów o wymiarze 1536 wymaga około 6 GB na surowe wektory. Plan 20 GB to komfortowy punkt wyjścia. Możesz zmniejszyć zużycie pamięci, wybierając model o mniejszej liczbie wymiarów: model 768-wymiarowy zużywa tylko połowę pamięci modelu 1536-wymiarowego.
Rzeczywiste zużycie pamięci możesz sprawdzić poleceniem FT.INFO chunksIndex oraz metryką used_memory w swoim monitoringu Prometheus. Jeśli potrzebujesz więcej pamięci, możesz w każdej chwili zwiększyć plan z poziomu dashboardu.
Warto wiedzieć
- Indeksowanie istniejących kluczy odbywa się w tle. Postęp możesz śledzić za pomocą
FT.INFO. Wyszukiwania będą zwracać częściowe wyniki, dopóki proces uzupełniania nie zostanie zakończony. - Moduł wyszukiwania implementuje wybrany zestaw poleceń
FT.*. Dla wyszukiwania wektorowego obejmuje wszystko, co najważniejsze: HNSW i dokładne KNN, filtrowanie hybrydowe orazFT.AGGREGATE.
Pełną dokumentację znajdziesz w dokumentacji Valkey search.