P R O J E K T YK O N T A K T

[02]

RETRIEVAL PRODUKTÓW

TIER A · ROK 2026 · STATUS: DZIAŁA · JĘZYKI: JAVASCRIPT

Wyszukiwarka hybrydowa i asystent zakupowy nad 10 000 produktów

[FIG. 1] MISJA

Zapytania pisane po ludzku — z literówkami („watterproof jaket men”) albo po polsku („namiot dla 2 osób”) — nie miały prawa trafić w anglojęzyczny korpus 10 000 produktów outdoorowych, więc zbudowałem pipeline rewrite → hybrid → rerank i zmierzyłem każde ogniwo osobno: LLM najpierw poprawia i tłumaczy zapytanie, potem jedno zapytanie SQL łączy ranking wektorowy (pgvector, HNSW) z pełnotekstowym (FTS, GIN) przez Reciprocal Rank Fusion, a na końcu model układa kandydatów listwise. Na trudnym zbiorze MRR wzrósł z 0.676 do 0.934 przy Recall@10 = 1.000. Projekt to świadoma przebudowa starszego stacku FastAPI/Spark/GCP do jednego deployu Next.js z jednym Postgresem i dokładnie pięcioma zależnościami npm — cały asystent czatowy, product-grid i dwuzakresowy suwak ceny powstały bez ani jednej biblioteki UI. Nad wyszukiwarką dobudowałem asystenta zakupowego, który wyciąga kryteria z rozmowy, filtruje cenę twardo w SQL i semantycznie weryfikuje, czy produkt naprawdę spełnia wymagania, zanim go poleci. Demo działa publicznie na Vercel + Supabase + OpenRouter.

[FIG. 2] ARCHITEKTURA

najedź na blok, by zobaczyć opis

[FIG. 3] WYZWANIA

[+][CH-01]

Rdzeniem asystenta jest sprawdzenie, czy produkt spełnia wymagania (kolor, rozmiar, pojemność), ZANIM go polecę — a najtańszy model okazał się do tego za słaby na trzy różne sposoby. Najpierw poprawnie opisywał sprzeczność w polu reason, ale wrzucał klucz do złej tablicy; potem JSON weryfikacji dziesięciu produktów naraz przekraczał limit tokenów, ucinał się i wpadał w fallback, który niczego nie odrzucał — but „White/Black” przechodził przy wymogu „red”; wreszcie nawet przy mniejszych partiach model leniwie oznaczał całość jako „unknown”. Pomogły dwa zabiegi naraz: chain-of-thought w JSON (model najpierw wypisuje wartość z tekstu, dopiero potem status) i partie po maksymalnie pięć produktów — a samą decyzję „spełnia / nie spełnia” przeniosłem z modelu do kodu. Mini-eval wszedł z 5/8 na 8/8, bez eskalacji do droższego modelu.

[+][CH-02]

Tuż przed deployem produkcyjnym wyciekł klucz OpenAI ze starego repozytorium i musiałem zmienić providera — z podwójnym ryzykiem: inna przestrzeń embeddingów oznaczałaby re-embedding 10 000 produktów, a tańszy model czatu mógł zepsuć jakość rewrite'u i reranku. Embeddingi zostawiłem w tej samej przestrzeni (ten sam model przez OpenRouter — zero re-embedowania), a model czatu wybrałem pomiarem: najtańszy kandydat zaliczył MRR 0.517 wobec baseline'u 0.739 i odpadł, a gemini-2.5-flash-lite dał 0.761 przy trzykrotnie niższej cenie. Cała migracja providera to w kodzie jedna zmienna środowiskowa.

[+][CH-03]

Dwa wywołania LLM w ścieżce każdego zapytania to dwa nowe punkty awarii publicznego endpointu: timeout providera, zepsuty JSON, halucynowane indeksy kandydatów. Przyjąłem zasadę, że błąd modelu nigdy nie daje HTTP 500: rewrite przy timeout'cie oddaje oryginalne zapytanie, a rerank waliduje strukturalnie output (deduplikacja, zakres indeksów), dopełnia braki kolejnością hybrydy i przy każdym wyjątku zwraca top-10 z fuzji. Testowałem to wymuszonym timeoutem 1 ms i celowo zepsutą odpowiedzią JSON — w obu przypadkach użytkownik dostaje wyniki, tylko bez tego jednego ogniwa.

[FIG. 4] WARSTWA AI

LLM pracuje tu w kilku rolach na ścieżce produkcyjnej: przepisuje zapytania przed embeddingiem, rerankuje kandydatów listwise, wyciąga kryteria z rozmowy, weryfikuje ich spełnienie per atrybut i pisze ugruntowaną odpowiedź po polsku. Każda rola ma timeout i fallback — błąd modelu nigdy nie kończy się HTTP 500, tylko wynikiem bez tego ogniwa. Model wybrałem evalem, nie cennikiem: najtańszy kandydat odpadł za MRR 0.517.

[FIG. 5] GALERIA

SCREENY W DRODZE — patrz SHOTS-WANTED

[FIG. 6] STACK & LINKI

NEXT.JS 15POSTGRESQL + PGVECTORSUPABASEOPENROUTEROPENAI SDKVERCELGITHUB ACTIONS