Silniki wyszukiwania Solr i Elasticsearch

Solr i Elasticsearch to dwa silniki wyszukiwania (lub bazy NoSQL) oparte o bibliotekę Lucene. Podobne, a zarazem prezentujące odmienne podejścia w zaspokajaniu potrzeb użytkowników. W tym artykule chciałbym dokonać porównania obu projektu jak i pakietów dostępnych w Pythonie.

Gdy nasza aplikacja potrzebuje zaawansowanego wyszukiwania po różnych parametrach, albo wyszukiwania pełnotekstowego uwzględniającego np. synonimy z oceną jakości dopasowania to nasza uwaga dość szybko skupi się na silnikach wyszukiwania lub usługach oferujących takie rozwiązania. Solr i Lucene to projekty z długą historią. Elasticsearch dołączył do nich nieco później.

Kto jest kim w rodzinie Lucene?

Elasticsearch bardzo często znajduje zastosowanie jako silnik do zbierania i przeszukiwania logów. Gotowy zestaw zazwyczaj zawiera także Logstash do przetwarzania i/lub Kibanę do wizualizacji i przeszukiwania logów. Solr także może służyć do tego samego, choć nie jest to jego najpopularniejsze zastosowanie. Historycznie można powiedzieć że był i jest stosowany jako silnik wyszukiwania. Także Elasticsearch może pełnić taką funkcję. Oba projekty oparte są o Lucene, która implementuje większość ich logiki. Skąd więc dwa projekty i czy tak naprawdę czymś się różnią?

Zanim przejdziemy do porównywania obu projektów należy zwrócić uwagę na numery wersji i czas publikacji różnych materiałów. Społeczność jest duża i bardzo aktywna. W sieci znajdziemy liczne prezentacje i porównania sprzed jednego czy kilku lat. Podstawowe formy użycia Solr czy Elasticsearch nie zmieniły się za bardzo, natomiast zestawy funkcjonalności, sposób hostowania i zarządzania przechodził spore zmiany.

W 2010 połączono zespoły Solra i Lucene zachowując dotychczasowy podział obu projektów - Lucene jako biblioteka wyszukiwania i Solr jako serwer wyszukiwania na niej oparty zawierający pełen pakiet funkcjonalności i dodatków. Elasticsearch natomiast wybrał podejście bardziej minimalistyczne. Samo wydanie Elasticsearch nie zawiera np. panelu admina czy stosuje podejście JSON in, JSON out (gdzie Solr obsługuje różne formaty komunikacji out of the box). Brakujące funkcjonalności można dodawać poprzez wtyczki.

Różnice pomiędzy Elasticsearch i Solr

Oba projekty to dwa różne podejścia do oferowania tej samej corowej funkcjonalności wyszukiwania. Dobre pomysły i rozwiązania zazwyczaj propagują się pomiędzy projektami. Wiele firm konsultingowych, czy programistycznych będących w temacie silników wyszukiwania korzysta z obu silników w zależności od przypadku i potrzeb.

Części wspólne to Lucene, funkcjonalność pełnotekstowego wyszukiwania, zapytania, filtry, cache, fasety, czy wsparcie do pracy w chmurze. Różnice Solr kontra Elasticsearch to rozmiar wydań (pełny vs minimalistyczny pakiet), konfiguracja vs magia, różne podejście do zagnieżdżonych dokumentów, typów, wtyczek, sposobu domyślnego użycia, czy sposobu rozwoju obu projektów.

Lucene i Solr są rozwijane w obrębie fundacji Apache na otwartej licencji. Projekty mają bezpośrednie wsparcie firmy Lucidworks. Elasticsearch powstał jako projekt Shay Banona i wokół niego powstała społeczność jak i firma Elasticsearch BV zapewniająca wsparcie komercyjne. Oba projekty wydawane są na licencji Apache.

Wywodząc się z projektu jednoosobowego Elasticsearch łatwiej podejmuje decyzje o nowych funkcjonalnościach. W rozwoju Solra stosuje się metodologię fundacji Apache i w pewnym uproszczeniu decyzje musi podjąć społeczność a nie jedna osoba będąca autorem projektu.

Startując z Elasticsearch możemy od razu POSTem lub PUTem wstawić dokument JSON do indeksu. Silnik przeanalizuje pierwszy wstawiony dokument i określi typy danych jakie zawiera. Magia rozpoznawania typów, przechowywania danych i wyszukiwania po nich znacząco przyśpiesza pracę programistów, lecz ma swój koszt - nie zawsze predefiniowane magiczne zachowanie będzie odpowiadać naszym potrzebom. Produkcyjnie trzeba zrobić to co Solr wymaga by w ogóle zacząć go używać - określić schemat wstawianych dokumentów do danego indeksu (kolecji w Solru). Podobne podejście Elasticsearch prezentuje przy indeksowaniu i zapytaniach - gdzie analizator może być podany dla danego pola w mapie (schemacie) danego typu dokumentu, ale także np. w samym dokumencie czy w samej treści zapytania. Ułatwia to programowanie, ale trzeba uważać co wystawia się produkcyjnie.

