This document discusses using Elasticsearch on AWS for text search capabilities. It provides an overview of what Elasticsearch is, when it would be needed (for faster search, large data search, or powerful text queries), and how it works by indexing and analyzing documents and terms to build an inverted index. It also discusses some limitations of using AWS Elasticsearch, including limited configuration options and lack of script support. The document then provides an example of how Elasticsearch was used at Tidio Chat to improve search performance over MariaDB for tasks like fetching conversations.
2. What’s Elasticsearch?
● Search & analytics engine
● Fast
● Scalable
● Distributed
● Full text search capabilities
● (near) Real time indexing
● Document oriented
● Schema free
3. When do I need it?
● If needed faster search mechanism
● If needed searching in large amount of data
● If needed powerful full text queries
4. How does it work?
Input Document Analyzer Terms Index
5. Inverted Index
Id Content
1 The quick brown fox jumped
over the lazy dog
2 Quick brown foxes leap over
lazy dogs in summer
analysis
Term Doc_1 Doc_2
brown X X
dog X X
fox X X
in X
jump X X
lazy X X
over X X
quick X X
summer X
the X X
6. Logical data structures
● Elasticsearch (cluster) contains indexes
● Index contains types
● Type contains documents
● Mappings are assigned to types
● Index aliases (optional) can point to indices and modify queries (e.g. add
filter)
● There are no classic SQL-like relationships (!)
8. Physical data structures
● Cluster contains nodes
● Index is stored in one or more shards (single shard is a Lucene index
instance)
● Single node contains shards of different indexes
9. How to deal with lack of joins?
● Denormalization
● Client-side joins
● Parent-child relationships
10. Elasticsearch in Tidio
● Tidio Chat - business communication tool where business owners (operators)
communicate with their customers (visitors)
● www.tidiochat.com
● ES used instead of MariaDB to perform:
○ Fetching last conversations in project
○ Perform search by message content and visitor email in project’s conversation history
11. Relations in Tidio Chat
Message
id
visitor_id
operator_id
content
time
Project
public_key
Visitor
id
project_public_key
name
email
Operator
id
project_public_key
12. Message document schema
● Project’s public key added to document
● Search by email performed in MariaDB
● Time mapped as date explicitly
● Client-side join with Visitor
Message
id
visitor_id
operator_id
project_public_key
content
time
13. Design decisions
● Questions
a. What indexes should be created?
b. What types should be created?
c. How shards should be distributed among nodes and indexes?
● Things to consider
a. Search in smaller dataset usually means faster search results
b. Index with small number of shards does not scale efficiently to new nodes
c. Types are used mainly to assign mappings, they are not separated “search entities” so there is
no direct performance boost from using many types
d. Index doesn’t need to represent domain entity
14. Ideas?
Index for each project, one type inside index
● 250k projects = 250k indexes
● Adding new index is slow
● Large overhead associated with shards and indices count
15. Ideas?
One index and separate type for each project
● Large index
● Nodes scaling up only to number of shards in particular index (default 5, no
auto index splitting)
● Every query would go through all shards and filter by project_public_key (large
amount of data to search in)
16. Ideas?
Group projects and create an index for each group
● Limited amount of data to search in
● Reasonable number of shards, which still can scale up to many nodes
● Possibility to add alias for each project and search as it would be separate
index
● Projects may be grouped by language and use specific analyzers
17. Amazon Web Services Elasticsearch cluster
● Quick and easy to install
● Extremely limited configuration options
● Limited query options (scripts disabled)
● Can be used with standard AWS authentication
● There is no AWS SDK that supports ES, so users have to write code that sign
requests manually
18. PHP clients for ES
● elasticsearch/elasticsearch
○ https://github.com/elastic/elasticsearch-php
○ Low level ES client
○ One-to-one mapping with REST API
○ Pluggable architecture (can use custom request handler and send AWS signed requests)
○ Does all things that you don’t want to know about, e.g. discovery of cluster nodes, load
balancing, Keep-Alive connections
○ Accepts queries in JSON
● ruflin/elastica
○ https://github.com/ruflin/Elastica
○ High level client
○ Classes representing indices/queries/terms - you do not have to write JSONs
21. Indexing performance
● Check your mappings!
● Set fields as not analyzed
● Disable _all field
● Tune your analyzer and index_options (advanced)
22. Search performance
● Unfair comparison
● Over 26 million documents
● Time of PHP requests in seconds
QueryService MariaDB (8 CPU) Elasticsearch (4 CPU)
Search by text 14.16 (σ=0.51) 0.80 (σ=0.20)
Last conversations 4.77 (σ=0.45) 0.87 (σ=0.23)
Czym jest Elasticsearch?
Silnik wyszukiwania
Szybki, skalowalny, rozproszony, z możliwością wyszukiwania pełnotekstowego, wyniki są dostępne praktycznie natychmiastowo po zaindeksowaniu, document oriented i schema free.
Kiedy myśleć o elasticsearchu?
Kiedy potrzebujesz wyników wyszukiwania szybciej, potrzebujesz wyszukiwać w dużych ilościach danych (w dokumentacji piszą nawet o petabajtach), potrzebujesz zaawansowanego wyszukiwania pełnotekstowego
Dane wrzucamy do elasticsearcha jako dokumenty, później przechodzą one przez Analyzer, zamieniane są na tokeny i zapisywane do indeksu. Zamiennie używa się słów term, token czy word.
Dane w ES przechowywane są w postaci odwróconego indeksu.
Zmiana na tokeny/termy/słowa i zapis czy token jest w dokumencie
-Lowercase
-Foxes i dogs zmodyfikowane do liczby pojedynczej (stemming - wyznaczenie trzonu)
-Jumped i leap to synonimy zapisane jako jump
-Można jeszcze pozbyć się the na podstawie listy stopwords.
Ciekawostka - przykład z dokumentacji, ale Elasticsearch nie zachowuje się tak domyślnie.
Co to index, typ, dokument.
Odwołanie do baza danych, tabela, wiersz w SQL
Co to mapping, często pomijany, a ważny. Jeśli zapomnimy o nim, to musimy przeindeksować cały indeks.
W rzeczywistości typ dokumentu to własność (nie są fizycznie grupowane po typie) i służy głównie do wybrania mappingu.
Aliasy, żeby łatwiej konstruować zapytania.
Brak relacji!
Graficzna wizualizacja struktur danych
Ważne, kluczowe, żeby zrozumieć kolejne slajdy
Jak to wszystko jest zapisywane fizycznie
Klaster zawiera nody, index jest zapisany w shardach, na danym node są shardy różnych indeksów.
Lucene index - Lucene to tak naprawdę silnik wyszukiwania, a Elasticsearch jest nakładką zapewniającą skalowalność, przyjemne api itp.
Denormalization
Put all data to one document
Types
Classic flat structure
Nested objects
After update of related entity, all associated objects has to be updated manually
Client-side joins
Few requests
Increase client load
Parent-child relationships
Documents has to be on the same shard
Generally slow
Zastosowanie w naszym przypadku
Tidio Chat - produkt Tidio służący właścicielom biznesów do komunikacji ze swoimi klientami.
Do podejrzenia jak działa na tidiochat.com
Jeden zainstalowany komunikator na stronie naszego klienta, to jeden projekt.
Używamy elasticsearcha w miejsce MariaDB do dwóch requestów - pobierania ostatnich konwersacji w projekcie i wyszukiwania w treści wiadomości i emailu visitora.
Tak wyglądają relacje w naszej bazie danych. Projekt identyfikowany przez klucz publiczny, Visitor przypisany do projektu posiadający name email i inne dane. Operator również przypisany do projektu. Wiadomość wysyłana jest pomiędzy operatorem i visitorem, ma treść i czas wysłania. To oczywiście uproszczony model.
Potrzebujemy szukać zarówno po mailu visitora, treści wiadomości, jak i zgrupować po visitor_id i wyświetlić ostatnią wiadomość. W widoku potrzebujemy również danych o visitorze - np. nazwę.
Klucz projektu dodany do dokumentu
Czas wiadomości musieliśmy jawnie zapisać jako typ time, ponieważ domyślny mapping nie złapal, że są tam daty
Wyszukiwanie po mailu visitora dalej na MariaDB
Pobranie danych visitora na MariaDB
Mając już dokument, trzeba zastanowić się gdzie go umieścić, jak go wkomponować w strukturę indexów, typów i dokumentów.
Pytanie też jak shardy powinny być rozłożone po nodach i indeksach.
Warto pamiętać o tym, że:
Wyszukiwanie w małym zbiorze danych pozwala na szybsze uzyskanie wyniku.
Indeksy z małą liczbą shardów źle się skalują. Z drugiej strony zbyt dużo shardów również powoduje duży narzut.
Typy służą głównie do mappingów, w ramach indeksu dokumenty o danym typie nie są zgrupowane razem.
Indeksy nie muszą reprezentować jakiejś biznesowej encji. W dokumentacji jest masa przykładów, kiedy zaleca się coś innego, więc nie jest to tak, jak w przypadku tabeli bazy danych.
Pomysły na rozwiązanie
Osobny indeks dla każdego projektu, jeden typ w każdym z indeksów
Definitywnie za dużo shardów
Dodawanie indeksów trwa długo, zapisuje się struktura indeksu, testowaliśmy to i import trwałby miesiącami
Każdy shard zżera zasoby (procesy, ram)
Jeden duży indeks i osobny typ dla każdego projektu
Bardzo duży indeks, trzeba by wrzucić na start dużą liczbę shardów, żeby uniknąć reindeksowania w przyszłości. To spowoduje duży narzut póki nie osiągnie się dużej liczby węzłów,
Dużo danych do wyszukiwania za każdym razem, ale to może być zmniejszone przez document routing, który pozwala nam wybrać shard, do którego trafi dany dokument i wyszukiwać tylko w nim. Jest to szybkie, za to minus jest taki, że jak ten shard padnie, to nie otrzymamy wyników w ogóle.
Nasze rozwiązanie
Podzieliliśmy projekty na grupy i dodaliśmy osobny indeks dla każdej z grup.
Dzięki temu otrzymujemy sensowną liczbę węzłów i shardów i padnięcie jednego shardu nie powoduje fuckupu u klienta..
Można poszaleć i dla każdego z projektów dodać alias, który będzie dodawał filtr. Nie udało nam się tego zrobić, bo nie da się założyć aliasu z filtrem, po nałożeniu którego nie ma jeszcze żadnej wiadomości. Obsłużenie tego w naszej architekturze wymagałoby sporo gimnastyki, a to tak naprawdę tylko lukier składniowy. Nie wiem też jak zachowuje się 250 tys aliasów. Nie wiem czy to ograniczenie Elasticsearcha samego w sobie, czy instancji na AWS.
Projekty pogrupowane po językach mogą używać różnych ustawień analyzerów, co pomoże w dostosowaniu wyszukiwania do języka.
Dwie dojrzałe biblioteki, używamy pierwszej, ale druga może się przydać.
W SQL da się zrobić prawie wszystko. Będzie to wolne, zrobi tmp table, ale zwróci poprawny wynik. Tutaj często czegoś po prostu nie da się policzyć, dlatego dobrze wrzucić część danych i przetestować zapytania przed decyzją o użyciu na produkcji. My mieliśmy problem z pobraniem ostatniej wiadomości w agregacjach. Da się to zrobić w zapytaniu bez agregacji, w agregacjach nie.
Nie ma wsparcia dla stronicowania w wynikach agregacji. Jest to najbardziej pożądana funkcjonalność na githubie, jednak zespół nie planuje tego wprowadzić. Ludzie obchodzą to na różne sposoby - najbardziej popularny polega na pobraniu po prostu całego wyniku, zapisania po stronie klienta i realizacji stronicowania tam.
U nas skorzystaliśmy z tego, że nie potrzebujemy stałej liczby wyników dla każdej strony, tylko możemy zwyczajnie “dociągać” kolejne wyniki (infinite scroll). Korzystamy z tego przy wyświetlaniu ostatnich konwersacji, gdzie wiadomości są grupowane po id visitora. W każdym requeście pobieramy wszystkie ostatnie wiadomości z takim limitem, jaki po zgrupowaniu średnio daje jedną stornę visitorów. Po stronie PHP filtrujemy, aby zwrócić tylko ostatnią wiadomość dla danego visitora. Przy pobraniu kolejnej strony przesyłamy do ES listę visitorów, których wiadomości mają być pominięte. Testy pokazały, że jest to najszybsze, a kolejne requesty mimo rosnącej liczby visitorów do pominięcia nie różnią się praktycznie czasami odpowiedzi.
Ograniczenia ES na AWS
Threadpool bulk queue_size
To jest bardzo kłopotliwe. Parametr ten odpowiada za liczbę dokumentów, które elasticsearch jest w stanie zapisać sobie w kolejce do zaindeksowania przy operacjach typu bulk. Nie da się tego zmienić, jest bardzo niskie. Wielkość kolejki powinna być w okolicach 20-50 MB, u nas 50 dokumentów to kilkaset kilobajtów. Przez to indeksowanie dokumentów w naszym przypadku trwało tydzien. Dosłownie 7 dni. Było to co prawda 20 mln rekordów, ale bez przesady. Obciążenie procesora było w tym czasie znikome.
Druga rzecz to brak wsparcia dla skryptów. To znacząco ogranicza możliwości pisania zapytań.
Planujemy przejść na Elasticsearcha postawionego ręcznie
Jak optymalizować wydajność indeksowania
Najważniejsze są mappingi
Dla pól typu email, kod pocztowy, wszelkie id itp. nie jest potrzebna analiza i zamiana na tokeny. Pozwala to też na filtrowanie po dokładnych wartościach. Trzeba ustawić dla pól typu string, bo te są domyślnie analizowane.
Zablokuj pole _all. Zamknie to możliwość wyszukiwania w całym indeksie (bez podania pola), ale nie jest to zazwyczaj potrzebne i pozwala zaoszczędzić czas i miejsce na serwerze.
Warto też dostosować rodzaj analizatora, w tym tokenizera i dostosować pod niego dane, ktore są zapisywane w indeksie. To nie tylko termy i ich obecność w dokumencie, ale też częstotliwości występowania, pozycje itp. które są wykorzystywane w zaawansowanych wyszukiwaniach.
U nas nie przyspieszyło to znacznie, wąskie gardło jest w limicie kolejki.
I na koniec porownanie czasów.
Niesprawiedliwe porównanie, bo w MySQL było wyszukiwanie po like, baza jest pod większym obciążeniem, ale też Maria ma mocniejszy sprzęt.
Czasy całego requesta razem z frameworkiem, client-side joins itp.