Marcin Lewandowski
Marcin Lewandowski
Programista PHP ( Symfony ), blogger, trener oraz miłośnik kawy. Na co dzień pracuję z Symfony, RabbitMQ, ElasticSearch, Node.js, Redis, Docker, MySQL.

Jak stworzyć własny analizer w ElasticSearch

Jak stworzyć własny analizer w ElasticSearch

W przypadku, gdy żaden z wbudowanych analizer-ów nie spełnia naszych wymagań. ElasticSearch daje nam możliwość zbudowania własnych.

Jednak jeśli mamy już stworzony indeks to dodanie nowego analizer-a wymaga odrobiny gimnastyki. Mianowicie konieczne jest zamknięcie indeksu:

POST /nazwa_indeksu/_close

Po czym dodajemy nowy analizer lub modyfikujemy istniejący, gdy zakończymy prace to otwieramy indeks:

POST /nazwa_indeksu/_open

W poniższych przykładach pominę ten proces i skupię się na pokazaniu procesu tworzenia analizera. Więc nie dziwcie się, że każdy przykład będzie miał nową nazwę indeksu. Dzięki temu będziecie mogli skopiować przykłady bez obaw o błąd resource_already_exists_exception:

{
  "error": {
    "root_cause": [
      {
        "type": "resource_already_exists_exception",
        "reason": "index [my_index/jgjij-zPS86T_lINGGvAUg] already exists",
        "index_uuid": "jgjij-zPS86T_lINGGvAUg",
        "index": "my_index"
      }
    ],
    "type": "resource_already_exists_exception",
    "reason": "index [my_index/jgjij-zPS86T_lINGGvAUg] already exists",
    "index_uuid": "jgjij-zPS86T_lINGGvAUg",
    "index": "my_index"
  },
  "status": 400
}

Tworzymy pierwszy analizer

Definiując nowy analizer, będziemy podawać trzy parametry wynikające ze sposobu analizy danych przez ElasticSearch-a. Bowiem proces analizy sprowadza się do trzech kroków:

  1. character filters, nałożenie na tekst filtrów, które dokonają jego modyfikacji np. eliminując tagi html,
  2. tokenizacja, rozbija na tokeny tekst,
  3. token filters, nałożenie na stworzone tokeny filtrów np. zamiana na małe litery

Dlatego tworząc analizer podajemy następujące parametry:

  1. char_filter - lista filtrów jakie mają zostać użyte przed tokenizacją np. html_strip czyli eliminacja tagów html (ten parametr nie jest obowiązkowy i można go pominąć),
  2. tokenizer - tokenizer jakiego chcemy użyć do rozbicia tekstu na tokeny (parametr obowiązkowy),
  3. filter - lista filtrów jakie mają zostać użyte na tokenach (parametr opcjonalny, można pominąć)

Zdefiniujmy nasz pierwszy analizer, który będzie usuwał znaczniki html, wykorzysta standardowy tokenizer. I na sam koniec zmieni tokeny na pisane małymi literami. Więc nasz zapis będzie wyglądał następująco:

"custom_analyzer_1": {
     "type": "custom",
     "char_filter": ["html_strip"]
     "tokenizer": "standard",
     "filter": ["lowercase"]
}

Mamy tutaj dwa elementy o których nie wspominałem wcześniej, jednak łatwo wywnioskować czym są ;) custom_analyzer_1 to nazwa naszego analizera, zaś type określa typ i w naszym przypadku jest to typ custom. Jednak analizer musimy gdzieś przypisać i najlepszym miejscem będzie indeks. W indeksie znajduje się klucz settings gdzie mamy ustawienia indeksu, zaś w nim znajdziemy klucz analysis. I pod tym kluczem znajdziemy kolejny klucz o znajomej nazwie analyzer, gdzie dodajemy nasze analizery. Taka struktura może wyglądać następująco:

{
  "settings": {
    "analysis": {
      "analyzer": {
        "custom_analyzer_1": {
          "type": "custom",
          "char_filter": ["html_strip"]
          "tokenizer": "standard",
          "filter": ["lowercase"]
        }
      }
    }
  }
}

Teraz pozostaje jedynie utworzenie nowego indeksu z naszym analizerem:

curl -XPUT 'localhost:9200/my_index_1?pretty' -H 'Content-Type: application/json' -d'{
  "settings": {
    "analysis": {
      "analyzer": {
        "custom_analyzer_1": {
          "type": "custom",
          "char_filter": ["html_strip"],
          "tokenizer": "standard",
          "filter": ["lowercase"]
        }
      }
    }
  }
}'

Skoro mamy własny analizer czas go przetestować, może nie robi on zbyt wiele jednak zobaczmy jak zadziała:

curl -XPOST 'localhost:9200/my_index_1/_analyze?pretty' -H 'Content-Type: application/json' -d '{
  "analyzer": "custom_analyzer_1",
  "text": "Pierwszy <b>analizer</b>, który usuwa znaczniki HTML !!!"
}'

Rezultatem działania będzie wyświetlenie poniższej listy tokenów:

[pierwszy, analizer, który, usuwa, znaczniki, html]

W porównaniu do wzorcowego tekstu widzimy, że zostały usunięte znaczniki html-a oraz przecinek i wykrzykniki. Analizer zadziałał zgodnie z naszymi oczekiwaniami.

Zaawansowane analizery

Nasz prosty analizer z poprzedniego przykładu jest idealnym wyjście do czegoś bardziej zaawansowanego. Zróbmy więc analizer, który będzie wyciągał adresy e-mail. Zapewne zastanawiasz się cóż w tym takiego zaawansowanego, otóż jeśli wykorzystamy standardowy mechanizm:

curl -XPOST 'localhost:9200/my_index_2/_analyze?pretty' -H 'Content-Type: application/json' -d '{
  "analyzer": "standard",
  "text": "Mój adres email: marcin.lewandowski@czterytygodnie.pl"
}'

Rezultatem działania będą tokeny:

[mój, adres, email, marcin.lewandowski, czterytygodnie.pl]

Jak widzisz, nie znajdziemy tutaj pełnego adresu e-mail. Został on rozbity na dwa tokeny, które mogą być przydatne jednak sam adres email także byłby mile widziany. W tym przypadku problemem jest tokenizer, który dzieli adres email na dwie części. Rozwiązanie było by użycie tokenizera uax_url_email.

curl -XPOST 'localhost:9200/my_index_2/_analyze?pretty' -H 'Content-Type: application/json' -d '{
  "tokenizer": "uax_url_email",
  "text": "Mój adres email: marcin.lewandowski@czterytygodnie.pl"
}'

Rezultatem działania będą tokeny:

[mój, adres, email, marcin.lewandowski@czterytygodnie.pl]

Więc jeśli chcielibyśmy mieć analizer uwzględniający adresy email to powinien wyglądać on następująco:

curl -XPUT 'localhost:9200/my_index_3?pretty' -H 'Content-Type: application/json' -d'{
  "settings": {
    "analysis": {
      "analyzer": {
        "email_analyzer": {
          "type": "custom",
          "char_filter": ["html_strip"],
          "tokenizer": "uax_url_email",
          "filter": ["lowercase"]
        }
      }
    }
  }
}'

Jednak dodatkowo chcielibyśmy, aby nasz analizer rozbijał nam adres email na dwie części jak to było robione wcześniej. I tu pojawia się problem bowiem nie ma tokenizera czy filtra, który potrafił by taką operację przeprowadzić. Jednak możemy zdefiniować własne filtry i to będzie rozwiązaniem naszego problemu.

Tworzenie filtrów

Właściwie to nie do końca tworzymy filtry, raczej je konfigurujemy tak samo jak to ma miejsce w przypadku analizerów. Z tą jednak różnicą, że filtry nie mają stałej struktury. Jest ona uzależniona od opcji konfiguracji jakie są udostępnione przez dany filtr. I tak filtr uppercase nie posiada żadnych opcji tym samym nie ma sensu tworzyć na jego bazie własnego filtra. Za to filtr pattern_capture już posiada kilka opcji, które pozwalają na odpowiednie skonfigurowanie tego filtra do naszych wymagań.

Struktura dla nowego filtra pattern_capture będzie wyglądała następująco:

"my_filter": {
    "type": "pattern_capture",
    "preserve_original": true,
    "patterns": [
        "([^@]+)@",
        "@(.+)"
    ]
}

Mamy tutaj nazwę naszego filtra my_filter. Następnie pod kluczem type znajdziemy nazwę filtra, na bazie którego tworzymy nasz filtr. Pozostałe opcje to indywidualne ustawienia dla filtra pattern_capture.

Gdy mamy już odpowiednią strukturę to możemy ją dodać do listy filtrów. Lista ta znajduje się podobnie jak analizery w ustawieniach indeksu settings. Pod kluczem analysis znajdziemy klucz filter, gdzie zapisujemy nasze filtry.

Zapis filtra może wyglądać następująco:

curl -XPUT 'localhost:9200/my_index_4?pretty' -H 'Content-Type: application/json' -d'{
  "settings": {
    "analysis": {
      "filter": {
        "my_filter": {
          "type": "pattern_capture",
          "preserve_original": true,
          "patterns": [
            "([^@]+)@",
            "@(.+)"
          ]
        }
      }
    }
  }
}'

Teraz czas na połączenie go z analizerem i weryfikację czy wszystko działa według naszych założeń.

curl -XPUT 'localhost:9200/my_index_5?pretty' -H 'Content-Type: application/json' -d'{
  "settings": {
    "analysis": {
      "filter": {
        "email": {
          "type": "pattern_capture",
          "preserve_original": true,
          "patterns": [
            "([^@]+)@",
            "@(.+)"
          ]
        }
      },
      "analyzer": {
        "email_analyzer": {
          "type": "custom",
          "char_filter": ["html_strip"],
          "tokenizer": "uax_url_email",
          "filter": ["lowercase", "email"]
        }
      }
    }
  }
}'

Mając tak przygotowany analizer zobaczmy jak zostanie przetworzony tekst:

curl -XPOST 'localhost:9200/my_index_5/_analyze?pretty' -H 'Content-Type: application/json' -d '{
  "analyzer": "email_analyzer",
  "text": "Mój adres email: marcin.lewandowski@czterytygodnie.pl"
}'

W rezultacie dostajemy listę tagów:

[mój, adres, email, marcin.lewandowski@czterytygodnie.pl, marcin.lewandowski, czterytygodnie.pl]

I to jest dokładnie to czego oczekiwaliśmy :) Może nie jest to najbardziej zaawansowany przykład na świecie, jednak pokazuje ideę jaka przyświeca tworzeniu analizer-ów oraz filtrów.

Podsumowanie

Możliwość tworzenia własnych analizer-ów oraz filtrów daje nam ogromne możliwości. Kiedy dodatkowo połączymy je z mapper-ami co pozwoli na analizę określonych pól poprzez nasze analizery i filtry. Zyskamy pełną kontrolę nad tym jak powstają tokeny, dlatego w kolejnym wpisie skupię się na mapper-ach.