Witajcie przyjaciele! Na naszym forum udzielają się ludzie o różnym poziomie doświadczenia - i to jest świetne! Ale teraz szukam prawdziwych tytanów Ruby!
Elasticsearch to wyszukiwarka oparta na zaufanej i dojrzałej bibliotece - Apache Lucene. Ogromna aktywność w git projekt repozytorium i implementacja w takich projektach jak GitHub, SoundCloud, Stack Overflow czy LinkedIn świadczą o jego ogromnej popularności. Część "Elastic" mówi wszystko o naturze systemu, którego możliwości są ogromne: od prostego wyszukiwania plików na małą skalę, poprzez odkrywanie wiedzy, aż po analizę dużych zbiorów danych w czasie rzeczywistym. To, co sprawia, że Elastic jest potężniejszy od konkurencji, to zestaw domyślnych konfiguracji i zachowań, które pozwalają na stworzenie klastra i rozpoczęcie dodawania dokumentów do indeksu w ciągu kilku minut. Elastic skonfiguruje klaster za nas, zdefiniuje indeks i określi typy pól dla pierwszego pozyskanego dokumentu, a po dodaniu kolejnego serwera automatycznie zajmie się podziałem danych indeksu pomiędzy serwerami.Niestety, wspomniana automatyzacja sprawia, że nie wiemy co implikują domyślne ustawienia i często okazuje się to mylące. Tym artykułem rozpoczynam serię, w której zajmę się najpopularniejszymi gotchasami, które można napotkać w procesie tworzenia aplikacji opartych o Elastic.
Liczba odłamków nie może zostać zmieniona
Zindeksujmy pierwszy dokument używając API indeksu:
$ curl -XPUT 'http://localhost:9200/myindex/employee/1' -d '{
"first_name" : "Jane",
"last_name" : "Smith",
"steet_number": 12
}'
W tym momencie Elastic tworzy dla nas indeks o nazwie myindex. To czego tutaj nie widać, to liczba shardów przypisanych do indeksu. Shardy można rozumieć jako poszczególne procesy odpowiedzialne za indeksowanie, przechowywanie i przeszukiwanie części dokumentów całego indeksu. Podczas procesu indeksowania dokumentów, elastic decyduje, w którym shardzie powinien znaleźć się dany dokument. Jest to oparte na następującym wzorze:
shard = hash(document_id) % number_of_primary_shards
Teraz jest jasne, że liczba podstawowych shardów nie może zostać zmieniona dla indeksu zawierającego dokumenty. Tak więc, przed indeksowaniem pierwszego dokumentu, zawsze należy utworzyć indeks ręcznie, podając liczbę shardów, która jest wystarczająca dla wolumenu indeksowanych danych:
$ curl -XPUT 'http://localhost:9200/myindex/' -d '{
"settings" : {
"number_of_shards" : 10
}
}'
Wartość domyślna dla number_of_shards
Oznacza to, że indeks może być skalowany do maksymalnie 5 serwerów, które zbierają dane podczas indeksowania. Dla środowiska produkcyjnego wartość shardów powinna być ustawiona w zależności od oczekiwanej częstotliwości indeksacji i wielkości dokumentów. Dla środowisk deweloperskich i testowych zalecam ustawienie wartości na 1 - dlaczego? Zostanie to wyjaśnione w następnym akapicie tego artykułu.
Sortowanie wyników wyszukiwania tekstowego przy stosunkowo niewielkiej liczbie dokumentów
Gdy wyszukujemy dokument za pomocą frazy:
$ curl -XGET 'http://localhost:9200/myindex/my_type/_search' -d
'{
"query": {
"match": {
"title": "The quick brown fox"
}
}
}'
Elastic przetwarza wyszukiwanie tekstu w kilku krokach:
- fraza z zapytania jest konwertowana do identycznej postaci, w jakiej dokument został zindeksowany, w naszym przypadku będzie to zestaw terminów:
["szybki", "brązowy", "lis"].
("the" zostało usunięte, ponieważ jest nieistotne),
- indeks jest przeglądany w celu wyszukania dokumentów zawierających co najmniej jedno z wyszukiwanych słów,
- Każdy pasujący dokument jest oceniany pod kątem dopasowania do wyszukiwanej frazy,
- Wyniki są sortowane według obliczonej trafności, a pierwsza strona wyników jest zwracana użytkownikowi.
W trzecim kroku brane są pod uwagę między innymi następujące wartości:
- ile słów z wyszukiwanej frazy znajduje się w dokumencie
- jak często dane słowo występuje w dokumencie (TF - term frequency)
- czy i jak często pasujące słowa występują w innych dokumentach (IDF - inverse document frequency) - im bardziej popularne słowo w innych dokumentach, tym mniej znaczące
- Jak długi jest dokument
Funkcjonowanie IDF jest dla nas ważne. Elastic ze względów wydajnościowych nie oblicza tej wartości dla każdego dokumentu w indeksie - zamiast tego każdy shard (index worker) oblicza swój lokalny IDF i wykorzystuje go do sortowania. Dlatego podczas przeszukiwania indeksu przy małej liczbie dokumentów możemy uzyskać znacząco różne wyniki w zależności od liczby shardów w indeksie i rozkładu dokumentów.
Wyobraźmy sobie, że mamy 2 shardy w indeksie; w pierwszym znajduje się 8 dokumentów indeksowanych słowem "fox", a w drugim tylko 2 dokumenty z tym samym słowem. W rezultacie słowo "fox" będzie się znacznie różnić w obu shardach, co może prowadzić do błędnych wyników. Dlatego do celów rozwojowych należy utworzyć indeks składający się tylko z jednego głównego shardu:
$ curl -XPUT 'http://localhost:9200/myindex/' -d
'{ "settings" : { "number_of_shards" : 1 } }'
Wyświetlanie wyników "odległych" stron wyszukiwania zabija klaster
Jak już pisałem w poprzednich akapitach, dokumenty w indeksie są współdzielone pomiędzy całkowicie indywidualnymi procesami indeksu - shardami. Każdy proces jest całkowicie niezależny i zajmuje się tylko dokumentami, które są do niego przypisane.
Gdy przeszukujemy indeks z milionami dokumentów i czekamy na 10 najlepszych wyników, każdy shard musi zwrócić 10 najlepiej dopasowanych wyników do klastra węzełktóry zainicjował wyszukiwanie. Następnie odpowiedzi z każdego shardu są łączone i wybieranych jest 10 najlepszych wyników wyszukiwania (w ramach całego indeksu). Takie podejście pozwala efektywnie rozdzielić proces wyszukiwania pomiędzy wiele serwerów.
Wyobraźmy sobie, że nasza aplikacja pozwala na przeglądanie 50 wyników na stronę, bez ograniczeń dotyczących liczby stron, które mogą być przeglądane przez użytkownika. Pamiętajmy, że nasz indeks składa się z 10 podstawowych shardów (po 1 na serwer).
Zobaczmy, jak będzie wyglądać pozyskiwanie wyników wyszukiwania dla pierwszej i setnej strony:
Strona nr 1 wyników wyszukiwania:
- Węzeł odbierający zapytanie (kontroler) przekazuje je do 10 shardów.
- Każdy shard zwraca 50 najlepiej dopasowanych dokumentów posortowanych według trafności.
- Po otrzymaniu odpowiedzi z każdego shardu, kontroler scala wyniki (500 dokumentów).
- Nasze wyniki to 50 najlepszych dokumentów z poprzedniego kroku.
Strona nr 100 wyników wyszukiwania:
- Węzeł odbierający zapytanie (kontroler) przekazuje je do 10 shardów.
- Każdy shard zwraca 5000 najlepiej dopasowanych dokumentów posortowanych według trafności.
- Po otrzymaniu odpowiedzi z każdego shardu, kontroler scala wyniki (50000 dokumentów).
- Nasze wyniki to dokumenty z poprzedniego kroku umieszczone na pozycjach 4901 - 5000.
Zakładając, że jeden dokument ma rozmiar 1 KB, w drugim przypadku oznacza to, że ~50 MB danych musi zostać przesłanych i przetworzonych w klastrze, aby wyświetlić 100 wyników dla jednego użytkownika.
Nietrudno zauważyć, że ruch sieciowy i obciążenie indeksu znacznie wzrasta z każdą kolejną stroną wyników. Dlatego też nie zaleca się udostępniania użytkownikowi "dalekich" stron wyszukiwania. Jeśli nasz indeks jest dobrze skonfigurowany, to użytkownik powinien znaleźć interesujący go wynik na pierwszych stronach wyszukiwania, a my uchronimy się przed niepotrzebnym obciążeniem naszego klastra. Aby udowodnić tę regułę, sprawdź, do jakiej liczby stron wyników wyszukiwania pozwalają przeglądać najpopularniejsze wyszukiwarki internetowe.
Interesująca jest również obserwacja czasu odpowiedzi przeglądarki dla kolejnych stron wyników wyszukiwania. Na przykład, poniżej można znaleźć czasy odpowiedzi dla poszczególnych stron wyników wyszukiwania w wyszukiwarce Google (wyszukiwaną frazą była "wyszukiwarka"):
| Strona wyników wyszukiwania (10 dokumentów na stronie) | Czas odpowiedzi |
|——————————————–|—————|
| 1 | 250ms |
| 10 | 290ms |
| 20 | 350ms |
| 30 | 380ms |
| 38 (ostatni dostępny) | |
W następnej części przyjrzę się bliżej problemom związanym z indeksowaniem dokumentów.