Elasticsearch

9 artykułów

Relacje w ElasticSearch ( parent – child )

W relacyjnych bazach danych tworzenie relacji jest naturalnym sposobem odwzorowywania rzeczywistości. Jednak w przypadku, gdy mamy odczynienia z wyszukiwarką opartą o nierelacyjną bazę danych sprawy nieco się komplikują.

10 lut 2020

Migracja serwera ElasticSearch na nowszą wersję z wykorzystaniem _reindex

ElasticSearch rozwija się bardzo dynamicznie w związku z czym możemy zaobserwować dość częste wydawanie nowej wersje silnika. I pojawia się pytanie, czy aktualizować ? Osobiście chętnie aktualizuję, czy to ElasticSearch-a, czy też frameworki na których pracuję. Wyznaję przy tym kilka zasad, jedna z nich to stabilność działania. Dlatego w tym wpisie pokażę, jak w prosty sposób przenieść nasze indeksy do nowszej wersji ElasticSearch-a wykorzystując _reindex.

9 maj 2019

Obsługa języków w ElasticSearch

Tworząc wyszukiwarkę o wysokim poziomie trafności, musimy wziąć pod uwagę obsługę języka. A jak wiemy nasz język do najłatwiejszych nie należy. Sam silnik wyszukiwania także nie wspiera naszego języka, ale pokażę Ci jak pomimo tych przeszkód poradzić sobie z obsługą języka polskiego.

9 sty 2019

Sposoby wyszukiwania w ElasticSearch

W pierwszym wpisie tej serii opisałem jak konstruować proste zapytania wyszukujące. Był to zaledwie przedsmak tego co można zrobić w ElasticSearch. Tym razem poszerzymy wiedzę o najczęściej wykorzystywane sposoby wyszukiwania oraz zobaczymy jakie problemy poszczególne sposoby rozwiązują.

4 wrz 2018

Wprowadzenie do agregacji danych w ElasticSearch

Oprócz zaawansowanego wyszukiwanie pełnotekstowego w ElasticSearch mamy także możliwość grupowania i zliczania dokumentów. Co ważne operacje zliczania mogą być wykonywane równolegle z operacjami przeszukiwania indeksu. Dzięki czemu możemy zmniejszyć ilość zapytań do wyszukiwarki.

3 kwi 2018

Mapping dokumentów w ElasticSearch

Jeśli mieliście kontakt z relacyjnymi bazami danych (MySQL, MSSQL, PostgreSQL) to przyzwyczaiły was one do definiowania schematów bazy danych. W takim schemacie bazę dzielimy na tabele, tabele na kolumny, którym z kolei przypisujemy określony typy danych. Odpowiednikiem tego podejścia jest mapping w ElasticSearch.

4 lut 2018

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.

23 sty 2018

Jak ElasticSearch analizuje dane przed dodaniem ich do indeksu

Podczas operacji wprowadzania danych czyli indeksacji, dane są analizowane przez mechanizm, który nazywa się analizerem. Elasticsearch dostarcza nam zestaw wbudowanych analizerów, które w większości przypadków będą wystarczające.

19 paź 2017

Wprowadzenie do Elasticsearch

Niezależnie od języka w jakim piszemy aplikacje prędzej czy później możemy się spotkać z koniecznością przeszukiwania dużych ilości danych. Z reguły przeszukiwanie to ma być “inteligentne” czyli pozwalać na drobne błędy w pisowni oraz uwzględniać odmiany słów. Jak już zapewne część z was się domyśliła chodzi mi o wyszukiwanie pełnotekstowe.

19 sie 2017

Mapping dokumentów w ElasticSearch

Jeśli mieliście kontakt z relacyjnymi bazami danych (MySQL, MSSQL, PostgreSQL) to przyzwyczaiły was one do definiowania schematów bazy danych. Gdzie bazę dzielimy na tabele, tabele na kolumny, którym z kolei przypisujemy określony typy danych. Odpowiednikiem tego podejścia jest mapping w ElasticSearch.

