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.

Mapping dokumentów w ElasticSearch

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