pgvector na produkcji: wnioski z praktyki
Nieraz stawałem po stronie pgvectora jako sensownego wyboru domyślnego do wyszukiwania wektorowego. Po jakimś czasie utrzymywania go na produkcji zebrałem to, o czym wprowadzające poradniki milczą: wybór indeksu i jego kompromisy, dostrajanie relacji recall–szybkość, utrzymywanie embeddingów w zgodzie ze źródłem i pułapki operacyjne, które ujawniają się dopiero przy skali.
Nie raz przekonywałem, że pgvector — wektorowe rozszerzenie Postgresa — to naturalny wybór, gdy chcesz dodać wyszukiwanie semantyczne, i że po dedykowaną bazę wektorową powinno się sięgać dopiero wtedy, gdy zmierzone ograniczenie faktycznie tego wymusi. Wciąż tak uważam. Tyle że „użyj pgvectora” to łatwa rada; prawdziwe wnioski płyną z prowadzenia go na produkcji — i to właśnie ten wątek wprowadzenia pomijają. Po jakimś czasie pracy z pgvectorem pod realnym obciążeniem oto, co chciałbym wiedzieć od samego początku: jak wybrać i dostroić indeks, jakiego kompromisu między recall a szybkością nie da się uniknąć, na czym polega dyscyplina utrzymywania embeddingów w zgodzie z danymi źródłowymi oraz jakie realia operacyjne pojawiają się dopiero przy skali. To praktyczna kontynuacja wprowadzenia.
Cała historia wydajności to indeks
Pierwsza i najważniejsza lekcja: bez właściwego indeksu pgvector się nie skaluje, a wybór indeksu jest centralną decyzją. Zapytanie wektorowe bez indeksu wykonuje dokładne wyszukiwanie najbliższego sąsiada — porównuje wektor zapytania z każdym wierszem. Jest to dokładne, ale liniowe względem rozmiaru danych, więc gdy tabela rośnie, bardzo szybko robi się to zdecydowanie zbyt wolne. Rozwiązaniem jest indeks przybliżonego najbliższego sąsiada (ANN), a pgvector oferuje dwa, z realnym kompromisem między nimi:
- HNSW (Hierarchical Navigable Small World). Zasadniczo lepszy wybór pod kątem wydajności zapytań na produkcji — daje doskonałą prędkość i wysokie recall. Ceną jest to, że buduje się wolniej i pochłania więcej pamięci, a sam indeks zajmuje więcej miejsca. Dla większości produkcyjnych obciążeń, w których dominuje odczyt, ten koszt budowy jest wart zapłacenia dla wydajności zapytań.
- IVFFlat. Buduje się szybciej i jest mniejszy, ale zwykle wypada gorzej pod względem wydajności zapytań niż HNSW — a w dodatku, co bywa prawdziwą pułapką, jego jakość zależy od tego, czy zbuduje się go po zgromadzeniu reprezentatywnej ilości danych, bo dzieli przestrzeń wektorową na listy według tego, co jest w tabeli w momencie budowy. Zbudowany na pustej albo prawie pustej tabeli działa kiepsko.
W większości produkcyjnych przypadków skończyło się na HNSW — biorę na klatę wolniejszą budowę w zamian za lepsze zapytania. Ale prawdziwa lekcja jest taka, że to decyzja, którą trzeba podjąć świadomie — wybór indeksu i jego parametry to nie detal, one są wydajnością wyszukiwania wektorowego. Zdanie się na wartości domyślne albo w ogóle pominięcie indeksu to najczęstszy powód, dla którego pgvector „się nie skaluje” u ludzi, którzy potem niesłusznie obwiniają samo rozszerzenie.
Recall a szybkość to suwak, który ustawiasz sam
Druga lekcja, którą poradniki puszczają mimochodem: indeksy ANN są przybliżone, a kompromisem między dokładnością (recall — jak często indeks znajduje rzeczywistych najbliższych sąsiadów) a szybkością sterujesz sam. Nie dzieje się to automatycznie — to suwak, który ustawiasz za pomocą parametrów budowy i zapytania:
- HNSW ma parametry sterujące tym, jak dokładnie buduje się indeks oraz jak szeroko zapytanie przeszukuje graf. Głębsze przeszukiwanie podnosi recall, ale kosztuje czas zapytania; płytsze jest szybsze, ale może przeoczyć część rzeczywistych dopasowań.
- IVFFlat ma parametr określający, ile partycji sprawdza zapytanie — więcej sprawdzeń oznacza wyższe recall i wolniejsze zapytanie.
Praktyczna zasada: dostrajaj te parametry pod własne dane i pod realne wymagania dotyczące recall, a nie akceptuj wartości domyślnych w ciemno. To, jakiego recall potrzebujesz, zależy od aplikacji: funkcja „znajdź podobne artykuły” bez problemu zniesie sporadyczne przeoczenie, natomiast funkcja, w której przeoczony wynik oznacza rzeczywistą awarię, wymaga przekręcenia suwaka w stronę dokładności. Sedno w tym, że ty decydujesz, w którym miejscu krzywej szybkość/dokładność chcesz się znaleźć, i powinieneś zdecydować to świadomie — mierząc recall na własnych danych — a nie zorientować się po wdrożeniu, że wartości domyślne posadziły cię w miejscu, którego nie wybierałeś.
Utrzymywanie embeddingów w zgodzie ze źródłem
Lekcja, która właściwie nie dotyczy wektorów, tylko produkcyjnej poprawności: embeddingi muszą być zgodne z danymi źródłowymi, które reprezentują. Embedding powstaje z jakiegoś tekstu — dokumentu, opisu, rekordu. Gdy źródło się zmienia, dotychczasowy embedding staje się nieaktualny, a wyniki wyszukiwania oparte na nim — błędne. Dyscyplina wygląda tak:
- Przelicz embedding przy każdej zmianie. Gdy tekst źródłowy zostaje zaktualizowany, wygeneruj embedding od nowa i podmień wektor. Wpleć to w ścieżkę zapisu danych, żeby nie dało się o tym zapomnieć — najlepiej tak, żeby zmiana źródła i aktualizacja embeddingu następowały razem.
- Zaplanuj koszt embedowania. Wygenerowanie embeddingu to wywołanie API (powolne, płatne), więc masowe aktualizacje albo pełne przeliczenia rób w zadaniach w tle, wsadowo — nie w linii żądania. Ta sama dyscyplina, co przy każdym powolnym wywołaniu zewnętrznym.
- Obserwuj dryf. Z czasem embeddingi potrafią się rozjechać ze źródłem — przez przeoczone aktualizacje albo zwykłe błędy. Okresowa rekoncyliacja — albo przynajmniej monitorowanie wierszy, których embedding jest starszy niż ostatnia modyfikacja źródła — pozwala złapać dryf, zanim po cichu obniży jakość wyszukiwania.
To dokładnie ten problem spójności, który w układzie jedna baza + pgvector jest o niebo łatwiejszy niż z osobnym magazynem wektorowym — ale „łatwiejszy” to nie „automatyczny”. Nadal trzeba pilnować, żeby embedding nadążał za źródłem; pgvector po prostu pozwala robić to w jednej transakcji, i jest to realna przewaga, z której warto skorzystać.
Realia operacyjne
Kilka rzeczy, które ujawniają się dopiero wtedy, gdy pgvector niesie realny produkcyjny ruch:
- Budowa indeksu jest kosztowna. Zbudowanie indeksu HNSW na dużej tabeli zajmuje sporo czasu i pamięci, a w trakcie potrafi obciążyć samą bazę. Planuj budowy indeksu (i przebudowy) jak poważne operacje, którymi są — poza godzinami szczytu, z zapasem mocy — a nie odpalaj ich od niechcenia na obciążonej produkcyjnej bazie.
- Wektory są duże, miejsce się sumuje. Wektory embeddingów mają setki albo tysiące wymiarów i przy skali samo ich składowanie (a do tego rozmiar indeksu) robi się znaczące. Uwzględnij to w planowaniu pojemności; łatwo nie doszacować, ile miejsca zajmuje kilka milionów embeddingów wraz z ich indeksem HNSW.
- Baza jest współdzielona z resztą aplikacji. Twoje zapytania wektorowe i indeks działają na tym samym Postgresie, który obsługuje resztę aplikacji, więc ciężkie obciążenie wektorowe to obciążenie twojej głównej bazy. W większości aplikacji jest to w porządku i właśnie ta prostota jest tu zyskiem — ale warto to monitorować i mieć świadomość, że w razie potrzeby można wynieść wektory do osobnej bazy, gdyby zaczęły wypychać wszystko inne.
- Mierz wydajność zapytań na danych o rozmiarach produkcyjnych. Wydajność zapytań wektorowych mocno zależy od rozmiaru danych i indeksu, więc benchmarkuj na realistycznych wolumenach. Wydajność, która wygląda przyzwoicie na dziesięciu tysiącach wektorów, potrafi zmienić charakter na dziesięciu milionach — a lepiej się o tym przekonać wcześniej niż twoi użytkownicy.
Werdykt
pgvector pozostaje sensownym wyborem domyślnym do wyszukiwania wektorowego, ale prowadzenie go dobrze na produkcji to prawdziwa dyscyplina, którą wprowadzenia pomijają. Najważniejszy wniosek jest taki, że od indeksu zależy praktycznie cała wydajność — bez indeksu ANN pgvector robi skan liniowy i się nie skaluje, a wybór między HNSW (lepsze zapytania, cięższa i wolniejsza budowa — zwykle właściwy na produkcji) a IVFFlat (lżejszy, ale słabsza wydajność i jakość uzależniona od zbudowania go dopiero po zgromadzeniu reprezentatywnych danych) to świadoma decyzja, a nie detal. Indeksy ANN są przybliżone, więc relacja recall–szybkość to suwak, który ustawiasz sam — przez parametry indeksu, które trzeba dostroić do własnych danych i realnych wymagań co do dokładności, a nie akceptować w ciemno. Embeddingi muszą pozostawać w zgodzie ze źródłem — przelicz je przy każdej zmianie, masowe embedowanie rób w zadaniach w tle i obserwuj dryf — a pgvector ułatwia to bardziej niż osobny magazyn, bo pozwala zamknąć wszystko w jednej transakcji; z tej przewagi po prostu warto skorzystać. Realia operacyjne dają o sobie znać przy skali: budowa indeksu jest kosztowna, wektory zajmują realnie dużo miejsca, obciążenie leży na głównej bazie i trzeba benchmarkować na danych o rozmiarach produkcyjnych. Nic z tego nie zmienia werdyktu, że pgvector to sensowny wybór domyślny — raczej go wyostrza, bo ludzie, u których pgvector „się nie skaluje”, to prawie zawsze ci, którzy pominęli indeks, nigdy nie dostroili recall albo pozwolili embeddingom dryfować. Zrób te trzy rzeczy świadomie, zaplanuj realia operacyjne — a pgvector spokojnie udźwignie produkcyjne wyszukiwanie wektorowe. I właśnie dlatego jest to punkt wyjścia, od którego warto zaczynać.