Czym jest mapping

Mapping w ElasticSearch jest opisem pól dla grupy dokumentów zawierający informację o typach pól oraz sposobach ich analizy podczas dodawania oraz wyszukiwania. Pozwala także na kontrolę struktury mappingu dla danej grupy, poprzez wyłączenie dynamicznej modyfikacji całości lub tylko niektórych elementów. Żeby lepiej to zobrazować załóżmy, że mamy sklep internetowy w którym mamy produkty. Uproszczony model produktu może wyglądać następująco:

I w modelu tym chcemy, aby poszczególne pola były traktowane jako określone typy:

name (text) - pole tekstowe, price (double) - pole przechowujące wartość zmiennoprzecinkową o dużej precyzji, category (text) - pole tekstowe, creation_date (date) - pole przechowujące datę

I mapping pozwala nam na jawne określenie jakie pola zawiera dana grupa dokumentów, oraz jakiego są one typu. Dodatkowo możliwe jest przypisanie do tych pól analizerów, wbudowanych lub stworzonych przez nas.

Ostatnią istotną rzeczą o jakiej warto wspomnieć jest możliwość kontroli takiej struktury. Jako że ElasticSearch nie narzuca struktury dokumentów, jednak kod naszej aplikacji może być nieco bardziej wymagający. Dlatego mamy możliwość ustalenia, które elementy mogą być tworzone dynamicznie (dynamic: true), a które mają pozostawać bez zmian (dynamic: strict).

Magia czyli automatyczne generowanie mappingu

Pomimo tego, że do tej pory nie mieliśmy pojęcia o mapping-u to był on tworzony przez ElasticSearch-a. Odbywa się to w momencie, gdy dodajemy dane do indeksu. Następuje wtedy weryfikacja, czy w mapping-u danego indeksu znajduje się opis wszystkich przesyłanych pól. Jeśli takiej informacji nie ma to nastąpi analiza danych dla nowych pól i dodanie ich do mappingu.

Zobaczmy jak to wygląda w praktyce. Dodajemy bardzo prostą strukturę produktu do nowego indeksu products.

curl -XPUT "http://localhost:9200/products/product/1" -H 'Content-Type: application/json' -d '{
  "name": "Xiaomi Redmi 4X",
  "price": 1200,
  "category": "smartfon",
  "creation_date": "2018-01-30"
}'

W rezultacie ElasticSearch powinien utworzyć mapping na podstawie dodanego produktu. Sprawdzamy to poleceniem:

curl -XGET "http://localhost:9200/products/_mapping?pretty"

I będzie on wyglądał mniej więcej jak poniższy zapis:

{
  "products": {
    "mappings": {
      "product": {
        "properties": {
          "category": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "creation_date": {
            "type": "date"
          },
          "name": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "price": {
            "type": "long"
          }
        }
      }
    }
  }
}

Zobaczmy jak ElasticSearch poradził sobie z określeniem typów poszczególnych pól:

category (text) - kategoria produktu, zostało poprawnie określone jako pole tekstowe, creation_date (date) - data utworzenia, została określona poprawnie jako data, name (text) - nazwa produktu, zostało poprawnie określone jako pole tekstowe, price (long) - cena została określona jako integer co nie jest zgodne z naszymi oczekiwaniami, a wynika to z faktu braku wartości po przecinku.

Zobaczmy jak zmieni się mapowanie po dodaniu produktu z ceną zawierającą wartości dziesiętne:

curl -XPUT "http://localhost:9200/products/product/1" -H 'Content-Type: application/json' -d '{
  "name": "Xiaomi Redmi 4X",
  "price": 1200.00,
  "category": "smartfon",
  "creation_date": "2018-01-30"
}'

Rezultat:

{
  "products": {
    "mappings": {
      "product": {
        "properties": {
          "category": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "creation_date": {
            "type": "date"
          },
          "name": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "price": {
            "type": "float"
          }
        }
      }
    }
  }
}