Sposób wyszukiwania danych w Solru to querystring z wieloma wartościami pod skrótowo nazwanymi kluczami. W Elasticsearch proste wyszukiwania też mogą używać querystringów, lecz dla bardziej złożonych stosowany jest JSON. Zapis jest znacznie dłuższy, ale możliwy do zrozumienia przez osobę nie znającą szczegółów silnika wyszukiwania. Biblioteki nakładkowe np. te dla Pythona wystawiają oczywiście swoją warstwę abstrakcji.

W przypadku struktur rodzic-dziecko Solr oferuje obsługę zagnieżdżonych obiektów (Nested objects), gdzie dzieci są widoczne jako dokumenty. W przypadku Elasticsearch dostępne są trzy opcje. Domyślna to wewnętrzny obiekt (Inner objects) gdzie pole ma typ object i jest dynamicznie mapowane. Dane są spłaszczane i reprezentowane jako pole multivalue. Operacje wyszukiwania dopasują się do wartości z dowolnego wewnętrznego obiektu (nie ma rozróżnienia). Druga możliwość to obiekty zagnieżdżone (Nested objects), które działają nieco inaczej niż w Solr - są explicite typowane i nie są domyślnie widoczne (można je pobrać tylko wraz z rodzicem). Trzecia opcja to relacje rodzic - dziecko (Parent and Child), gdzie wszystkie obiekty są oddzielnymi dokumentami i mają explicite podane referencje. Ta opcja jest dość powolna a obiekty łączone są w pamięci w trakcie pobierania przy wyszukiwaniu.

Deployment w chmurze obu projektów trzyma się tych samych koncepcji, jednak różni się mocno w ich implementacji. Mamy sharding, wykrywanie nodów, replikacji i routing. Solr używa Apache Zookeepera jako swojego zarządcy, natomiast Elasticsearch ma własne rozwiązania. Zookeeper ma dobre opinie, jest battle-tested natomiast na początku 2015 roku rozwiązania Elasticsearch dopiero dojrzewały do pracy w dużych chmurach.

Poniżej dobra prezentacja opisująca te różnice. Pochodzi ze stycznia 2015 i od tego czasu nastąpiło już trochę zmian, które trzeba wziąć pod uwagę.

Biblioteki w Pythonie

django-haystack to biblioteka pozwalająca na łatwą integrację silników wyszukiwania z Django. Wspiera zarówno Solra jak i ES, lecz w chwili pisania tego artykułu nie wspiera jeszcze najnowszego Elasticsearch 5. Backendy te są też jedynymi obsługiwanymi pod Pythonem 3.

Biblioteka ta dostarcza wysokopoziomowe komponenty do obsługi indeksowania i wyszukiwania danych. Tworzymy indeks reprezentujący schemat dokumentu jaki chcemy indeksować i następnie za pomocą management commands możemy dane zaindeksować. W implementacji wyszukiwania pomogą nam gotowe widoki i komponenty.

Dla Solra dostępny jest szereg bibliotek, jak i można po prostu wykonywać żądania HTTP i parsować JSONa bez żadnej dodatkowej biblioteki (choć wtedy wszystkie potrzebne rzeczy trzeba implementować samemu). Na aktywnie rozwijane wyglądają trzy biblioteki: SolrClient, solrcloudpy oraz pysolr. Do współpracy z Zookeeperem przydać może się Kazoo.

Dla Elasticsearch mamy dwie oficjalne biblioteki: elasticsearch-py oraz wysokopoziomową nakładkę elasticsearch-dsl-py.

Solr szybki start

Do uruchomienia silników wykorzystam Dockera - można zastosować go pod Linuksem, natomiast pod innymi systemami pakiet dokera odpali Linuksa w wirtualnej maszynie. Obraz solra jest dostępny i możemy użyć go odpalić wedle instrukcji:

docker run --name my_solr -d -p 8983:8983 -t solr

Po odpaleniu pod adresem http://localhost:8983/ dostępny będzie panel admina Solra. Kolejny krok to stworzenie nowego core (pojedynczego indeksu):

docker exec -it --user=solr my_solr bin/solr create_core -c pomidor

Teraz możemy wykorzystać Pythonową bibliotekę do operacji na gotowym indeksie. Możemy pobrać domyślny schemat indeksu za pomocą SolrClient:

from pprint import pprint

from SolrClient import SolrClient


solr = SolrClient('http://localhost:8983/solr')
res = solr.schema.get_schema_fields('pomidor')
pprint(res)

Operacja ta zwróci listę czterech pól:

{'fields': [{'docValues': False,
             'indexed': True,
             'name': '_root_',
             'stored': False,
             'type': 'string'},
            {'indexed': True,
             'multiValued': True,
             'name': '_text_',
             'stored': False,
             'type': 'text_general'},
            {'indexed': False,
             'name': '_version_',
             'stored': False,
             'type': 'long'},
            {'indexed': True,
             'multiValued': False,
             'name': 'id',
             'required': True,
             'stored': True,
             'type': 'string'}],
 'responseHeader': {'QTime': 0, 'status': 0}}

