SQL LIKE – Kiedy Twój serwer płonie cz. 2

Opublikowane przez admin w dniu

Cześć, ten post jest kontynuacją krótkiej serii na temat alternatyw dla zasobożernej klauzuli LIKE w SQL Serwerze. Jeśli nie widziałeś poprzedniej części, zachęcam Cię do zapoznania się z nią. Są tam opisane zagadnienia oraz domena problemu o których będę wspominał w tej części. Część pierwszą możesz znaleźć tutaj. Jeśli rozwiązania istniejące w bazie Microsoftu nie są w jakimś stopniu satysfakcjonujące dla Twojego przypadku, to zapraszam do poniższej lektury. W tym wpisie opiszę kilka opcji wyszukiwania pełnotekstowego, jakie oferuje nam Elasticsearch.

Czym w ogóle jest Elasticsearch ?

Jest to NO-SQL’owa baza danych, która umożliwia bardzo wydajne i elastyczne wyszukiwanie pełnotekstowe. Sam Elastic bazuje na bibliotece Lucene, która korzysta ze struktury zwanej odwróconym indeksem, o której pisałem w poprzednim wpisie. Samą bazę możemy odpytywać poprzez REST API. Elasticsearch jest także bardzo dobrze skalowalną bazą danych, wspierającą klastrowanie oraz partycjonowanie.

Co trzeba wziąć pod uwagę

Użycie oddzielnej bazy danych dla przeszukiwania naszych danych niesie ze sobą sporo pracy, więc najpierw warto się zastanowić, czy gra jest warta świeczki. Jak zwykle to zależy. Co musimy wziąć pod uwagę?

Migracja danych

Jak migrować nawet duże ilości danych opisałem już tutaj. Zapraszam do lektury.

Zadbanie o aktualizacje danych

Kod, który aktualnie w jakiś sposób modyfikuje dane po których będziemy wyszukiwać musimy  rozszerzyć tak, aby aktualizacje tych danych także w odbywały się Elasticsearchu. Pomocna tutaj może być biblioteka NEST.

Zadbanie o spójność danych

Tutaj sprawy trochę się komplikują. Nasz aktualny kod modyfikujący dane w SQL Serwerze jest transakcyjny. Kod, który będzie aktualizował nasze dane w Elasticsearchu już taki nie jest. Musimy zadbać o to, by w przypadku awarii serwera dane pozostały spójne. Do ponawiania kodu w przypadku wyjątku/błędu/awarii może pomóc biblioteka Polly. Innym rozwiązaniem może być użycie szyny danych np. RabbitMQ lub Azure Service Bus, jednakże wiąże się to z dodatkową pracą i utrzymywaniem dodatkowego rozwiązania. Być może posiadasz już w swoim projekcie taką infrastrukturę. Warto wtedy jej użyć. Jednak dalej użycie szyny, czy polityki ponawiania requestów nie będzie transakcyjne z modyfikacjami naszej tabeli w SQL Serwerze. Jak zadbać o spójność danych, gdy nasza transakcja zaczyna być rozpraszana pomiędzy usługami pisałem już tutaj.

 

Po przygotowaniu całej infrastruktury możemy w końcu zacząć przeszukiwać nasze dane. Przed tym warto wspomnieć o jednym z kluczowych narzędzi w Elasticsearchu – analizatorach.

Analyzers

Analizatory są wbudowanym mechanizmem analizy naszego tekstu. Mogą analizować nasz tekst podczas indeksowania, jak i również podczas zadawania zapytań do naszej bazy. Możemy używać wbudowanych analizatorów lub stworzyć własne. Analizatory składają się  z 3 mechanizmów:

  1. Character filters
  2. Tokenizers
  3. Token filters

Character filters

Mogą zmieniać/usuwać nasze znaki w tekście, np. usuwać znaczniki html, czy kropki z tekstu, lub zamieniać znaki specjalne na bardziej przyjazne wyszukiwaniu odpowiedniki  ę -> e. Jeden analizator może posiadać wiele filtrów znaków.

Tokenizers

Dzielą nasz tekst na tokeny. Najprostszym przykładem jest tokenizer dzielący nasz tekst usuwając białe znaki. Czyli tekst: `Kiedy Twój serwer płonie` zostanie podzielony na wyrazy: `Kiedy` Twój serwer płonie. Innym przykładem może być n-gram tokenizer, o którym wspominałem już w części pierwszej. W Elasticsearchu jest on wbudowanym narzędziem. Niestety, W SQL serwerze musimy pisać taki tokenizer sami. W jednym analizatorze musi istnieć tylko jeden tokenizer.

Token filters

