W sklepach, które audytuję, wyszukiwarka i filtry są zwykle najsłabiej dopracowaną częścią serwisu, choć korzystają z nich klienci najbliżsi zakupu: wiedzą, czego szukają, i chcą to szybko znaleźć. Brak wyników dla zapytania z literówką, filtry przeładowujące całą stronę albo tysiące kombinacji filtrów w indeksie Google to problemy, które widzę najczęściej. W tym wpisie pokazuję, jak projektuję i programuję wyszukiwarkę oraz filtry, od wyboru silnika, przez obsługę w przeglądarce, po SEO i pomiar.
Wybór silnika wyszukiwania
Zapytanie LIKE '%fraza%' działa przy kilkuset produktach, ale nie radzi sobie z odmianą słów, literówkami ani oceną trafności,
a przy dużych tabelach mocno obciąża bazę. Dobieram rozwiązanie do wielkości katalogu i budżetu:
| Rozwiązanie | Kiedy wybieram | Na co uważać |
|---|---|---|
| Wyszukiwanie pełnotekstowe w MySQL lub PostgreSQL | do kilkudziesięciu tysięcy produktów, bez dodatkowej infrastruktury | słaba obsługa polskiej odmiany i literówek bez dodatkowej konfiguracji |
| Meilisearch lub Typesense | średnie sklepy, szybkie wdrożenie z tolerancją literówek | osobna usługa do utrzymania i synchronizacji danych |
| Elasticsearch lub OpenSearch | duże katalogi, złożone fasety, własne reguły trafności | wymaga doświadczenia, do polskiego potrzebne dodatkowe analizatory |
| Algolia i usługi SaaS | gdy liczy się czas wdrożenia, a nie koszt | cena rośnie z liczbą zapytań i produktów |
Niezależnie od silnika dodaję słownik synonimów, który budują sami klienci: frazy z raportu wyszukiwań bez wyników. Gdy ktoś szuka „adidasów”, a w sklepie są „buty sportowe”, synonim rozwiązuje problem bez zmian w opisach produktów.
Podpowiedzi podczas pisania
Podpowiedzi wyświetlam po wpisaniu dwóch znaków i dopiero wtedy, gdy użytkownik przerwie pisanie na ok. 250 ms. Bez tego każde naciśnięcie
klawisza wysyła zapytanie, a odpowiedzi mogą wrócić w złej kolejności i pokazać wyniki dla wcześniejszej frazy. Poprzednie zapytanie
anuluję przez AbortController:
let controller;
let timer;
input.addEventListener('input', (e) => {
clearTimeout(timer);
const q = e.target.value.trim();
if (q.length < 2) return;
timer = setTimeout(async () => {
controller?.abort();
controller = new AbortController();
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal });
renderSuggestions(await res.json());
} catch (err) {
if (err.name !== 'AbortError') throw err;
}
}, 250);
});
W podpowiedziach pokazuję zdjęcie, cenę i kategorię, a nie tylko nazwę, oraz pozwalam przejść do wyników przyciskiem Enter. Całość musi działać z klawiatury, z poprawnymi rolami ARIA dla listy podpowiedzi.
Strona bez wyników
Pusta strona z komunikatem „Brak wyników” to najprostsza droga do utraty klienta. Na takiej stronie pokazuję poprawioną frazę, jeśli wyszukiwarka wykryła literówkę, popularne produkty z kategorii, do której pasuje zapytanie, oraz kontakt do obsługi. Każde wyszukiwanie bez wyników zapisuję, bo to najlepsza lista brakujących synonimów i produktów, których klienci szukają.
Filtry, adresy i przeładowanie strony
Wybrane filtry zapisuję w parametrach adresu, np. /buty-damskie?rozmiar=38&kolor=czarny. Dzięki temu link do przefiltrowanej
listy można wysłać, przycisk Wstecz działa zgodnie z oczekiwaniami, a po odświeżeniu filtry zostają. Wyniki doładowuję bez przeładowania
strony, a adres aktualizuję przez history.pushState.
Kilka zasad, których pilnuję w samych filtrach:
- przy każdej wartości pokazuję liczbę produktów, a wartości dające zero wyników wyszarzam zamiast je ukrywać,
- aktywne filtry wyświetlam nad listą jako etykiety z możliwością usunięcia pojedynczo lub wszystkich naraz,
- na telefonie filtry otwierają się w osobnym panelu z przyciskiem „Pokaż produkty”, a nie po każdym zaznaczeniu,
- podczas doładowywania wyników pokazuję skeleton screen zamiast spinnera, żeby lista nie przeskakiwała.
Przy długich listach decyduję też, czy lepsza będzie paginacja czy infinite scroll. W sklepach zwykle wybieram paginację z przyciskiem „Pokaż więcej”, bo pozwala wrócić do tego samego miejsca na liście.
Filtry a SEO
Każda kombinacja filtrów to nowy adres. Przy kilku filtrach z kilkunastoma wartościami powstają setki tysięcy adresów z prawie tą samą treścią, które pochłaniają crawl budget i rozmywają widoczność kategorii. Dlatego dzielę filtry na dwie grupy:
- Filtry z popytem w wyszukiwarce, np. „buty do biegania damskie” albo „sofa narożna szara”. Takie kombinacje zamieniam w pełnoprawne podstrony z czytelnym adresem, własnym tytułem, opisem i linkami z kategorii.
- Pozostałe filtry i sortowanie nie trafiają do indeksu. Adresy z wieloma parametrami mają adres kanoniczny wskazujący na
kategorię albo
noindex, a linki do nich nie są zwykłymi linkami w kodzie strony.
Strony wyników wyszukiwarki wewnętrznej zawsze wyłączam z indeksowania. Wpisanie dowolnej frazy tworzy nowy adres, a Google traktuje indeksowane wyniki wyszukiwania jako treść niskiej jakości.
Wydajność i pomiar
Najczęściej używane filtry i frazy warto buforować po stronie serwera, bo powtarzają się setki razy dziennie. Liczniki produktów przy filtrach liczę w silniku wyszukiwania albo z wcześniej przygotowanej tabeli, a nie osobnym zapytaniem do bazy dla każdej wartości. Gdy lista filtrów ładuje się wolno, przyczyną jest zwykle właśnie liczba zapytań, a ich ograniczenie to podstawa, na której opiera się wydajna komunikacja z bazą danych w CMS.
W Google Analytics 4 włączam śledzenie wyszukiwania w witrynie i porównuję konwersję osób korzystających z wyszukiwarki z resztą ruchu. Lista najczęstszych fraz i fraz bez wyników to dla mnie podstawowe źródło kolejnych poprawek, a większe zmiany w układzie filtrów sprawdzam, przeprowadzając testy A/B.