Schematem zarządzać możemy też w panelu admina. Biblioteka SolrClient jako chyba jedyna z trzech implementuje obsługę API do zarządzania schematem:

from SolrClient import SolrClient


solr = SolrClient('http://localhost:8983/solr')

solr.schema.create_field('pomidor', {
     "name":"color",
     "type":"string",
     "stored":True
})
solr.schema.create_field('pomidor', {
     "name":"name",
     "type":"string",
     "stored":True
})

W chwili pisania artykułu indeksownie nie działa poprawnie (jest już pull-request), więc dla przykładu użyłem pysolr:

import pysolr

solr = pysolr.Solr('http://localhost:8983/solr/pomidor', timeout=10)
solr.add([
    {
        "id": "p1",
        "name": "Tomato",
        "color": "red",
    },
    {
        "id": "p2",
        "name": "Orangemato",
        "color": "orange",
    },
])

Po zaindeksowaniu dokumenty można wyszukać:

from SolrClient import SolrClient


solr = SolrClient('http://localhost:8983/solr')
res = solr.query('pomidor', {
    'q': 'color:red'
})
print(res.docs)

Elasticsearch szybki start

Elasticsearch też dostępny jest jako obraz Dockera. Ze względu na sposób wystawiania silnika po lokalnej sieci działa on w trybie produkcyjnym, który jest bardziej wybredny jeżeli chodzi o dostępną dla niego pamięć. Jeżeli ustawienia są za niskie - kontener padnie przy próbie użycia Elasticsearcha. Żeby temu zaradzić pod Linuksem wystarczy wykonać polecenie:

sudo sysctl -w vm.max_map_count=262144

Żeby odpalić kontener:

docker run --name my_es -d elasticsearch

Będziemy też potrzebować adres IP działającego kontenera:

docker inspect my_es

Mając adres IP możemy wykorzystać elasticsearch-py:

from datetime import datetime

from elasticsearch import Elasticsearch

es = Elasticsearch(host="ADRES.IP.ESa")
es.indices.create(index='my-index', ignore=400)
es.index(index="my-index", doc_type="test-type", id=42, body={"any": "data", "timestamp": datetime.now()})
print(es.get(index="my-index", doc_type="test-type", id=42)['_source'])

Wstawiamy dane, szukamy i dostajemy surową odpowiedź. Docelowo warto używać elasticsearch-dsl dla znacznie łatwiejszego zarządzania schematem danych:

from elasticsearch_dsl import DocType, Keyword, Text
from elasticsearch_dsl.connections import connections

connections.create_connection(hosts=["ADRES.IP.ESa"])

class Pomidor(DocType):
    name = Text(analyzer='snowball', fields={'raw': Keyword()})
    color = Text(analyzer='snowball')

    class Meta:
        index = 'vegetables'


Pomidor.init()

tomato = Pomidor(meta={'id': "p1"}, name='Tomato', color="red")
tomato.save()


orangemato = Pomidor(meta={'id': "p2"}, name='Orangemato', color="orange")
orangemato.save()

vegetable = Pomidor.get(id="p2")
print(vegetable)

Podstawy Elasticsearch

Używając relacyjnej bazy danych mamy do czynienia z bazami danych (np. serwer PostgreSQL i szereg dostępnych na nim baz danych). W ES rolę bazy danych pełni Index. Rolę tabeli pełni Type a rolę wiersza pełni Document.

Żeby stworzyć nowy index wystarczy wykonać żądanie POST /nazwa_indeksu. Żeby wstawić dokument wystarczy JSON postować na adres /nazwa_indeksu/typ_dokumentu/ID (ID można pominąć - wtedy ES wygeneruje identyfikator).

Dane mogą być analizowane lub nie. Dla analizowanych wykonywane są dopasowania pełnotekstowe, natomiast dla pól nie analizowanych - dopasowania co do wartości. Domyślnie łańcuchy są analizowane. Jeżeli interesuje Ciebie wyszukiwanie pełnotekstowe to filtry i analizatory językowe są najważniejszym elementem wpływającym na działanie silnika przy wyszukiwaniu.

Żeby wyszukać dokumenty wystarczy wysłać POSTem JSONa pod adres /nazwa_indeksu/typ_dokumentu/_search. Proste zapytania można robić także GETem poprzez querystring.

W Elasticsearch są dostępne dwie ścieżki wyciągania określonych dokumentów - Filter i Query. Filtrowanie jest dla nie analizowanych danych, opiera się o dokładne dopasowania, jest szybkie i keszowalne. Query operuje na wyszukiwaniu pełnotekstowym analizowanych danych i zwraca wyniki z oceną dopasowania. W silniku wbudowane jest wiele tego typu filtrów i zapytań (np. geolokalizacyjne).

RkBlog

Programowanie Sieciowe, 16 January 2017

Comment article
Comment article RkBlog main page Search RSS Contact