Filtrują podzielone tokeny, np. eliminują z wyszukiwania i indeksowania słowa powszechne (stop/noise words), które mogą zaciemniać wynik przeszukania tj the, is, and. Taki filtr istnieje także w Full Text Search w SQL Serverze. Innym przykładem może być zamienianie tokenów, aby zawierały tylko małe litery. Filtrów tokenów może istnieć wiele w jednym analizatorze.

 

Przejdźmy teraz do samych opcji przeszukiwania indeksu. Przykłady będą z użyciem biblioteki NEST. Połączenie z Elasticsearchem i wybranym indeksem w każdym przykładzie wyglądać będzie tak:

var node = new Uri("http://localhost:9200");
var settings = new ConnectionSettings(node);
var client = new ElasticClient(settings);

var response = client.Search<Model>(s => s.Index(indexName)

 

Match

Przed przeszukaniem Match używa analizatora na naszym query. Jeśli jawnie nie użyjemy żadnego to zostanie użyty domyślny, a jeśli przynajmniej jeden token zostanie znaleziony w naszym tekście to taki dokument zostanie zwrócony jako pasujący do zapytania. Tokeny nie muszą znajdować się w dokumencie w takiej samej kolejności, w jakiej były w query. Im więcej tokenów będzie zawierał nasz dokument oraz im będą znajdować się one bliżej siebie, tym dokument zostanie lepiej wyceniony.

.Query(q => q.Match(c => c.Field(p => p.Body)
                          .Query(txt)))

Dla przykładu dla frazy You don't need Microservices

Zostanie znaleziony dokument z tekstem You need Microservices, ale także i dokument zawierający te słowa w innej kolejności np. Microservices ? Who need them ?. Im bliżej dokumentowi do frazy wyszukiwanej, tym będzie on lepiej wyceniony (będzie wyżej w wyniku wyszukiwania). Liczy się także sama ilość wyrazów w dokumencie. Fraza `You need Microservices! You need Microservices!` zostanie wyceniona prawie najlepiej. Bardzo przydatnym parametrem w tym wyszukiwaniu jest parametr cutoff_frequency oraz `fuzziness`, jednak o nich trochę później.

 

Zapytanie Znalezione tokeny Scoring Co nie zostanie znalezione
You don’t need Microservices This is CRUD app, you don’t need Microservices. Stay with modular monolith 1.58 How to center div vertically and horizontally
Right now on every conference you can hear: You need Microservices! You need Microservices! This is funny 1.24
Your app quite complex. You will need Microservices. Remember about DevOps 0.86
Hype development with Microservices ? Who need them ? 0.40

 

 

Match Phrase

To przeszukanie, podobnie jak Match użyje analizatora na naszym query. Jednak w tym przypadku nasze tokeny muszą znajdować się w dokumencie w takiej samej kolejności, w jakiej były w query, aby dany dokument został zwrócony. Możemy konfigurować odległość pomiędzy tokenami w dokumencie poprzez właściwość slop. Aby to wyszukiwanie zwróciło poszczególny dokument, musi on zawierać całą frazę, którą chcemy wyszukać.

.Query(q => q.MatchPhrase(mp => mp.Field(f => f.Body)
                                  .Query(txt))));

 

Dla przykładu dla frazy You need Microservices, zostanie znaleziony tylko dokument ze zdaniem `You need Microservices! You need Microservices!`.  Fraza Microservices ? Who need them ? nie zostanie znaleziona, podobnie jak  You don't need Microservices. Jednak jeśli ustawimy właściwość slop na przynajmniej 1 słowo (domyślnie jest to 0) to fraza You don't need Microservices zostanie znaleziona. W takiej sytuacji słowem, które to wyszukiwanie pominie będzie słowo don't.

 

Zapytanie Znalezione frazy Co może także zostać znalezione (slop:1) Co nie zostanie znalezione
You need Microservices Right now on every conference you can hear: You need Microservices! You need Microservices! This is funny Right now on every conference you can hear: You need Microservices! You need Microservices! This is funny Hype development with Microservices ? Who need them ?
This is CRUD app, you don’t need Microservices. Stay with modular monolith How to center div vertically and horizontally
Your app quite complex. You will need Microservices. Remember about DevOps

Match Phrase Prefix

Działa podobnie jak Match Phrase jednak ostatnie słowo w zdaniu będzie traktowane jako niepełne ze znakiem wildcard. Mechanizm wyszukiwania pełnotesktowego w SQL Serverze także umożliwia podobną funkcjonalność, jednak moim zdaniem działa ona w sposób niezadowalający, o czym pisałem już w poprzedniej części.

.Query(q => q.MatchPhrasePrefix(mp => mp.Field(f => f.Body)
                                        .Query(txt))));

Dla frazy  You need M znaleziony zostanie dokument z frazą You need Microservices, ale i również You need more calories. Tutaj także możemy używać właściwości slop.

