Wyszukiwanie semantyczne porównuje znaczenie zapytania ze znaczeniem treści. „Jak przyspieszyć stronę" i „Optymalizacja Core Web Vitals" mają bliskie wektory w przestrzeni — system je dopasuje, nawet jeśli nie ma wspólnych słów.
To jest inny problem niż chatbot . W tym artykule budujemy wyszukiwarkę: użytkownik wpisuje frazę, a aplikacja zwraca listę najlepszych dokumentów. RAG może używać takiego samego indeksu, ale dokłada do tego generowanie odpowiedzi modelem językowym.
Jak działa wyszukiwanie wektorowe
Model danych: wybierasz, co jest jednym dokumentem w indeksie — artykuł, produkt, fragment FAQ, wpis dokumentacji albo oferta.
Tekst do embedowania: składasz tytuł, opis, najważniejsze atrybuty i treść w jeden normalizowany tekst.
Indeksowanie: każdy dokument zamieniasz na wektor () i zapisujesz w Upstash Vector razem z metadanymi.
Zapytanie: tekst wyszukiwania zamieniasz na wektor tym samym modelem.
Porównanie: baza wektorowa znajduje najbliższe wektory () i zwraca score.
Ranking i filtr: odrzucasz wyniki poniżej progu, nakładasz metadane i sortujesz albo łączysz wynik z full-text search.
Kiedy Upstash Vector ma sens
Upstash Vector pasuje do projektów, w których chcesz szybko uruchomić semantyczną wyszukiwarkę bez utrzymywania własnej bazy wektorowej. Najlepiej sprawdza się przy katalogach produktów, bazach wiedzy, blogach, dokumentacji, marketplace'ach i panelach B2B, gdzie wyszukiwanie musi rozumieć intencję użytkownika.
Nie używałbym go jako zamiennika każdej wyszukiwarki, ponieważ w sytuacji gdy użytkownicy wpisują głównie numery katalogowe, SKU, identyfikatory faktur albo dokładne nazwy własne, full-text search nadal jest potrzebny. I właśnie dlatego produkcyjna wersja często kończy jako hybryda wektorów (odpowiadają za znaczenie), a full-text (odpowiada za precyzję).
Implementacja z Upstash Vector
Upstash Vector to serverless baza wektorowa z HTTP API, w Next.js możesz użyć jej z , Server Action albo joba indeksującego. Do embeddingów sięgamy przez . Minimalny zestaw pakietów:
Najczęstszy błąd to embedowanie przypadkowego HTML-a albo całego obiektu JSON. Indeks powinien mieć stabilny kształt: ID, tekst do porównywania i metadane potrzebne do linkowania, filtrowania oraz renderowania wyniku.
Co do limitu, chodzi o kontrolę kosztu i szumu. Ddługi dokument warto pociąć na fragmenty, jeśli jeden artykuł zawiera kilka niezależnych tematów, a dla krótkich wpisów, produktów i FAQ często wystarczy jeden wektor na rekord.
Ten sam mechanizm powinien działać przy publikacji, edycji i usunięciu treści. Indeks nie może być jednorazowym skryptem odpalanym ręcznie przed launch'em.
Code
// app/api/reindex/route.tsimport { Index } from '@upstash/vector'export const runtime = 'nodejs'const index = new Index({ url: process.env.UPSTASH_VECTOR_REST_URL!, token: process.env.UPSTASH_VECTOR_REST_TOKEN!,})export async function POST(request: Request) { const secret = request.headers.get('x-reindex-secret') if (secret !== process.env.REINDEX_SECRET) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } const event = await request.json() if (event.type === 'deleted') { await index.delete(event.id) return Response.json({ ok: true }) } // Pobierz aktualny dokument z CMS/bazy i wywołaj indexDocuments([doc]). // Ten endpoint powinien być idempotentny: ten sam dokument ma zawsze to samo ID. return Response.json({ ok: true })}
Endpoint działa na , bo zapytanie do Upstash Vector i embedding query idą przez HTTP. Nie potrzebujesz Node'owego runtime'u, a edge daje niższe opóźnienie bliżej użytkownika.
Filtry po metadanych są bardo istotne, ponieważ bez nich polskie zapytanie może zwrócić angielski dokument, wyszukiwarka produktów zacznie mieszać wpisy blogowe z FAQ, a wewnętrzny panel może pokazać treści spoza uprawnień użytkownika.
Debounce na 300 ms to minimum, a bez niego każde wciśnięcie klawisza odpala zapytanie i embedding. Na produkcji dorzuć jeszcze AbortController, żeby anulować poprzednie zapytanie przy szybkim pisaniu, stan ładowania i obsługę błędu fetch. Surowego score'u nie pokazuj użytkownikowi końcowemu, ponieważ jest przydatny do debugowania i strojenia progu, ale w interfejsie lepiej pokazać tytuł, opis i ewentualnie wyróżnione dopasowanie.
Hybrid search — wektory + full-text
Najlepiej działa . Wektory łapią znaczenie, full-text łapie dokładne frazy, czyli nazwy własne, numery katalogowe, terminy, których nie chcesz „interpretować":
Code
type RankedResult = { id: string score: number source: 'vector' | 'text'}function reciprocalRankFusion( lists: RankedResult[][], k = 60,): Map<string, number> { const scores = new Map<string, number>() for (const list of lists) { list.forEach((result, index) => { const current = scores.get(result.id) ?? 0 scores.set(result.id, current + 1 / (k + index + 1)) }) } return scores}export async function hybridSearch(query: string) { const [vectorResults, textResults] = await Promise.all([ vectorSearch(query), // semantyka, synonimy, intencja fullTextSearch(query), // dokładne frazy, SKU, nazwy własne ]) const rrfScores = reciprocalRankFusion([vectorResults, textResults]) return [...rrfScores.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([id, score]) => ({ id, score }))}
To nie musi być pierwszy etap MVP, bo na start wystarczy czyste vector search. Hybrydę warto dodać, gdy pojawiają się zapytania typu „SKU-1839", „Next.js 16", „GA4", nazwy produktów albo dokładne tytuły dokumentów.
Jak oceniać jakość wyników
Nie stroisz wyszukiwarki po jednym przykładzie z demo, przygotuj mały zestaw zapytań testowych:
Zapytania semantyczne: „jak przyspieszyć stronę", „strona wolno działa na telefonie", „automatyzacja obsługi leadów".
Zapytania dokładne: nazwy produktów, tytuły artykułów, numery katalogowe, skróty technologii.
Zapytania puste intencyjnie: frazy, dla których nie powinno być wyniku.
Zapytania wielojęzyczne: jeśli indeksujesz kilka języków, sprawdź, czy filtr locale nie miesza wyników.
Dla każdego zapytania zapisz oczekiwany top 1 albo top 3, a potem mierz:
hit rate@3 — czy oczekiwany wynik jest w pierwszych trzech pozycjach
zero-results rate — jak często użytkownik nie dostaje nic
bad-results rate — jak często dostaje wynik pozornie podobny, ale
bezużyteczny
click-through z wyników — czy użytkownicy klikają to, co wyszukiwarka
uważa za trafne
Dopiero na tej podstawie ustawiaj próg podobieństwa, decyduj o dzieleniu długich treści na fragmenty i o dołożeniu wyszukiwania po dokładnych słowach. W przeciwnym razie łatwo zbudować wyszukiwarkę, która wygląda inteligentnie w prezentacji, ale gubi najważniejsze zapytania użytkowników.
Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.
Koszt zależy od modelu embeddingów, liczby zapytań oraz rozmiaru indeksu. Dla małej bazy wiedzy to zwykle tani element architektury. Embeddingi liczy się raz przy indeksowaniu, a ponownie tylko dla krótkiego tekstu zapytania. Przy dużym ruchu monitoruj osobno koszt embeddings (per token), storage w bazie wektorowej i liczbę zapytań wykonywanych przez autocomplete.
Jak szybko działa wyszukiwanie wektorowe?
Zwykle mówimy o setkach milisekund, ale wynik zależy od regionu, providera embeddingów, wielkości indeksu i tego, czy łączysz wektory z dodatkowymi filtrami. Dla autocomplete ważniejsze od mediany jest stabilne p95 i sensowny debounce (np. 300 ms), żeby nie odpalać zapytania przy każdym wciśnięciu klawisza.
Czy mogę indeksować produkty e-commerce?
Tak. Indeksuj nazwy, opisy i atrybuty produktów. Wyszukiwanie semantyczne umożliwi wpisanie przez użytkownika „ciepłe buty na zimę", a wyszukiwarka znajdzie „Trapery zimowe ocieplane" mimo braku wspólnych słów.
Jak dobrać próg podobieństwa (score)?
Próg score >= 0.68 z przykładu to wskaźnik na start. Zbyt wysoki próg odsiewa trafne wyniki, a zbyt niski wpuszcza szum. Najlepiej przejść przez realne zapytania użytkowników, zobaczyć rozkład score'ów dla trafień i nietrafień, i ustawić próg empirycznie. Wszystko zależy od modelu, języka i charakteru treści.
Po co łączyć wektory z full-text search?
Bo każda metoda łapie co innego. Wektory rozumieją intencję i synonimy, ale potrafią rozmyć dokładne dopasowania (numery katalogowe, nazwy własne, precyzyjne frazy). Full-text trafia w dokładne tokeny. Hybrid search (np. przez Reciprocal Rank Fusion) łączy obie listy wyników i w praktyce daje najlepszą trafność.
Czy wyszukiwanie semantyczne to to samo co RAG?
Nie. Wyszukiwanie semantyczne zwraca listę trafnych dokumentów lub produktów. RAG używa podobnego etapu wyszukiwania, ale dokleja znaleziony kontekst do promptu i generuje odpowiedź modelem językowym. Ten artykuł skupia się na wyszukiwarce.
Jak aktualizować indeks po zmianie treści?
Traktuj indeks jak osobną projekcję danych. Przy publikacji, edycji lub usunięciu dokumentu odpal webhook albo job, który ponownie przelicza embedding i robi upsert lub delete po stabilnym ID. Nie zostawiaj indeksu jako jednorazowego skryptu uruchamianego ręcznie.
O autorze
Maciej Sala
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty side-projecty.