Baza wektorowa na VPS: pgvector, Qdrant czy Chroma?
Porównanie wydajności pgvector, Qdrant i Chroma przy hostingu na własnym VPS. Dowiedz się, jak obliczyć zapotrzebowanie na pamięć RAM i uniknąć błędów przy indeksowaniu.
Rzeczywiste koszty utrzymania bazy wektorowej na VPS
Uruchomienie bazy wektorowej na serwerze VPS (virtual private server) eliminuje problem, którego rozwiązanie oferują wszyscy dostawcy usług zarządzanych. Aplikacja oraz indeks znajdują się na tej samej maszynie, więc żądanie wyszukiwania przechodzi przez gniazdo loopback zamiast przez sieć. Pozostaje koszt, który zawsze był tym rzeczywistym: przekształcanie tekstu na wektory. Za nim kryją się dwa kolejne: czas budowania indeksu oraz pamięć RAM, którą indeks zajmuje przez cały czas działania.
Zmienia to priorytety podejmowanych decyzji. Region oraz czas odpowiedzi punktu końcowego (round trip) przestają być istotne. Kluczowym parametrem staje się iloczyn liczby wektorów, liczby wymiarów oraz czterech bajtów, ponieważ to on decyduje, czy indeks zmieści się w pamięci RAM opłacanej co miesiąc.
Gdzie faktycznie znikają milisekundy na pojedynczej maszynie
Prześledźmy zapytanie o podobieństwo w stosie technologicznym hostowanym samodzielnie.
- Tekst zapytania jest zamieniany na wektor przez model embeddingowy. Na procesorze CPU zajmuje to od dziesiątek do setek milisekund dla krótkiego ciągu znaków. Na układzie GPU czas ten wynosi pojedyncze milisekundy.
- Wektor jest przesyłany do magazynu danych. Przez interfejs loopback TCP lub gniazdo domenowe Unix (Unix domain socket) zajmuje to ułamek milisekundy.
- Magazyn przeszukuje swój indeks i zwraca najbliższe wiersze.
- Kod odczytuje dopasowany tekst i składa prompt.
Krok 1 zazwyczaj generuje największą wartość w tym zestawieniu. Krok 2 jest obszarem, w którym konkurują dostawcy usług chmurowych, jednak na pojedynczej maszynie praktycznie nie istnieje. Nie należy zgadywać podziału czasu. Należy zmierzyć oba końce na własnym serwerze.
curl http://127.0.0.1:11434/api/embed -s -o /dev/null \
-w 'embed: %{time_total}s\n' \
-d '{"model": "nomic-embed-text", "input": "how do I rotate the api key"}'Następnie należy uruchomić \timing on w psql przed zapytaniem wyszukiwania. Jeśli pierwsze polecenie wyświetli embed: 0.184312s, a psql odpowie Time: 4.201 ms, to optymalizacja indeksu jest błędnym kierunkiem: opóźnienie wynika z działania modelu embeddingowego. Uruchamianie modelu embeddingowego lokalnie za pomocą Ollama umieszcza krok 1 na tym samym procesorze CPU co kroki 2 i 3, więc obie połowy rywalizują o te same rdzenie i tę samą pamięć RAM. Pętla ingestii i wyszukiwania otaczająca ten magazyn została opisana w przewodniku po potoku RAG hostowanym samodzielnie. RAG to retrieval augmented generation (generowanie wspomagane wyszukiwaniem): przeszukujesz własne dokumenty i wklejasz najlepsze dopasowania do promptu.
Przy liczbie wektorów poniżej stu tysięcy, przeskanuj wszystkie
Wyszukiwanie wyczerpujące porównuje zapytanie z każdym zapisanym wektorem. Kompletność (recall) jest z definicji idealna. Metoda ta nie wymaga indeksu ani etapu budowania i nie może stać się nieaktualna względem danych.
Arytmetyka wskazuje moment, w którym to rozwiązanie przestaje być wydajne. Skanowanie odczytuje n * d * 4 bajtów na zapytanie, gdzie n to liczba wektorów, a d to wymiarowość. Dla 100 000 wektorów o 768 wymiarach daje to 307 MB na zapytanie, co współczesny procesor przetwarza w kilkadziesiąt milisekund. Przy 5 milionach wektorów jest to 15 GB na zapytanie, co przestaje być zapytaniem w czasie rzeczywistym.
Dlatego przechowuj wektory w SQLite, a porównania wykonuj w NumPy.
import sqlite3, numpy as np
db = sqlite3.connect("docs.db")
db.execute("CREATE TABLE IF NOT EXISTS docs (id INTEGER PRIMARY KEY, body TEXT, vec BLOB)")
def add(body, vec):
v = np.asarray(vec, dtype=np.float32)
v /= np.linalg.norm(v)
db.execute("INSERT INTO docs (body, vec) VALUES (?, ?)", (body, v.tobytes()))
db.commit()
def search(query_vec, k=5):
rows = db.execute("SELECT id, body, vec FROM docs").fetchall()
mat = np.frombuffer(b"".join(r[2] for r in rows), dtype=np.float32).reshape(len(rows), -1)
q = np.asarray(query_vec, dtype=np.float32)
q /= np.linalg.norm(q)
scores = mat @ q
return [(rows[i][0], rows[i][1], float(scores[i])) for i in np.argsort(-scores)[:k]]Obie strony są skalowane do jednostkowej długości, więc iloczyn skalarny jest podobieństwem cosinusowym, a wyższy wynik oznacza lepsze dopasowanie. Wczytaj mat raz podczas uruchamiania, zamiast przy każdym zapytaniu, a odczyt z SQLite całkowicie zniknie z krytycznej ścieżki wykonania.
Przetestuj wydajność na własnym sprzęcie, zanim odrzucisz to rozwiązanie.
import time
t = time.perf_counter()
search(q)
print(f"{(time.perf_counter() - t) * 1000:.1f} ms")Realne ograniczenia: jest to jeden proces utrzymujący całą macierz w pamięci RAM, który nie oferuje filtrowania metadanych ani obsługi współbieżnych zapisów. Gdy którykolwiek z tych punktów stanie się problemem, zmień rozwiązanie. Samo SQLite jest poważnym magazynem danych po stronie serwera, co opisuje przewodnik o SQLite w środowisku produkcyjnym, a jeśli rzeczywiste obciążenie polega na skanowaniu kolumn, a nie serwowaniu wierszy, bardziej przydatna będzie lektura porównania DuckDB i SQLite.
pgvector, gdy korzystasz już z Postgres
Jeśli aplikacja korzysta już z bazy danych Postgres, pgvector wprowadza najmniejszą możliwą powierzchnię ataku. Jest to rozszerzenie, a nie osobna usługa. Wektory znajdują się w zwykłej tabeli obok wiersza, który opisują, więc wyszukiwanie z filtrowaniem jest klauzulą WHERE, a nie drugim systemem wymagającym synchronizacji.
Ubuntu 24.04 zawiera to rozszerzenie w komponencie universe.
sudo apt update
sudo apt install -y postgresql-16-pgvector
sudo -u postgres psql -d yourdb -c 'CREATE EXTENSION vector;'Ten pakiet to pgvector w wersji 0.6.0 (stan na sierpień 2026), co stanowi znaczące opóźnienie względem wersji upstream. W szczególności iteracyjne skanowanie indeksów wymaga wersji 0.8, dlatego należy pobierać je z oficjalnego repozytorium projektu PostgreSQL.
sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt install -y postgresql-17-pgvectorZastąp 17 główną wersją swojego serwera, którą wyświetla polecenie sudo -u postgres psql -tAc 'SHOW server_version'. Instalacja pakietu rozszerzenia zbudowanego dla niewłaściwej wersji głównej powoduje błąd CREATE EXTENSION, ponieważ Postgres przeszukuje katalog share wyłącznie dla uruchomionej wersji:
ERROR: could not open extension control file "/usr/share/postgresql/16/extension/vector.control": No such file or directorySchemat to standardowy SQL z jednym nowym typem danych.
CREATE TABLE chunks (
id bigserial PRIMARY KEY,
doc_id bigint NOT NULL,
body text NOT NULL,
embedding vector(768)
);
SELECT id, body FROM chunks
ORDER BY embedding <=> '[0.013, -0.021, 0.004]'
LIMIT 5;<=> to odległość cosinusowa, <-> to odległość L2 (euklidesowa), a <#> to ujemny iloczyn skalarny. Należy użyć typu, dla którego wytrenowano model osadzeń (embedding model). Wybór niewłaściwego typu nie powoduje błędu, a jedynie pogorszenie jakości wyników.
Bez indeksu to zapytanie stanowi dokładne wyszukiwanie w każdym wierszu, co jest odpowiednikiem powyższego brute force w Postgresie i zapewnia taką samą pełną skuteczność (recall). Zwiększenie max_parallel_workers_per_gather pozwala zaangażować więcej rdzeni procesora. Wykonaj to w pierwszej kolejności, a indeksowanie później, ponieważ dzięki temu uzyskasz bazowy poziom skuteczności, do którego będzie można odnieść wyniki indeksu.
Qdrant, gdy indeks przerasta bazę danych
Qdrant to dedykowana baza wektorowa napisana w języku Rust. Zyskuje na znaczeniu, gdy indeks jest na tyle duży, że jego budowanie nie powinno konkurować z zasobami Postgres używanymi przez aplikację, lub gdy wymagane jest filtrowanie metadanych i kwantyzacja, których pgvector nie oferuje.
docker run -d --name qdrant \
-p 127.0.0.1:6333:6333 -p 127.0.0.1:6334:6334 \
-e QDRANT__SERVICE__API_KEY="$(openssl rand -hex 32)" \
-v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
qdrant/qdrantPort 6333 obsługuje API REST (representational state transfer) oraz panel sterowania pod adresem /dashboard, natomiast port 6334 obsługuje gRPC. W przypadku publicznego VPS istotne są dwa szczegóły. Dokumentacja Qdrant wskazuje, że usługa domyślnie działa "bez szyfrowania i uwierzytelniania", a konfiguracja -p 6333:6333 z przewodnika szybkiego startu wiąże się z każdym interfejsem. Docker publikuje ten port z pominięciem reguł ufw, ponieważ samodzielnie dopisuje reguły przekierowania. Należy powiązać usługę z 127.0.0.1 i ustawić klucz API. Instancja Qdrant dostępna na publicznym adresie IP bez klucza oznacza publiczny dostęp do przechowywanych dokumentów.
curl -s http://127.0.0.1:6333/collections -H "api-key: $QDRANT_API_KEY"Poprawna odpowiedź wygląda jak {"result":{"collections":[]},"status":"ok","time":0.00002}. Otrzymanie {"status":{"error":"Unauthorized"}} oznacza błędną nazwę nagłówka lub nieprawidłowy klucz, natomiast brak odpowiedzi oznacza, że kontener nie działa lub jest powiązany z innym adresem. Kwestia tego, czy dany kontener jest odpowiedni dla danej infrastruktury, to standardowe pytanie dotyczące usług stanowych, dlatego porównanie Docker kontra baza danych na hoście ma tutaj pełne zastosowanie.
Chroma i jej przeznaczenie
Chroma to najkrótsza droga od zera do działającej demonstracji wyszukiwania (retrieval).
pip install chromadb
chroma run --path /srv/chromaUsługa działa na porcie 8000, a chromadb.HttpClient(host="localhost", port=8000) łączy się z nią. Chroma dostarcza domyślną funkcję embeddingu, więc pierwszy prototyp nie wymaga osobnego serwera modelu.
Należy uczciwie ocenić ten kompromis. Chroma jest wygodna, ponieważ ukrywa decyzje, których dotyczy ten przewodnik: wybór metryki odległości oraz ilość pamięci RAM potrzebnej do przechowywania wyników. Jest to poprawne podejście w przypadku prototypu, ale błędne dla systemu, za który otrzymuje się powiadomienia o awariach (on-call). Jeśli dane znajdują się już w Postgres, przeniesienie ich do Chroma dodaje kolejny proces i problem synchronizacji, aby rozwiązać kwestię, której pgvector nie posiada.
Ile pamięci RAM będzie potrzebował indeks
Należy zacząć od surowych wektorów, ponieważ stanowią one podstawę, której nie zmieni żadna optymalizacja.
bytes = number_of_vectors * dimensions * 4Cztery bajty odpowiadają jednej 32-bitowej liczbie zmiennoprzecinkowej na wymiar. Dokumentacja planowania pojemności Qdrant dodaje mnożnik 1.5 dla metadanych oraz tymczasowych segmentów tworzonych podczas optymalizacji:
memory_size = number_of_vectors * vector_dimension * 4 bytes * 1.5Poniżej przedstawiono tę formułę zastosowaną dla miliona wektorów przy wymiarach generowanych przez rzeczywiste modele osadzeń (embedding models).
The data behind this chart
[
{
"label": "384 dims",
"raw_gib": 1.43,
"with_overhead_gib": 2.15
},
{
"label": "768 dims",
"raw_gib": 2.86,
"with_overhead_gib": 4.29
},
{
"label": "1024 dims",
"raw_gib": 3.81,
"with_overhead_gib": 5.72
},
{
"label": "1536 dims",
"raw_gib": 5.72,
"with_overhead_gib": 8.58
},
{
"label": "3072 dims",
"raw_gib": 11.44,
"with_overhead_gib": 17.17
}
]Są to wyniki obliczeń w gibibajtach (GiB), a nie pomiary. Należy je traktować jako ilość pamięci, którą trzeba zarezerwować. Model o 768 wymiarach, taki jak nomic-embed-text, dla miliona fragmentów wymaga około 4.29 GiB, co mieści się w planie 8 GB z zapasem dla Postgres. Ten sam korpus przy 3072 wymiarach wymaga 17.17 GiB i nie mieści się w tym limicie.
Kluczowym czynnikiem jest pierwsza kolumna tabeli, a nie ostatnia. Zmniejszenie liczby wymiarów o połowę redukuje o połowę liczbę bajtów na każdym kolejnym etapie. Model o 768 wymiarach, który uzyskuje nieco gorsze wyniki w publicznych rankingach, jest często lepszym wyborem inżynieryjnym w przypadku VPS. Typ halfvec w pgvector przechowuje liczby 16-bitowe, co ponownie zmniejsza liczbę bajtów o połowę, a indeksowanie odbywa się poprzez wyrażenie:
CREATE INDEX ON chunks USING hnsw ((embedding::halfvec(768)) halfvec_cosine_ops);Oto ograniczenie, o którym należy wiedzieć przed wyborem modelu. Typ vector w pgvector obsługuje do 16 000 wymiarów, ale jego indeksy HNSW oraz IVFFlat obejmują tylko 2 000. Powyżej tej wartości indeksuje się rzutowanie halfvec, które osiąga 4 000, lub rezygnuje z indeksowania.
Czym są m oraz ef_construction w czasie budowania indeksu
HNSW (hierarchical navigable small world) to indeks, z którego korzystają zarówno pgvector, jak i Qdrant. Jest to graf warstwowy. Każdy wektor jest węzłem z połączeniami do pobliskich węzłów, a wyszukiwanie przeskakuje po tych połączeniach w kierunku zapytania zamiast odczytywać wszystkie dane.
m określa, ile połączeń utrzymuje każdy węzeł. Dokumentacja Faiss podaje, że pamięć HNSW wynosi (d * 4 + m * 2 * 4) bajtów na wektor i zaleca utrzymywanie m w zakresie od 4 do 64. Uruchom to przy 768 wymiarach dla miliona wektorów.
The data behind this chart
[
{
"label": "m = 8",
"link_bytes_per_vector": 64,
"total_gib": 2.92
},
{
"label": "m = 16 (default)",
"link_bytes_per_vector": 128,
"total_gib": 2.98
},
{
"label": "m = 32",
"link_bytes_per_vector": 256,
"total_gib": 3.1
},
{
"label": "m = 64",
"link_bytes_per_vector": 512,
"total_gib": 3.34
}
]Użytecznym zaskoczeniem jest wielkość tej różnicy. Przejście z wartości domyślnej m = 16 na m = 64 dodaje 512 bajtów połączeń na wektor przy 3072 bajtach danych wektorowych, więc suma zmienia się z 2.98 GiB na 3.34 GiB. To około 12 procent. Przy tych wymiarach m nie jest miejscem, w którym znika pamięć. Są nim wektory.
To, co faktycznie kosztuje m, to czas budowania i wstawiania, ponieważ umieszczenie węzła oznacza znalezienie i połączenie tylu sąsiadów. ef_construction to rozmiar listy kandydatów, którą budowniczy rozważa podczas umieszczania każdego węzła. Zwiększenie tej wartości tworzy lepszy graf i wolniejszą budowę, ale w ogóle nie zmienia rozmiaru gotowego indeksu.
SET maintenance_work_mem = '4GB';
SET max_parallel_maintenance_workers = 7;
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);maintenance_work_mem to ustawienie, które decyduje o tym, czy budowa zajmie minuty, czy godziny, ponieważ pgvector składa graf w pamięci, gdy się w niej mieści. Gdy przestaje się mieścić, pgvector informuje o tym:
NOTICE: hnsw graph no longer fits into maintenance_work_mem after 100000 tuples
DETAIL: Building will take significantly more time.
HINT: Increase maintenance_work_mem to speed up builds.Ten komunikat jest najbardziej użyteczną linią, jaką drukuje pgvector. Oznacza on, że proces budowy przeszedł na znacznie wolniejszą ścieżkę, więc należy go anulować, zwiększyć ustawienie powyżej obliczonej wcześniej wartości RAM i rozpocząć ponownie. Monitoruj postęp z drugiej sesji:
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS pct
FROM pg_stat_progress_create_index;HNSW raportuje initializing, a następnie loading tuples. Budowa utrzymująca się przez długi czas na niskim procencie na dysku, który nie jest obciążony, to problem maintenance_work_mem, a nie zawieszone zapytanie.
Warto zaplanować pracę w oparciu o dwa fakty. README pgvector podaje, że HNSW "ma wolniejszy czas budowania i zużywa więcej pamięci" niż IVFFlat, oferując w zamian lepszy kompromis między szybkością a odzyskiwaniem danych (recall). HNSW można utworzyć na pustej tabeli, podczas gdy IVFFlat musi najpierw uruchomić k-means na reprezentatywnych danych, więc budowanie go na pustej tabeli daje słabe wyniki. W przypadku nowego schematu, HNSW jest indeksem, który można utworzyć od razu.
ef_search: parametr dostrajany po zbudowaniu indeksu
m oraz ef_construction są na stałe zapisane w indeksie. ef_search nie jest. Określa on, ile kandydatów przechowuje wyszukiwanie podczas przeszukiwania grafu; wartość tę można zmieniać dla każdej sesji lub zapytania.
SET hnsw.ef_search = 100;
SELECT id, body FROM chunks ORDER BY embedding <=> $1 LIMIT 5;Wartość domyślna to 40. Zwiększenie jej powoduje wzrost skuteczności wyszukiwania (recall), ale także wzrost opóźnień. Zmniejszenie powoduje spadek obu tych parametrów. Jest to jedyny mechanizm kontroli skuteczności, który można zmieniać bez przebudowywania indeksu, dlatego należy dostrajać go w oparciu o ustalony zestaw zapytań, dla których znane są poprawne odpowiedzi, i zatrzymać się w momencie, gdy skuteczność przestaje rosnąć.
Uwaga na jeden problem. ef_search źle współpracuje z selektywną klauzulą WHERE, ponieważ indeks zwraca ustaloną liczbę kandydatów, a filtr jest stosowany dopiero później. Filtr odrzucający większość wierszy może pozostawić użytkownika z liczbą wyników mniejszą niż LIMIT, mimo że pasujące wiersze znajdują się w tabeli. pgvector 0.8 rozwiązuje ten problem za pomocą skanowania iteracyjnego:
SET hnsw.iterative_scan = relaxed_order;Indeks jest wtedy skanowany ponownie w poszukiwaniu kolejnych kandydatów, aż do uzyskania wymaganej liczby wyników, maksymalnie do wartości hnsw.max_scan_tuples, której wartość domyślna wynosi 20000. strict_order zachowuje dokładne sortowanie według odległości i wiąże się z wyższym kosztem obliczeniowym. Jest to funkcja, której nie posiada pakiet w wersji 0.6.0 dla Ubuntu, a brakujące wiersze przy zastosowaniu filtra są sygnałem, że jej brakuje.
Dlaczego indeks musi mieścić się w pamięci RAM
Wyszukiwanie HNSW polega na przechodzeniu po grafie. Każdy przeskok odczytuje węzeł zapisany w miejscu niezwiązanym z poprzednim, więc wzorzec dostępu jest zbliżony do losowego, a mechanizm read-ahead nie przynosi korzyści. Gdy graf znajduje się w pamięci RAM, każdy przeskok jest odwołaniem do pamięci. Gdy go tam nie ma, przeskok może stać się odczytem z dysku, a wyszukiwanie obejmujące kilkaset węzłów zamienia się w kilkaset operacji odczytu.
Dokumentacja Qdrant precyzuje tę zależność: „jeśli w pamięci RAM przechowywana jest połowa liczby wektorów, opóźnienie wyszukiwania wzrośnie mniej więcej dwukrotnie”. Należy planować architekturę w oparciu o to stwierdzenie.
Jeśli indeks faktycznie nie mieści się w pamięci, każda dostępna opcja jest kompromisem, na który należy zdecydować się świadomie.
- Użyj mapowania pamięci (memory-map) dla wektorów, aby system operacyjny buforował aktywne strony, a nieużywane pozostawiał na dysku. Wymaga to szybkiej pamięci masowej NVMe, aby rozwiązanie było akceptowalne pod względem wydajności.
- Zastosuj kwantyzację, przechowując każdy wymiar jako jeden bajt zamiast czterech. Zmniejsza to zapotrzebowanie na pamięć czterokrotnie przy niewielkim, mierzalnym spadku precyzji (recall).
- Użyj rzutowania na
halfvecw pgvector, co zmniejsza liczbę bajtów o połowę przy mniejszej utracie precyzji niż w przypadku kwantyzacji jednobajtowej. - Użyj mniejszego modelu do tworzenia embeddingów. Jest to najtańsze rozwiązanie i jednocześnie takie, które jest często pomijane, ponieważ wymaga ponownego przetworzenia całego korpusu danych.
Tryby awarii i komunikaty błędów
could not open extension control file. Pakiet pgvector dla używanej wersji głównej Postgres nie jest zainstalowany. Wyświetl wersję za pomocą sudo -u postgres psql -tAc 'SHOW server_version' i zainstaluj odpowiedni postgresql-NN-pgvector.
ERROR: expected 768 dimensions, not 1536. Typ kolumny nie zgadza się z modelem. Zmieniono model osadzeń (embedding model) bez ponownego wygenerowania wektorów. Nie istnieje częściowe rozwiązanie tego problemu, ponieważ wektory z dwóch różnych modeli są całkowicie nieporównywalne, co wymusza regenerację każdego wiersza.
Zapytanie działa wolno, a EXPLAIN wykazuje skanowanie sekwencyjne (sequential scan). Klasa operatora indeksu nie pasuje do operatora użytego w zapytaniu. vector_cosine_ops obsługuje wyłącznie <=>. Uruchom EXPLAIN ANALYZE dla zapytania i poszukaj Index Scan using ... on chunks. Jeśli zamiast tego widoczny jest Seq Scan on chunks, przebuduj indeks z klasą operatora zgodną z operatorem używanym w zapytaniu.
Zwrócono mniej wierszy niż wynosi LIMIT, a obecna jest klauzula WHERE. Jest to pułapka filtrowania opisana powyżej. Zwiększ hnsw.ef_search lub przejdź na pgvector 0.8 i ustaw hnsw.iterative_scan.
Budowa indeksu kończy się nagłym przerwaniem procesu bez błędu w psql. Ustawienie maintenance_work_mem na wartość bliską całkowitej pamięci maszyny, przy jednoczesnym zapotrzebowaniu na pamięć ze strony shared_buffers oraz aplikacji, prowadzi do aktywacji mechanizmu OOM Killer w jądrze systemu. sudo dmesg -T | grep -i 'killed process' zawiera wiersz wskazujący na postgres. Obniż ustawienie lub zbuduj indeks na większej instancji i przywróć zrzut bazy danych.
Wybór rozwiązania
Jeśli korzystasz już z Postgres i posiadasz mniej niż kilka milionów wektorów, użyj pgvector. Indeks znajduje się obok danych, filtrowanie odbywa się za pomocą klauzuli WHERE, a istniejąca procedura kopii zapasowych obejmuje również te dane. Jeśli indeks jest na tyle duży, że wymaga własnego limitu pamięci, lub jeśli potrzebujesz zaawansowanego filtrowania ładunku, uruchom Qdrant obok i zaakceptuj konieczność utrzymywania drugiej usługi.
Przy liczbie wektorów poniżej około stu tysięcy, przed instalacją czegokolwiek zmierz wydajność skanowania brute-force. Wyczerpujące przeszukiwanie z pełną precyzją (recall) i bez etapu budowania indeksu nie stanowi kompromisu przy tej skali. Jest to właściwe rozwiązanie, a sięganie po indeks przybliżony oznacza konieczność dostrajania i obciążenie pamięci RAM w zamian za milisekundy, których wcześniej nie tracono.
FAQ
Czy potrzebuję dedykowanej bazy wektorowej, czy wystarczy Postgres?
Jeśli dane już znajdują się w Postgres, pgvector wystarcza na znacznie dłużej, niż sugeruje większość porównań. Przechowuje on wektory w zwykłej kolumnie, więc wyszukiwanie z filtrowaniem to klauzula WHERE, a istniejąca kopia zapasowa obejmuje również indeks. Przejście na dedykowane rozwiązanie, takie jak Qdrant, jest zasadne, gdy obciążenie wektorowe wymaga własnego limitu pamięci lub gdy potrzebne jest filtrowanie ładunku (payload) i kwantyzacja, których pgvector nie oferuje.
Ile wektorów pomieści jeden VPS?
Zamiast zgadywać, należy wykonać obliczenia przy użyciu number_of_vectors * dimensions * 4 bytes * 1.5. Milion wektorów o 768 wymiarach zajmuje około 4.3 GiB, więc plan 8 GB pomieści je z zapasem na Postgres. Milion wektorów o 3072 wymiarach zajmuje około 17 GiB i wymaga znacznie większego planu. Parametrem mającym największy wpływ jest wymiar modelu osadzeń (embedding model), dlatego należy wybierać model z uwzględnieniem kosztów pamięci.
Dlaczego wyszukiwanie wektorowe jest wolne, mimo że indeks znajduje się na tej samej maszynie?
W przypadku jednej maszyny sieć nie jest przyczyną problemu, więc należy sprawdzić dwa inne czynniki. Po pierwsze, zmierz czas samego wywołania osadzenia, ponieważ generowanie wektora zapytania na CPU często trwa znacznie dłużej niż samo wyszukiwanie. Po drugie, sprawdź, czy indeks znajduje się w pamięci RAM. Wyszukiwanie HNSW wykonuje losowe skoki po grafie, więc gdy graf zostanie przeniesiony na dysk, każdy skok może stać się operacją odczytu z dysku. Zgodnie z wytycznymi Qdrant, zmniejszenie o połowę liczby wektorów przechowywanych w RAM mniej więcej podwaja opóźnienie wyszukiwania.
Czy w ogóle powinienem budować indeks HNSW?
Nie przy liczbie mniejszej niż około sto tysięcy wektorów. Wyszukiwanie pełne (exhaustive scan) odczytuje n * d * 4 bajtów na zapytanie, co przy 100 000 wektorów o 768 wymiarach daje 307 MB. Nowoczesny procesor przetwarza taką ilość danych w dziesiątkach milisekund, zapewniając pełną dokładność (recall) bez konieczności budowania indeksu. Najpierw zmierz czas skanowania na własnym sprzęcie. Buduj indeks tylko wtedy, gdy zmierzony czas skanowania jest faktycznie zbyt długi, a nie dlatego, że tak sugeruje artykuł z benchmarkami.
Jaki jest rzeczywisty koszt zwiększenia m?
Kosztuje to głównie czas budowania i wstawiania danych, znacznie bardziej niż pamięć. Przy 768 wymiarach, przejście z wartości domyślnej m = 16 na m = 64 dodaje 512 bajtów linków grafu na wektor w stosunku do 3072 bajtów danych wektora, więc całkowite zużycie pamięci wzrasta o około 12 procent. Jednak każda operacja wstawiania musi znaleźć i połączyć cztery razy więcej sąsiadów. Najpierw dostosuj ef_search, ponieważ zmiana tego parametru nic nie kosztuje i nie wymaga przebudowy indeksu.