Zapytanie Znalezione frazy Scoring Co może zostać znalezione (slop:1) Co nie zostanie znalezione
You need M Right now on every conference you can hear: You need Microservices! You need Microservices! This is funny 2.37 Your app quite complex. You will need Microservices. Remember about DevOps Hype development with Microservices ? Who need them ?
Your diet looks bad. You need more calories to gain weight. Maybe add more fats 2.3 Right now on every conference you can hear: You need Microservices! You need Microservices! This is funny How to center div vertically and horizontally
Your diet looks bad. You need more calories to gain weight. Maybe add more fats
his is CRUD app, you don’t need Microservices. Stay with modular monolith

 

Multi Match

Domyślnie jest to wyszukiwanie typu Match - Best Fields, czyli Match które będzie użyte na wielu polach. Typ tego wyszukiwania może być konfigurowalny. Możemy np. dla kilku pól ustawić typ wyszukiwania na Match Phrase (w przykładzie jest to enum `TextQueryType`).

.Query(q => q.MultiMatch(mm => mm.Fields(d => d.Field(f => f.Subject)
                                                            .Field(f => f.Body)
                                                            .Field(f => f.Description))
                                                            .Type(TextQueryType.BestFields)
                                                            .Query(txt))));

Multi Match  domyślnie dla dokumentu zawierającego dwa pola Topic oraz Body, będzie preferował sytuacje, gdy większość słów z frazy znajdzie się w jednym polu. Oczywiście, dokument zawierający te słowa w dwóch polach także zostanie znaleziony, ale zostanie oceniony gorzej. Opis typów, które można użyć w tym wyszukiwaniu możemy znaleźć tutaj. Domyślnie operator logiczny wyszukiwania w wielu polach to typ OR (dostępne operatory to OR oraz AND). Oznacza to, że przynajmniej jedno słowo z zapytania musi znaleźć się w przynajmniej w jednym polu.

Zapytanie Znalezione frazy Scoring
you need M topic:Microservices
body:Right now on every conference you can hear: You need Microservices! You need Microservices! This is funny
0.92
topic: CRUD App
body: This is CRUD app, you don’t need Microservices. Stay with modular monolith
0.72
topic: Microservices
body: Just joking you need more McDonalds
0.52

 

Query String Query

Jest to bardzo elastyczne wyszukiwanie, które może być bardzo proste lub bardzo skomplikowane. Udostępnia ono bardzo wiele funkcjonalności samej biblioteki Lucene. Jednak nie polecałbym wystawiać tego query jako wyszukiwarki dla użytkownika końcowego w naszym systemie. Dlaczego ? Oczywiście to zależy od stopnia technicznego zaawansowania naszego użytkownika. Ponieważ, gdy input naszego użytkownika nie mógłby zostać sparsowany Elasticsearch wyrzuci wyjątek. W takim wypadku wiąże się to z nauką naszych użytkowników jak działa wyszukiwarka w naszym systemie. Większość ludzi raczej oczekuje prostego rozwiązania.

Dla przykładu query `title:Microservices+status:active` znajdzie artykuły o mikroserwisach oraz wszystkie, które są aktywne. Aby znaleźć tylko aktywne artykuły w tym temacie należałoby zmienić operator: `default_operator=AND&title:Microservices+status:active`. To wyszukiwanie posiada masę dodatkowych opcji, jednak wystawienie tego użytkownikowi może mocno zredukować user experience naszej wyszukiwarki. 

 

Simple Query String Query

Jest to okrojona wersja poprzedniego wyszukiwania. Dodatkowo, nie rzuca wyjątkami przy błędach składniowych, a po prostu pomija części zapytania, którego nie był w stanie sparsować. Jeśli więc chcemy dać użytkownikowi więcej możliwości, tzn. podejmowania decyzji co i jak ma być wyszukiwane, może to być dobre rozwiązanie. Więcej o tym wyszukiwaniu tutaj.

 

Common Terms Query

