Wyszukiwanie semantyczne w Postgresie z pgvector
Wyszukiwanie słów kluczowych dopasowuje słowa; wyszukiwanie semantyczne dopasowuje znaczenie. Deep-dive o embeddingach i podobieństwie wektorowym oraz o tym, jak pgvector pozwala robić wyszukiwanie semantyczne w Postgresie, który już uruchamiasz — bez osobnej bazy wektorowej.
Wpisy o wyszukiwaniu pełnotekstowym dotyczyły dopasowywania słów — rdzeni, trigramów, szeregowania nakładaniem terminów. To ma twardy sufit: nie potrafi dopasować znaczenia. Wyszukiwanie „laptop się nie włącza” powinno wynieść dokument zatytułowany „notebook nie bootuje”, ale nie dzielą słów, więc wyszukiwanie słów kluczowych całkowicie to pomija. Wyszukiwanie semantyczne zamyka tę lukę, dopasowując po znaczeniu, a nie sformułowaniu, a technologia, która to umożliwia — embeddingi i podobieństwo wektorowe — jest teraz dostępna wewnątrz Postgresa przez rozszerzenie pgvector. Oto jak działa i jak to zbudować.
Embeddingi: zamiana znaczenia w wektory
Kluczową ideą jest embedding: model (zwykle model embeddingowy LLM, który wołasz przez API) zamienia kawałek tekstu w wektor — listę liczb, często setek lub więcej — reprezentujący jego znaczenie w wysokowymiarowej przestrzeni. Kluczową właściwością jest to, że teksty o podobnym znaczeniu produkują wektory, które są blisko siebie, nawet jeśli nie dzielą słów. „Laptop się nie włącza” i „notebook nie bootuje” lądują blisko siebie; „najlepszy przepis na makaron” ląduje daleko.
# uzyskaj embedding dla kawałka tekstu (przez API embeddingów)
embedding = EmbeddingClient.embed("notebook nie bootuje")
# => [0.012, -0.045, 0.231, ...] wektor, powiedzmy, 1536 liczb
Wyszukiwanie staje się wtedy problemem geometrii: zembedduj zapytanie użytkownika i znajdź przechowywane dokumenty, których wektory są najbliżej niego. Bliskość w tej przestrzeni oznacza podobieństwo znaczenia, więc najbliższe wektory to najbardziej semantycznie istotne wyniki — bez względu na dzielone słowa kluczowe.
pgvector: wektory jako kolumna Postgresa
Rozszerzenie pgvector dodaje typ kolumny vector i operatory odległości do ich porównywania, więc
twoje embeddingi żyją jako kolumna na twoich istniejących tabelach — bez osobnej bazy wektorowej do
uruchamiania:
CREATE EXTENSION vector;
ALTER TABLE documents ADD COLUMN embedding vector(1536);
-- znajdź 10 dokumentów najbardziej podobnych znaczeniem do wektora zapytania
-- <=> to odległość kosinusowa: mniejsza = bardziej podobne
SELECT id, title
FROM documents
ORDER BY embedding <=> '[0.012, -0.045, ...]'
LIMIT 10;
Operator <=> liczy odległość między wektorami (odległość kosinusowa to zwykły wybór dla embeddingów
tekstu; pgvector oferuje też L2 i iloczyn wewnętrzny). Sortuj po niej rosnąco, a dostajesz najbardziej
semantycznie podobne dokumenty najpierw. To całe zapytanie — znajdź najbliższe wektory — i zwraca
wyniki, których wyszukiwanie słów kluczowych nigdy by nie znalazło, bo dopasowuje znaczenie, nie słowa.
Budowa: przechowuj embeddingi, szukaj po embeddingu
Kształt od końca do końca w aplikacji Rails:
# 1. Gdy dokument jest tworzony/aktualizowany, wygeneruj i przechowaj jego embedding (w zadaniu w tle)
class EmbedDocumentJob < ApplicationJob
def perform(doc_id)
doc = Document.find(doc_id)
doc.update!(embedding: EmbeddingClient.embed(doc.searchable_text))
end
end
# 2. By szukać, zembedduj zapytanie i znajdź najbliższych sąsiadów
def semantic_search(query)
query_vector = EmbeddingClient.embed(query)
Document.order(Arel.sql("embedding <=> '#{query_vector}'")).limit(10)
end
Dwie uwagi inżynierskie przenoszące się z wpisu o API LLM: generowanie embeddingów to wywołanie API, więc rób je w zadaniu w tle, nie w żądaniu; a embeddingi kosztują (per token), a to samo wejście daje stabilny embedding, więc re-embeddujesz tylko, gdy tekst się zmieni. Embeddingi, raz przechowane, są przeszukiwane w całości wewnątrz Postgresa — szybko i za darmo per zapytanie.
Indeksowanie pod skalę
Brute-force wyszukiwanie najbliższego sąsiada porównuje zapytanie z każdym wierszem, co jest w porządku dla tysięcy dokumentów, ale wolne dla milionów. pgvector dostarcza indeksy przybliżonego najbliższego sąsiada (IVFFlat, z wyłaniającym się HNSW), które czynią wyszukiwanie wektorowe szybkim przy skali, za mały koszt recall:
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
To ta sama zasada indeksowania co indeksy GIN dla wyszukiwania pełnotekstowego — bez niego skanujesz wszystko; z nim skaczesz do prawdopodobnych sąsiadów. Dla dużego korpusu indeks ANN jest tym, co czyni wyszukiwanie semantyczne wydajnym.
Semantyczne vs słów kluczowych: używaj obu
Wyszukiwanie semantyczne jest potężne, ale nie zastępuje wyszukiwania słów kluczowych — mają komplementarne siły, a najlepsze wyniki często łączą oba:
- Wyszukiwanie semantyczne błyszczy w znaczeniu, synonimach i rozmytej intencji — znajduje właściwy dokument, nawet gdy sformułowanie jest całkowicie inne. Ale może być zbyt rozmyte: może pominąć dokładne dopasowanie słowa kluczowego, którego użytkownik oczekuje, albo uszeregować mgliście powiązany wynik nad dokładnym.
- Wyszukiwanie słów kluczowych (FTS) błyszczy w dokładnych terminach, nazwach, kodach i precyzji — gdy użytkownik szuka SKU produktu albo konkretnej frazy, chce tego dokładnego dopasowania, które podobieństwo semantyczne mogłoby uszeregować poniżej czegoś „znaczeniowo podobnego”.
Najsilniejszym podejściem jest często wyszukiwanie hybrydowe: połącz wyniki semantyczne (wektor) i
słów kluczowych (FTS), mieszając ich szeregowania, by dostać zarazem znaczenie i precyzję. A fakt, że
oba żyją w Postgresie — FTS przez tsvector, semantyczne przez pgvector — czyni wyszukiwanie
hybrydowe sprawą jednej bazy, co jest dokładnie przewagą „Postgres do wszystkiego” zastosowaną do ery
AI. Nie potrzebujesz osobnej bazy wektorowej obok osobnej wyszukiwarki; jeden Postgres trzyma twoje
dane, indeks słów kluczowych i indeks wektorowy razem.
Werdykt
Wyszukiwanie semantyczne dopasowuje znaczenie tam, gdzie wyszukiwanie słów kluczowych dopasowuje tylko słowa — embeddując tekst w wektory tak, by podobne znaczenia siedziały blisko siebie, potem znajdując najbliższe wektory zapytania. Znaczącą rzeczą w 2023 jest to, że nie potrzebujesz dedykowanej bazy wektorowej, by to zrobić: pgvector wnosi kolumny wektorowe, operatory odległości i indeksowanie ANN do Postgresa, który już uruchamiasz, więc twoje embeddingi żyją obok twoich danych, a wyszukiwanie dzieje się w jednym miejscu. Generuj embeddingi w zadaniu w tle (to wywołanie API z kosztem per token), indeksuj IVFFlat pod skalę i — co kluczowe — traktuj wyszukiwanie semantyczne jako uzupełnienie wyszukiwania słów kluczowych, nie zastępstwo, łącząc je w wyszukiwanie hybrydowe dla zarazem znaczenia i precyzji. Nagłówkiem jest ten sam motyw, do którego te wpisy wciąż wracają, teraz rozszerzony na AI: zanim sięgniesz po wyspecjalizowany skład danych, sprawdź, co Postgres potrafi — a z pgvector potrafi teraz też wyszukiwanie semantyczne.