Jak naprawdę działa wyszukiwanie fasetowe w Elasticsearch (i gdzie się psuje)
Faseta to dwa pytania, nie jedno
Strona wyszukiwania fasetowego zadaje jednocześnie dwa różne pytania. Pierwsze jest oczywiste: które dokumenty pasują do bieżącego wyszukiwania i do już zastosowanych filtrów. Drugie jest zadawane raz dla każdej dostępnej wartości filtra: ile dokumentów by pasowało, gdyby ta wartość również została wybrana. Pole „Czerwony (12)” jest odpowiedzią na drugie pytanie, obliczoną w trakcie odpowiadania na pierwsze.
Zasadnicza trudność wyszukiwania fasetowego, w Elasticsearch i gdzie indziej, wynika właśnie z tego drugiego pytania. Uzyskanie listy pasujących dokumentów nie jest skomplikowane. Prawdziwym problemem inżynierskim jest uzyskanie uczciwego zliczenia obok każdej wartości każdego filtra, przy każdym załadowaniu strony, tak aby czas odpowiedzi nie eksplodował wraz ze wzrostem liczby faset.
Podstawowy element: agregacja terms na przefiltrowanym zbiorze
Elasticsearch odpowiada na oba pytania jednym zapytaniem. Klauzula zapytania zawęża zbiór dokumentów (dopasowanie pełnotekstowe plus filtry już aktywne), a zestaw agregacji przypiętych do tego samego zapytania oblicza dla każdego pola fasety, ile dokumentów z tego przefiltrowanego zbioru trafia do każdej wartości. Agregacja terms na polu color zwraca jeden bucket dla każdej odrębnej wartości wraz z liczbą dokumentów; agregacja range lub histogram robi to samo dla pól liczbowych, takich jak cena. Wszystko to dzieje się w ramach jednego wykonania zapytania, na tym samym przefiltrowanym zbiorze, w jednej wymianie z serwerem.
Łatwo to przeoczyć, jeśli używało się wyłącznie SQL: agregacja nie jest tu drugim zapytaniem wykonywanym na wyniku pierwszego. Wykonuje się na doc values i postings indeksu podczas tego samego zapytania, dlatego dodanie dwudziestu kolejnych faset do strony nie oznacza dwudziestu kolejnych pełnych przejść przez pasujące dokumenty. Oznacza dwadzieścia kolejnych obliczeń bucketów na danych, których silnik i tak musiał dotknąć, by odpowiedzieć na zapytanie.
Kontekst filter, a nie kontekst query
Fasety oraz filtry stosowane po kliknięciu w nie powinny prawie zawsze znajdować się w kontekście filter, a nie w kontekście query. Filtr term na brand: acme nie musi obliczać wyniku trafności, potrzebna jest mu odpowiedź tak lub nie, a Elasticsearch buforuje klauzule filtrów znacznie agresywniej niż klauzule zapytania z punktacją. Budowanie strony wyszukiwania fasetowego wyłącznie z klauzul bool must, z których wszystkie są punktowane, to częsty sposób na uczynienie jej wolniejszą, niż to konieczne, bez żadnej korzyści: nikt nie sortuje strony kategorii według „trafności względem filtra Czerwony”. Filtry trafiają do filter, wyszukiwanie pełnotekstowe do must lub do dedykowanego zapytania match, a ten podział nie jest kosmetyczny: zmienia to, co jest buforowane, i to, co jest punktowane.
Pułapka wielokrotnego wyboru
Szczegół, który psuje więcej implementacji wyszukiwania fasetowego niż jakikolwiek inny, jest następujący: przy obliczaniu zliczenia fasety trzeba wykluczyć filtr tej fasety ze zbioru dokumentów, na którym się liczy, zachowując wszystkie pozostałe aktywne filtry. W przeciwnym razie każda faseta, po której już filtrowano, wyświetli zliczenie równe liczbie wyników widocznych aktualnie na ekranie, ponieważ liczony jest przekrój z warunkiem już w pełni zastosowanym. Po kliknięciu „Czerwony” każda inna wartość w „Kolorze” wyświetlałaby błędnie tę samą liczbę co suma, zamiast liczby produktów czerwonych i tej wartości, które faktycznie istnieją.
Zwykła poprawka polega na zbudowaniu jednej agregacji na pole fasety, każdej opakowanej we własną agregację filter, która ponownie stosuje wszystkie aktywne filtry z wyjątkiem filtra tego pola, wszystkie na tym samym poziomie pod tym samym zapytaniem głównym. W praktyce: zapytanie bazowe niesie już wyszukiwanie pełnotekstowe i wszystkie filtry, które nie są akurat liczone, a agregacja każdej fasety dodaje pozostałe wybrane filtry, bez własnego. Daje to więcej agregacji w zapytaniu, a nie więcej zapytań, i jest to część, którą warto przetestować jawnie, zamiast zatwierdzać na oko, ponieważ strona może wyglądać zupełnie poprawnie w demonstracji z trzema produktami i po cichu wyświetlać fałszywe zliczenia, gdy tylko załadowane zostaną prawdziwe dane z prawdziwymi częściami wspólnymi.
Jest jeszcze druga decyzja, związana z pierwszą, którą lepiej podjąć świadomie: co oznacza grupa faset, gdy wybrano razem kilka wartości pod tą samą etykietą. Jeśli sklep grupuje „Karmazynowy”, „Szkarłatny” i „Bordowy” pod jednym filtrem „Czerwony”, to czy wybranie dwóch z tych podwartości oznacza „ma to I tamto”, czy „ma to LUB tamto”? Zliczanie, które odpowiada na pytanie I, podczas gdy kliknięcie stosuje pytanie LUB (albo odwrotnie), jest najczęstszą przyczyną filtra, który wyświetla jedną liczbę, a zwraca inną. Oba muszą być obliczane według dokładnie tej samej logiki, co zwykle sprowadza się do traktowania grupy od początku jako jednego warunku sumy mnogościowej, zamiast dodawania zliczeń dla poszczególnych wartości, które mogą się pokrywać lub liczyć dwa razy ten sam produkt.
Dlaczego bez silnika agregacji wszystko się załamuje, z prawdziwą liczbą
Sens istnienia wyszukiwarek fasetowych, zamiast robienia wszystkiego w SQL, staje się oczywisty, gdy tylko spróbuje się uzyskać dokładne zliczenia dla każdej wartości w relacyjnej bazie danych na dużą skalę. W testowym katalogu liczącym 200 000 produktów, z wbudowaną wyszukiwarką PrestaShop ograniczoną do bazy danych, z około 2 250 grup atrybutów i cech, obliczenie dokładnego zliczenia „ile wyników na wartość” zapytaniem COUNT(DISTINCT ...) na fasetę sprawiało, że strona kategorii z jednym aktywnym filtrem ładowała się 43 sekundy. Wyłączenie dokładnych zliczeń dla tych faset (wyświetlanie listy wartości bez liczby) sprowadziło tę samą stronę do nieco poniżej 12 sekund, co pokazuje, jaka część początkowego kosztu wynikała z samego liczenia, a nie z wyszukiwania.
Pokrewny błąd, wykryty podczas wewnętrznego przeglądu kodu modułu wyszukiwania fasetowego, polegał na obliczaniu zakresu fasety liczbowej (dla suwaka „Cena” lub „Waga”) przez załadowanie wszystkich pasujących wartości do pamięci aplikacji i wyznaczenie minimum i maksimum tam, zamiast poproszenia o to bazy danych lub wyszukiwarki. W katalogu, w którym dotyczyło to około dwóch milionów wierszy, sam ten krok trwał 11 sekund i zajmował 965 MB pamięci na zapytanie, co w niektórych konfiguracjach wystarczało, by wyczerpać limit pamięci hostingu współdzielonego i zwrócić stronę błędu zamiast suwaka. Przeniesienie tego samego obliczenia do GROUP BY bazy danych albo do agregacji range wyszukiwarki znów uczyniło z niego zwykłe, szybkie zapytanie, ponieważ jest to dokładnie ten rodzaj obliczenia zbiorczego, który te systemy są zaprojektowane wykonywać w jednym przebiegu na zindeksowanych danych, a nie w kodzie aplikacji po fakcie.
Obie liczby opisują ten sam podstawowy błąd na różnych poziomach: pytanie „ile, dla każdej możliwej wartości” zadane raz na wartość, zamiast raz dla wszystkich wartości razem. Mechanizm agregacji Elasticsearch istnieje właśnie po to, by odpowiadać na to pytanie w jednym przebiegu na danych już zindeksowanych. Na tym polega jego prawdziwa wartość, niezależnie od rankingu trafności pełnotekstowej, i dlatego nawigacja fasetowa należy do najczęstszych powodów przejścia na Elasticsearch, nawet gdy samo wyszukiwanie nie potrzebuje zaawansowanych funkcji trafności.
Krótka lista kontrolna przed uruchomieniem wyszukiwania fasetowego
- Filtry trafiają do kontekstu filter, pełny tekst do kontekstu query. Proszę nie obliczać punktacji dla pola wyboru.
- Agregacja zliczająca każdej fasety musi wykluczać aktywny filtr tej fasety, zachowując wszystkie pozostałe, w przeciwnym razie fasety z wielokrotnym wyborem będą kłamać, gdy tylko użyje się razem dwóch filtrów.
- Proszę zdecydować od początku i jawnie przetestować, czy grupa wartości pod tą samą etykietą działa jako I, czy jako LUB, i sprawić, by zliczanie i kliknięcie dzieliły dokładnie tę samą logikę.
- Proszę ograniczać rozmiar agregacji i obserwować
doc_count_error_upper_boundw polach o dużej kardynalności; agregacja terms bez limitu, zwracająca tysiące bucketów, jest odrębnym problemem wydajnościowym, niezależnym od opisanej wyżej kwestii wielokrotnego wyboru. - Nigdy nie należy obliczać liczb fasety przez ładowanie surowych wierszy do pamięci aplikacji. Jeśli ktoś łapie się na pisaniu pętli po wynikach zapytania w celu zbudowania zliczenia lub zakresu, ta pętla powinna znaleźć się w agregacji, a nie w kodzie.
Kiedy Elasticsearch nie jest odpowiedzią
Nic z tego nie oznacza, że każdy katalog potrzebuje Elasticsearch. Sklep z kilkoma tysiącami produktów i garstką faset rzadko zauważy różnicę, jaką daje dedykowana wyszukiwarka, a utrzymywanie klastra ma własny koszt eksploatacji: pamięć, dysk oraz ktoś, kto potrafi przeprowadzić ponowne indeksowanie, gdy zmienia się schemat. Powyższe liczby zaczynają mieć znaczenie dopiero wtedy, gdy liczba faset, wielkość katalogu albo jedno i drugie stają się na tyle duże, że naiwne zliczanie mierzy się w sekundach, a nie w milisekundach. Jeśli Państwo jeszcze do tego nie doszli, dobrze zoptymalizowane zapytanie do bazy danych jest całkowicie rozsądnym wyborem, a powyższa lista kontrolna również ma tu zastosowanie: kwestia I/LUB i pułapka wielokrotnego wyboru to ten sam problem, bez względu na to, jaki silnik odpowiada.
W skrócie
Wyszukiwanie fasetowe to nie jedno zapytanie, lecz zapytanie plus agregacja na filtr, i niemal wszystkie rzeczywiste błędy biorą się z naruszenia tego modelu: punktowania tego, co powinno być filtrem, liczenia fasety z uwzględnieniem jej własnego, już zastosowanego warunku albo odpowiadania na zliczanie i na kliknięcie dwiema różnymi definicjami grupy. Mechanizm agregacji Elasticsearch porządnie rozwiązuje problem skali, w jednym przebiegu na zindeksowanych danych zamiast jednego zapytania na wartość fasety, i jest to konkretny powód, dla którego powyższe liczby 43 i 11 sekund pojawiły się w silniku ograniczonym do bazy danych, a nie w silniku do tego stworzonym. Pracujemy nad tym na co dzień w cll_essearch, naszym module wyszukiwania fasetowego dla PrestaShop, zarówno na natywnym sterowniku, jak i na Elasticsearch, OpenSearch oraz Meilisearch (zob. przewodnik instalacji), jeśli chcą Państwo zobaczyć tę samą logikę zastosowaną w sklepie internetowym, a nie w surowym indeksie.
Wyszukiwarka Elasticsearch i nawigacja fasetowaBłyskawiczne wyszukiwanie z tolerancją literówek i nowoczesna nawigacja fasetowa we wbudowanym silniku lub w o…99,99 €Zobacz produkt