Poprzednie zapytania domyślnie używają analizatora z filtrem tokenów, które pomija w wyszukiwania tzw. stop/common words. W niektórych przypadkach takie słowa mogą mieć jednak znaczenie. Przykładowo, słowo not jest przez domyślny filtr pomijane, co prowadzi do tego, że przy zapytaniu Acceptable solution zwrócimy dokumenty, które zawierają frazy `not acceptable solution` i `acceptable solution`. Właśnie w takich przypadkach przychodzi z pomocą Common Terms Query. Możemy dzięki niemu obsłużyć słowa, które występują często w naszym indeksie (tzw. stop/noise words) podczas działania samego zapytania, zamiast bazowania na oddzielnym pliku z tymi słowami, który musiał zostać zdefiniowany wcześniej (tak jak np. ma to miejsce w SQL Server Full Text Search). Dla przykładu, jeśli naszą domeną aplikacji jest fryzjerstwo to słówko hair może być traktowane jako stopword, ponieważ może występować w każdym dokumencie kilka razy. Dzięki użyciu parametru cutoff_frequency wynik wyszukiwania będzie wtedy skupiony na słowach, które występują rzadziej, co może poprawić jego dokładność. Zapytanie pod spodem zostanie podzielone na dwie grupy. Będą to słowa low frequency terms , czyli takie, które mają duży wpływ na dokładność naszego query, ale występują rzadziej. Druga grupa to high frequency terms, czyli słowa które występują często w danym języku/domenie, ale mają mniejszy wpływ na dokładność. Następnie Elastic wykona zapytanie wyszukując tylko wyrazów z grupy high frequency terms. Następnie na zwróconych dokumentach wykona drugie zapytanie, wyszukując słów z grupy low frequency words. Dzięki temu impakt na wydajność będzie dużo mniejszy, ponieważ pierwszym zapytaniem mocno ograniczymy ilość dokumentów do przeszukania słowami, które występują często. Tą funkcjonalność posiada także zapytanie Match, o którym pisałem wyżej. Dodatkowo Common Terms Query umożliwia ustawienie operatorów logicznych dla tych dwóch grup. Domyślnie  low_freq_operator i high_freq_operator są ustawione na operator or

.Query(q => q.CommonTerms(c => c.Field(p => p.Body)
                                .HighFrequencyOperator(Operator.And)
                                .LowFrequencyOperator(Operator.Or)
                                .Query(txt)))

 

O czym warto jeszcze wspomnieć

Opisałem tutaj najważniejsze funkcjonalności wyszukiwań pełnotekstowych w Elasticsearchu, jednak warto pokrótce wspomnieć jeszcze o kilku rzeczach.

Fuziness

Niektóre z wyszukiwań (z opisanych tutaj Match,Multi Match,`Query string` oraz Simple query string) wspierają obsługę literówek. Ta funkcjonalność bazuje na tzw. odległości Levenshteina. Sama odległość mówi o tym, ile operacji na danym wyrazie, jest potrzebnych, aby zamienić jeden wyraz w drugi. Dozwolone operacje to wstawienie, zamiana, usunięcie pojedynczej litery. Na przykład do zamiany słowa krzesło na słowo krzesła jest potrzebna jedna operacja zamiany litery (o na a). 

.Query(q => q.Match(fs => fs.Field(f => f.Subject)
                                         .Fuzziness(Fuzziness.Auto)
                                         .Operator(Operator.And))));

Powyższy operator AND jest tutaj ustawiony celowo. Inaczej dokumenty tylko z wyrazami you oraz need także zostałyby znalezione. Dla przykładu, dla zapytania z literówką you need Ma`croservices` znalazłoby frazy pomijając literówkę.

Zapytanie typu match z parametrem fuziness i operatorem AND Znalezione frazy Scoring Co nie zostanie znalezione
you need Macroservices Your app quite complex. You will need Microservices. Remember about DevOps 1.31 Hype development with Microservices ? Who need them ?
Right now on every conference you can hear: You need Microservices! You need Microservices! This is funny 1.21 How to center div vertically and horizontally
This is CRUD app, you don’t need Microservices. Stay with modular monolith 0.84

 

 

Język Polski

Niestety, Elasticsearch nie ma wbudowanego wspierania języka polskiego. Jednakże istnieją pluginy, które możemy doinstalować, aby nasz język zaczął być wspierany. Wszystkie z nich są zewnętrznymi narzędziami, jednak jeden z nich jest oficjalnie polecany przez zespół Elasticsearcha.

https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-stempel.html

https://github.com/allegro/elasticsearch-analysis-morfologik

 

Highlighting

Elasticsearch pozwala w prosty sposób podświetlić znalezione wyniki. Jest to bardzo przydatne, jeśli chcemy pokazać użytkownikowi, co znalazła nasza wyszukiwarka.

.Query(q => q.Match(x=>x.Field(f=>f.Body)
                                   .Query(txt)))                                
.Highlight(h => h.PreTags("<span style=\"background-color: #FFFF00\">")
                 .PostTags("</span>")
                 .Fields(fs => fs.Field(p => p.Body))));

Podsumowanie

Opisałem tutaj podstawowe opcje, jakie oferują wyszukiwania pełnotekstowe w Elasticsearchu. Jak zapewne zauważyłeś, jest to rozwiązanie o wiele bardziej elastyczne i dokładniejsze niż  Full Text Search w SQL Serverze. Jednak wymaga więcej pracy przy migracji, wdrożeniu oraz utrzymaniu. Jeśli jednak w Twoim systemie wyszukiwarka staje się wąskim gardłem to warto przyjrzeć się alternatywom dla wyszukiwań bazującym tylko na klauzuli LIKE.

Kategorie: Bazy danych