[02]
WYSZUKIWARKA PRODUKTÓW / ASYSTENT AI
█ TIER A · ROK 2026 · STATUS: DZIAŁA · JĘZYKI: JAVASCRIPT
Pipeline rewrite → hybrid → rerank mierzony ogniwo po ogniwie
[FIG. 1] MISJA
Składam wieloetapowe pipeline'y wyszukiwania rewrite → hybrid → rerank i mierzę każde ogniwo osobno, zamiast oceniać całość na oko. 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 obsługuję je przepisaniem zapytania przed embeddingiem: LLM poprawia literówki i tłumaczy zapytanie, dopiero potem idzie ono do wyszukiwania. Ranking wektorowy łączę z pełnotekstowym w jednym zapytaniu SQL (pgvector z HNSW plus FTS z GIN, fuzja Reciprocal Rank Fusion), zamiast zszywać dwa systemy w aplikacji, a na końcu model układa kandydatów listwise. Dowożę mierzalną poprawę: na trudnym zbiorze MRR wzrósł z 0.676 do 0.934 przy Recall@10 = 1.000. Nad wyszukiwarką dobudowałem RAG konwersacyjny: asystent wyciąga kryteria z rozmowy, filtruje cenę twardo w SQL i semantycznie weryfikuje per atrybut, czy produkt naprawdę spełnia wymagania, zanim go poleci. Niezawodność i koszt traktuję jak wymagania, nie jak życzenia: każda rola modelu ma timeout i ścieżkę awaryjną. 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. 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

