Blog
Serwis autocomplete z użyciem Elasticsearch krok po kroku – część 1/3
Wstęp
Funkcjonalność podpowiadania fraz podczas wpisywania obecna jest w niemal każdej nowoczesnej wyszukiwarce internetowej. W eSky również od dawna funkcjonuje takie rozwiązanie, oparte o Apache Solr, jednak z powodu problemów z wydajnością i utrudnionym wprowadzaniem nowych funkcjonalności w ostatnim czasie została podjęta decyzja o stworzeniu jej nowej wersji. Niniejsza seria postów przedstawia proces powstawania takiego rozwiązania. Pierwszy post traktować będzie o początkach pracy, zawiera elementy teorii, niezbędne do dogłębnego poznania działania systemu. W kolejnych przedstawiony zostanie krok po kroku cały proces od zera do funkcjonalnego serwisu podpowiadania wpisywanych fraz.
Wymagania
Po konsultacjach z biznesem została stworzona lista wymagań, które nowy serwis powinien spełniać:
- system musi działać bez przerw, możliwe jednak musi być modyfikowanie zawartych w nim danych
- system powinien ignorować wielkość liter w wpisywanych frazach (kraków = Kraków)
- system powinien ignorować znaki diakrytyczne (krakow = kraków)
- system powinien potrafić zwracać wyniki dla fraz wpisanych z literówkami (krakuw = kraków)
- system powinien potrafić wyróżnić wpisaną przez użytkownika frazę w tekście
Bazując na dostępnej w firmie wiedzy, dodatkowo popartej przeglądnięciem dostępnych w sieci rozwiązań, podjęta została decyzja o budowie rozwiązania z użyciem silnika wyszukiwania Elasticsearch, który spełnia wszystkie stawiane systemowi wymagania. Dodatkowo by usprawnić prace, i wygodniej rozwijać serwis na środowiskach deweloperskich podjęto decyzję o użyciu dockera, by uzyskać izolację aplikacji w kontenerze i uspójnić środowisko, od lokalnego komputera dewelopera po środowisko produkcyjne.
Przedstawione w tym tekście modele użyte w przykładach są uproszczoną wersją modeli, używanych w produkcyjnej aplikacji, nie ma to jednak wpływu na powstałą funkcjonalność i ma na celu jak najzwięźlej przedstawić problem.
Dlaczego Elasticsearch?
Na wstępie wspomniano że podjęta została decyzja o użyciu Elasticsearch, rodzi to pytanie dlaczego Elasticsearch, i jakie jego alternatywy. Zaczynając od definicji ElaticSearch to wydajna baza danych zintegrowana z silnikiem wyszukiwania opartym o Apache Lucene, bibliotekę open-source wysokiej wydajności, przeznaczoną do wyszukiwania pełnotekstowego. Sam Elasticsearch jest bazą danych, który do wyszukiwania wykorzystuje indeksy Apache Lucene i umożliwia dostęp do skalowalnego środowiska, które wyszukiwanie wykonuje niemal w czasie rzeczywistym. Dzięki rozproszonemu modelowi możliwe jest przetwarzanie naprawdę dużych ilości danych. Wszystko to dostępne jest po RESTowym API. Najlepszą rekomendacją do użycia Elasicsearch są firmy i organizacje, które zdecydowały się na użycie tego rozwiązania, a są to m.in. Facebook, GitHub, Wikipedia czy CERN.
Głównym konkurentem Elasticsearch jest wspomniany już Apache Solr, który także bazuje na Apache Lucene, lecz na korzyść Elasticsearch przemawia niższy próg wejścia w projekt, dostępniejsze API, przystępniejszy format zapytań oraz bardzo dobra dokumentacja.
W przykładach użyto Elasticsearch w wersji 2.4 – ostatniej stabilnej w momencie pisania posta, w międzyczasie wyszła stabilna wersja 5.0, która nie powinna mieć wpływu na działanie poniższego kodu.
Środowisko
Środowisko deweloperskie może zostać uruchomione na lokalnej maszynie bardzo szybko z użyciem oficjalnego kontenera dockera. Podstawowa konfiguracja sprowadza się do pliku docker-compose.yml o poniższej treści:
elasticsearch: image: elasticsearch:2.4 ports: - "9200:9200"
Budowa kontenera odbywa się przy użyciu komendy:
docker-compose up -d
Po zaciągnięciu i zbudowaniu obrazów API Elasticsearch dostępne jest pod adresem:
http://127.0.0.1:9200/
Przykładowa odpowiedź zawiera między innymi nazwę klastra oraz wersję Elasticsearch oraz Apache Lucene.
{ name: "Erg", cluster_name: "elasticsearch", version: { number: "2.4.0", build_hash: "ce9f0c7394dee074091dd1bc4e9469251181fc55", build_timestamp: "2016-08-29T09:14:17Z", build_snapshot: false, lucene_version: "5.5.2" }, tagline: "You Know, for Search" }
Koncepcja Elasticsearch
Na początek kilka słów o koncepcji wokół której zbudowany jest Elasticsearch.
Cluster to kolekcja jednego lub więcej Nodeów, które są pojedynczymi serwerami, w których przetrzymywane, analizowane i wyszukiwane są dane. Index jest kolekcją dokumentów o zbliżonej charakterystyce, jest identyfikowany nazwą, po której odwołuje się do niego podczas wszelkich operacji takich jak dodawanie danych, aktualizacja, usuwanie czy wyszukiwanie. Cluster zawierać może dowolną ilość indeksów. Index zawierać może dowolną liczbę Typeów, które porównać można do tabeli w bazie danych. Typy mogą mieć dowolną strukturę, definiowaną przez przechowywane w nim Documenty. Dokumenty przechowywane są w formacie JSON i muszą znajdować się w określonym typie i określonym indeksie. Każdy indeks składa się z jednego lub więcej Shardów – jest to część indeksu, która znajdować może się na innym fizycznym serwerze. Pojedynczy Shard to wewnętrznie indeks Lucene, w którym przechowywane może być ponad 2 miliardy dokumentów.
By przekonać się jak zbudowany jest lokalny klaster Elasticsearch wystarczy w przeglądarce wykonać zapytanie
http://127.0.0.1:9200/_cat/health?v
Odpowiedź:
epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1476007013 09:56:53 elasticsearch green 1 1 0 0 0 0 0 0 - 100.0%
By wyświetlić listę indeksów wystarczy zapytać:
http://127.0.0.1:9200/_cat/indices?v
Odpowiedź:
health status index pri rep docs.count docs.deleted store.size pri.store.size
Powyższe oznacza że istnieje jeden klaster o nazwie elasticsearch, który składa się z jednego node’a, status green oznacza że wszystko jest w porządku. Aktualnie nie istnieje żaden indeks.
Pierwszy indeks, pierwsze dane, pierwsze wyszukiwanie
Tworzenie nowego indeksu należy rozpocząć od ustalenia jego nazwy. Na potrzeby tekstu tworzyć będziemy serwis podpowiadający nazwy lotnisk. Utworzymy zatem indeks o nazwie airports_1. Suffix _1 jest nieprzypadkowy, będzie występował także w kolejnych przykładach, cel jego istnienia zostanie wyjaśniony w późniejszej części tekstu.
Na potrzeby tego testu wszystkie zapytania podawane będą w formacie requestów programu curl. Równie dobrze można użyć dowolnego klienta HTTP. By w odpowiedzi otrzymać sformatowany JSON należy do każdego zapytania dodać parametr pretty.
Request o utworzenie indeksu airports_1:
curl -XPUT '127.0.0.1:9200/airports_1?pretty'
Odpowiedź oznaczająca powodzenie operacji:
{ "acknowledged" : true }
Teraz można zacząć operacje dodawania danych do indeksu airports_1, jak wspomniano dokumenty muszą należeć do określonego typu, nazwijmy go airport.
Jeżeli nie zaznaczono inaczej, operacje przeprowadzane będą na poniższym zbiorze danych.
Kod | Nazwa | Nazwa Miasta | Nazwa Kraju |
---|---|---|---|
KRK | Balice | Kraków | Polska |
KAT | Pyrzowice | Katowice | Polska |
WMI | Modlin | Warszawa | Polska |
WAW | Okęcie | Warszawa | Polska |
WRO | Strachowice | Wrocław | Polska |
LGW | Gatwick | Londyn | Wielka Brytania |
STN | Stansted | Londyn | Wielka Brytania |
LHR | Heathrow | Londyn | Wielka Brytania |
Dodawanie danych do indeksu airports_1 i typu airport odbywa się metodą POST w poniższy sposób:
curl -XPOST '127.0.0.1:9200/airports_1/airport?pretty' -d ' { "code": "KRK", "name": "Balice", "cityName": "Kraków", "countryName": "Polska" }' curl -XPOST '127.0.0.1:9200/airports_1/airport?pretty' -d ' { "code": "KAT", "name": "Pyrzowice", "cityName": "Katowice", "countryName": "Polska" }' curl -XPOST '127.0.0.1:9200/airports_1/airport?pretty' -d ' { "code": "WMI", "name": "Modlin", "cityName": "Warszawa", "countryName": "Polska" }' curl -XPOST '127.0.0.1:9200/airports_1/airport?pretty' -d ' { "code": "WAW", "name": "Okęcie", "cityName": "Warszawa", "countryName": "Polska" }' curl -XPOST '127.0.0.1:9200/airports_1/airport?pretty' -d ' { "code": "WRO", "name": "Strachowice", "cityName": "Wrocław", "countryName": "Polska" }' curl -XPOST '127.0.0.1:9200/airports_1/airport?pretty' -d ' { "code": "LGW", "name": "Gatwick", "cityName": "Londyn", "countryName": "Wielka Brytania" }' curl -XPOST '127.0.0.1:9200/airports_1/airport?pretty' -d ' { "code": "STN", "name": "Stansted", "cityName": "Londyn", "countryName": "Wielka Brytania" }' curl -XPOST '127.0.0.1:9200/airports_1/airport?pretty' -d ' { "code": "LHR", "name": "Heathrow", "cityName": "Londyn", "countryName": "Wielka Brytania" }'
Po dodaniu pierwszych danych Elaticsearch automatycznie ustali ich format i typ. By sprawdzić jaki wewnętrzny format mają dane należy zapytać endpoint _mapping.
Przykładowy request:
curl -XGET '127.0.0.1:9200/airports_1/_mapping?pretty'
Odpowiedź:
{ "airports_1" : { "mappings" : { "airport" : { "properties" : { "cityName" : { "type" : "string" }, "code" : { "type" : "string" }, "countryName" : { "type" : "string" }, "name" : { "type" : "string" } } } } } }
Elasticsearch automatycznie stworzył typ airport i poprawnie wykrył i zmapował pola z wprowadzonych danych na pola dokumentów w bazie.
Dysponując zaindeksowanymi danymi można zacząć wykonywać pierwsze zapytania do Elaticsearch przy użyciu URI Search:
Najprostszy przykład, bez kryteriów:
curl -XGET '127.0.0.1:9200/airports_1/airport/_search?pretty'
Lotnisko o kodzie KRK:
curl -XGET '127.0.0.1:9200/airports_1/airport/_search?q=code:KRK&pretty'
Wszystkie lotniska w Warszawie:
curl -XGET '127.0.0.1:9200/airports_1/airport/_search?q=cityName:Warszawa&pretty'
Pierwsze lotnisko w Polsce:
curl -XGET '127.0.0.1:9200/airports_1/airport/_search?q=countryName:Polska&size=1&pretty'
Trzecie lotnisko w Londynie:
curl -XGET '127.0.0.1:9200/airports_1/airport/_search?q=cityName:Londyn&from=2&size=1&pretty'
Lotniska w Londynie posortowane alfabetycznie po nazwie:
curl -XGET '127.0.0.1:9200/airports_1/airport/_search?q=cityName:Londyn&sort=name:asc&pretty'
Metoda ta, zwana URI Search dobrze sprawdza się w prostych przypadkach, jednak wraz ze wzrostem skomplikowania jej składnia staje się coraz trudniejsza do umieszczenia w URI. Rozwiązaniem jest użycie języka zapytań DSL, który jest częścią Elasticsearch. Każde z tych wyszukań można przedstawić w formie zapytań w języku DSL. Endpoint _search pozwala na wygodniejsze prowadzenie wyszukań, warto zwrócić uwagę że od teraz zapytania wykonywane będą metodą POST.
Najprostszy przykład, bez kryteriów
curl -XPOST '127.0.0.1:9200/airports_1/airport/_search?pretty' -d ' { "query": { "match_all": {} } } '
Lotnisko o kodzie KRK:
curl -XPOST '127.0.0.1:9200/airports_1/airport/_search?pretty' -d ' { "query": { "match": { "code": "KRK" } } } '
Wszystkie lotniska w Warszawie:
curl -XPOST '127.0.0.1:9200/airports_1/airport/_search?pretty' -d ' { "query": { "match": { "cityName": "Warszawa" } } } '
Pierwsze lotnisko w Polsce:
curl -XPOST '127.0.0.1:9200/airports_1/airport/_search?pretty' -d ' { "query": { "match": { "countryName": "Polska" } }, "size": 1 } '
Trzecie lotnisko w Londynie:
curl -XPOST '127.0.0.1:9200/airports_1/airport/_search?pretty' -d ' { "query": { "match": { "cityName": "Londyn" } }, "from": 2, "size": 1 } '
Lotniska w Londynie posortowane alfabetycznie po nazwie:
curl -XPOST '127.0.0.1:9200/airports_1/airport/_search?pretty' -d ' { "query": { "match": { "cityName": "Londyn" } }, "sort": { "name": "asc" } } '
Podsumowanie
Niniejszy post jest jedynie wstępem do szerokiego tematu podpowiadania fraz. Dysponując skonfigurowanym środowiskiem, znając wewnętrzną strukturę indeksów Elasticsearch oraz znając podstawowe metody wyszukiwania dysponujemy dostateczną wiedzą teoretyczną i umiejętnościami praktycznymi, by w kolejnych częściach zacząć tworzyć system podpowiadania wpisywanych fraz.