Po dokonaniu zmian pole price zostało określone jako typ float. Jest lepiej, jednak było by idealnie gdyby typ został ustawiony na double. I tu pojawia się zasadnicze pytanie, czy powinniśmy zdawać się przy mapping-u na mechanizmy ElasticSearch-a ?? Według mnie nie, powinniśmy sami taki mapping zaprojektować zwłaszcza, że definiowanie typów to tylko mała część tego co możemy ustawić.

Ręczne tworzenie mappingu

Automatyczne generowanie mappingu nie jest najlepszym rozwiązaniem dla naszej aplikacji. Na poziomie testów oraz nauki może być to przydane, jednak nie ma co się oszukiwać. Pisane przez nas aplikacje zakładają pewne stałe struktury danych co przekłada się na dane jakie trafią do indeksu. Dlatego powinniśmy wiedzieć jak definiować mapping w ElasticSearch.

Mapping definiujemy przy tworzeniu indeksu, tak jak to zostało zaprezentowane poniżej:

curl -XPUT "http://localhost:9200/products?pretty" -H 'Content-Type: application/json' -d '{
    "mappings": {
      "product": {
        "properties": {
          "category": {
            "type": "text"
          },
          "creation_date": {
            "type": "date"
          },
          "name": {
            "type": "text"
          },
          "price": {
            "type": "double"
          }
        }
      }
    }
}'

Struktura przekazywana do indeksu jest bardzo prosta i ogranicza się do klucza mappings. W nim przechowywana jest lista mapping-ów dla wszystkich typów dokumentów. My zaś przesłaliśmy w tym przypadku tylko jeden typ product, gdzie w kluczu properties zdefiniowaliśmy typy pól.

Aktualizacja mappingu

Pomimo tego, że ElasticSearch nie przepada za wprowadzaniem modyfikacji do raz zdefiniowanych struktur to umożliwia on modyfikację mappingu w pewnych sytuacjach.

Zanim przejdziemy do tych sytuacji określimy sobie wzorcowy mapping, który będziemy modyfikowali.

{
  "product": {
    "properties": {
      "category":      {"type": "keyword", ignore_above: 100},
      "creation_date": {"type": "date"},
      "name":          {"type": "text"},
      "price":         {"type": "double"}
    }
  }
}

Możliwe jest dodawanie nowych właściwości do typów lub pól, co pozwala nam na dodanie np. nowego pola. Więc do naszego wzorcowego mapping-u dodamy pole określające czy produkt jest opublikowany.

curl -XPUT "http://localhost:9200/products/_mapping/product?pretty" -H 'Content-Type: application/json' -d '{
  "properties": {
    "published": {
      "type": "boolean"
    }
  }
}'

W rezultacie nastąpiła modyfikacja struktury i będzie ona wyglądała następująco:

{
  "product": {
    "properties": {
      "category":      {"type": "keyword", ignore_above: 100},
      "creation_date": {"type": "date"},
      "name":          {"type": "text"},
      "price":         {"type": "double"},
      "published":     {"type": "boolean"}
    }
  }
}

Kolejna możliwość to modyfikacja parametru ignore_above odpowiedzialnego za przechowywanie informacji o maksymalnej długości ciągu znaków.

curl -XPUT "http://localhost:9200/products/_mapping/product?pretty" -H 'Content-Type: application/json' -d '{
  "properties": {
    "category": {
      "type": "keyword",
      "ignore_above": 200
    }
  }
}'

Co w rezultacie da nam strukturę, w której wydłużyliśmy długość ciągu znaków do 200 dla pola category.

{
  "product": {
    "properties": {
      "category":      {"type": "keyword", ignore_above: 200},
      "creation_date": {"type": "date"},
      "name":          {"type": "text"},
      "price":         {"type": "double"},
      "published":     {"type": "boolean"}
    }
  }
}

Ostatnią możliwą operacją jaką możemy wykonać na istniejącym mapping-u w celu jego aktualizacji, jest dodanie multi-fields do istniejących pól. W telegraficznym skrócie multi-fields to indeksowanie jednego pola na kilka sposobów. I aktualizacja może wyglądać następująco:

curl -XPUT "http://localhost:9200/products/_mapping/product?pretty" -H 'Content-Type: application/json' -d '{
  "properties": {
    "name": {
      "type": "text",
      "fields": {
        "first": {
          "type": "text"
        },
        "keyword": {
          "type": "keyword",
          "ignore_above": 256
        }
      }
    }
  }
}'

Co wpływa na strukturę następująco:

{
  "product": {
    "properties": {
      "category":      {"type": "keyword", ignore_above: 100},
      "creation_date": {"type": "date"},
      "name":          {
        "type": "text",
        "fields": {
	        "first": {
	          "type": "text"
	        },
	        "keyword": {
	          "type": "keyword",
	          "ignore_above": 256
	        }
	      }
      },
      "price":         {"type": "double"},
      "published":     {"type": "boolean"}
    }
  }
}

Szablony

W różnego typu sytuacjach spotkamy się z organizacją indeksów opartą o czas. Spójrzmy chociażby na zamówienia w sklepie internetowym, gdzie zamówienia z zeszłego roku są dla nas średnio interesujące. Przez co mogą znajdować się w osobnym indeksie nie obciążając naszego bieżącego indeksu.

Takie podejście wymusza na nas co jakiś czas tworzenie nowego indeksu, którego mapping będzie kopią poprzedniego indeksu, czyli typowe copy-paste. Czy nie łatwiej by było, gdyby mapping sam się przypisał do tworzonego indeksu na podstawie jego nazwy ??

Właśnie w ten sposób działają szablony, definiujemy wzorzec dla nazwy np. shop-orders-*. Teraz każdy tworzony indeks jest przyrównywany do tego wzorca i jeśli pasuje to przypisywany jest do niego zdefiniowany mapping z szablonu.

Zobaczmy jak taki szablon definiujemy:

curl -XPUT "http://localhost:9200/_template/shop-orders" -d '{
    "index_patterns": "shop-orders-*",
    "mappings": {
      "product": {
        "properties": {
          "category": {
            "type": "text"
          },
          "creation_date": {
            "type": "date"
          },
          "name": {
            "type": "text"
          },
          "price": {
            "type": "double"
          }
        }
      }
    }
}'

Teraz dodając nowy indeks pasujący do wzorca spowoduje przypisanie mappingu z dodanego szablonu. Dodajmy więc nowy indeks i zobaczmy, czy mechanizm działa prawidłowo:

curl -XPUT "http://localhost:9200/shop-orders-201802?pretty"

Indeks dodany, zobaczmy czy został przypisany mapping.

curl -XGET "http://localhost:9200/shop-orders-201802/_mapping?pretty"

Mapping przypisał się prawidłowo do nowo utworzonego indeksu. Pamiętajcie że W szablonie możemy zdefiniować nie tylko typy pól, ale także ich formaty oraz analizery. Dodatkowo możemy zdefiniować ustawienia indeksu oraz własne analizery. Więc możliwości jest całkiem sporo i warto pomyśleć czy to rozwiązanie nie sprawdzi się w naszym przypadku.

Podsumowanie

ElasticSearch sam zadba o tworzenie i odpowiednie modyfikowanie mapping-u. Jednak powinniśmy zdawać sobie sprawę z niedoskonałości tego podejścia, co powinno nas skłonić do ręcznego definiowania mapping-u. Jednak jeśli myślicie że jest to odpowiednik schematu w relacyjnych bazach danych to przestańcie. Bo co byście powiedzieli o schemacie którego praktycznie nie możecie edytować ??

I na koniec kilka dobrych rad ;)

  • korzystajcie z szablonów, zarządzanie mapping-ami będzie dużo łatwiejsze,
  • definiujcie mapping wraz z dodawaniem indeksu,
  • zaprzyjaźnijcie się z flagą dynamic: strict, a jeśli jej nie znacie to migiem do dokumentacji