Dlaczego mapping i indexing są kluczowe
Elasticsearch to nie relacyjna baza danych – nie możesz po prostu „wrzucić” danych i liczyć na dobrą wydajność. Mapping to definicja tego, jak Elasticsearch interpretuje i przechowuje twoje dane, podczas gdy indexing określa strategię dodawania dokumentów do indeksu.
Właściwie skonfigurowane mapowanie może oznaczać różnicę między wyszukiwaniem trwającym 50ms a tym, które zajmuje 2 sekundy. W środowisku produkcyjnym, gdzie obsługujesz tysiące zapytań na sekundę, ta różnica decyduje o sukcesie aplikacji.
Co się nauczysz
- Jak tworzyć efektywne mapowania dla różnych typów danych
- Strategie optymalizacji procesu indexowania dla dużych wolumenów
- Konfiguracja analizatorów i tokenizatorów dla lepszego wyszukiwania
- Monitoring wydajności i identyfikacja wąskich gardeł
- Praktyczne wzorce mapping dla typowych przypadków użycia
- Bulk indexing i jego optymalizacja
- Rozwiązywanie problemów z wydajnością w środowisku produkcyjnym
Wymagania wstępne
- Podstawowa znajomość Elasticsearch (instalacja, podstawowe operacje)
- Doświadczenie z JSON i REST API
- Rozumienie koncepcji full-text search
- Znajomość pracy z curl lub Kibana Dev Tools
- Podstawy monitoringu aplikacji (opcjonalne ale zalecane)
Elasticsearch Mapping – Fundament wydajności
Mapping w Elasticsearch to definicja schematu dokumentów w indeksie. W przeciwieństwie do tradycyjnych baz danych, Elasticsearch może automatycznie wykrywać typy pól (dynamic mapping), ale w środowisku produkcyjnym zawsze powinieneś definiować mapowanie explicite.
Typy pól i ich wpływ na wydajność
{ "mappings": { "properties": { "title": { "type": "text", "analyzer": "standard", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "price": { "type": "scaled_float", "scaling_factor": 100 }, "created_date": { "type": "date", "format": "yyyy-MM-dd||epoch_millis" }, "status": { "type": "keyword" }, "description": { "type": "text", "analyzer": "custom_analyzer", "search_analyzer": "search_analyzer" } } } }
Multi-field mapping dla elastyczności
Multi-field pozwala indeksować to samo pole na różne sposoby:
{ "mappings": { "properties": { "product_name": { "type": "text", "analyzer": "standard", "fields": { "exact": { "type": "keyword" }, "suggest": { "type": "completion" }, "sort": { "type": "keyword", "normalizer": "lowercase_normalizer" } } } } } }
Custom Analyzers – Konfiguracja dla języka polskiego
Domyślny analizator Elasticsearch działa dobrze dla języka angielskiego, ale dla polskiego tekstu potrzebujemy customowej konfiguracji:
{ "settings": { "analysis": { "filter": { "polish_stop": { "type": "stop", "stopwords": ["a", "aby", "ale", "albo", "am", "an", "ani", "are", "as", "at", "and", "być", "czy", "dla", "do", "gdy", "i", "ich", "ie", "if", "in", "is", "it", "jak", "jako", "już", "lub", "ma", "może", "na", "nad", "nie", "o", "od", "po", "pod", "oraz", "się", "to", "w", "we", "z", "za"] }, "polish_stemmer": { "type": "stemmer", "language": "light_polish" } }, "normalizer": { "lowercase_normalizer": { "type": "custom", "filter": ["lowercase", "asciifolding"] } }, "analyzer": { "polish_analyzer": { "type": "custom", "tokenizer": "standard", "filter": [ "lowercase", "asciifolding", "polish_stop", "polish_stemmer" ] } } } } }
Strategie indexowania – Od pojedynczych dokumentów do bulk operations
Pojedyncze dokumenty vs Bulk API
Indexowanie pojedynczych dokumentów:
# Pojedynczy dokument - WOLNE! curl -X POST "localhost:9200/products/_doc/1" -H "Content-Type: application/json" -d '{ "name": "iPhone X", "price": 1000, "category": "smartphones" }'
Bulk indexing – wydajne dla większych wolumenów:
# Bulk API - SZYBKIE! curl -X POST "localhost:9200/_bulk" -H "Content-Type: application/json" --data-binary " {"index":{"_index":"products","_id":"1"}} {"name":"iPhone X","price":1000,"category":"smartphones"} {"index":{"_index":"products","_id":"2"}} {"name":"Samsung Galaxy","price":800,"category":"smartphones"} {"index":{"_index":"products","_id":"3"}} {"name":"Google Pixel","price":700,"category":"smartphones"} "
Optymalizacja bulk indexing w Javie
// Elasticsaerch Java Client (v6.x) BulkRequestBuilder bulkRequest = client.prepareBulk(); // Batch processing Listproducts = productService.getProductsBatch(1000); int batchSize = 100; for (int i = 0; i < products.size(); i += batchSize) { List batch = products.subList(i, Math.min(i + batchSize, products.size())); for (Product product : batch) { bulkRequest.add(client.prepareIndex("products", "_doc", product.getId()) .setSource(objectMapper.writeValueAsString(product), XContentType.JSON)); } // Execute batch when reaches optimal size if (bulkRequest.numberOfActions() >= batchSize) { BulkResponse bulkResponse = bulkRequest.execute().actionGet(); if (bulkResponse.hasFailures()) { logger.error("Bulk indexing failures: " + bulkResponse.buildFailureMessage()); } // Clear for next batch bulkRequest = client.prepareBulk(); } } // Index remaining documents if (bulkRequest.numberOfActions() > 0) { bulkRequest.execute().actionGet(); }
Index Settings – Konfiguracja dla wydajności
Optymalizacja dla indexowania vs wyszukiwania
{ "settings": { "number_of_shards": 1, "number_of_replicas": 0, "refresh_interval": "30s", "index": { "max_result_window": 50000, "mapping": { "total_fields": { "limit": 2000 } }, "translog": { "flush_threshold_size": "1gb", "sync_interval": "30s" } } } }
Setting | Indexing optymalne | Search optymalne | Opis |
---|---|---|---|
refresh_interval | 30s lub -1 | 1s | Częstotliwość odświeżania segmentów |
number_of_replicas | 0 | 1+ | Liczba kopii dla HA i performance |
translog.sync_interval | 30s | 5s | Częstotliwość synchronizacji translog |
Monitoring wydajności indexowania
Metryki które musisz śledzić
# Statistyki indexowania curl -X GET "localhost:9200/_stats/indexing?pretty" # Wydajność poszczególnych indeksów curl -X GET "localhost:9200/products/_stats?pretty" # Informacje o segmentach curl -X GET "localhost:9200/products/_segments?pretty"
Kluczowe metryki do monitorowania:
- Indexing rate: dokumenty/sekundę – cel: >1000 docs/sec dla bulk
- Index latency: czas indexowania – cel: <50ms średnio
- Merge time: czas łączenia segmentów – wysoki = problem
- Refresh time: czas odświeżania – cel: <100ms
- Flush time: czas flush translog – cel: <500ms
Częste problemy wydajnościowe i ich rozwiązania
Problem: Powolne indexowanie
**Rozwiązanie:**
// Przed masowym indexowaniem PUT /products/_settings { "refresh_interval": "-1", "number_of_replicas": 0, "translog": { "sync_interval": "120s", "durability": "async" } } // Po zakończeniu indexowania PUT /products/_settings { "refresh_interval": "1s", "number_of_replicas": 1, "translog": { "sync_interval": "5s", "durability": "request" } } // Wymuś refresh POST /products/_refresh
Problem: Out of Memory podczas bulk indexing
**Rozwiązanie – kontrola rozmiaru batch:**
public class OptimizedBulkIndexer { private static final int MAX_BATCH_SIZE = 100; private static final int MAX_BATCH_SIZE_BYTES = 5 * 1024 * 1024; // 5MB public void indexDocuments(Listdocuments) { BulkRequestBuilder bulkRequest = client.prepareBulk(); int currentBatchSize = 0; for (Document doc : documents) { String jsonDoc = objectMapper.writeValueAsString(doc); currentBatchSize += jsonDoc.getBytes().length; bulkRequest.add(client.prepareIndex("products", "_doc", doc.getId()) .setSource(jsonDoc, XContentType.JSON)); // Flush when batch is full OR size limit reached if (bulkRequest.numberOfActions() >= MAX_BATCH_SIZE || currentBatchSize >= MAX_BATCH_SIZE_BYTES) { executeBulkRequest(bulkRequest); bulkRequest = client.prepareBulk(); currentBatchSize = 0; } } // Index remaining documents if (bulkRequest.numberOfActions() > 0) { executeBulkRequest(bulkRequest); } } private void executeBulkRequest(BulkRequestBuilder bulkRequest) { BulkResponse response = bulkRequest.execute().actionGet(); if (response.hasFailures()) { // Log failures but continue processing logger.warn("Bulk indexing had failures: " + response.buildFailureMessage()); } logger.info("Indexed {} documents in {}ms", response.getItems().length, response.getTook().getMillis()); } }
Template mapping dla wielu indeksów
Gdy masz wiele podobnych indeksów (np. logi dzienne), użyj index templates:
{ "index_patterns": ["logs-*"], "settings": { "number_of_shards": 1, "number_of_replicas": 1, "refresh_interval": "5s" }, "mappings": { "properties": { "timestamp": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||epoch_millis" }, "level": { "type": "keyword" }, "message": { "type": "text", "analyzer": "standard" }, "application": { "type": "keyword" } } } }
Testing mappingu – Jak weryfikować konfigurację
# Test analizatora curl -X POST "localhost:9200/products/_analyze?pretty" -H "Content-Type: application/json" -d '{ "analyzer": "polish_analyzer", "text": "Programowanie w Javie jest fascynujące" }' # Sprawdzenie mappingu curl -X GET "localhost:9200/products/_mapping?pretty" # Test wydajności wyszukiwania curl -X GET "localhost:9200/products/_search?pretty" -H "Content-Type: application/json" -d '{ "query": { "match": { "description": "programowanie" } }, "explain": true }'
W środowisku produkcyjnym – tak, zawsze. Dynamic mapping może utworzyć nieoptymalne typy pól. Na przykład, ID produktu może zostać zmapowane jako text zamiast keyword, co znacznie spowalnia filtering i sorting.
Możesz dodawać nowe pola, ale nie możesz zmieniać typów istniejących pól. W takim przypadku musisz utworzyć nowy indeks z poprawnym mappingiem i przeprowadzić reindexing. Dlatego warto przemyśleć mapping na początku.
Reguła: jeden shard na ~30-50GB danych lub na jeden node. Więcej shardów = więcej overhead. Dla indeksu poniżej 5GB używaj jednego sharda. Elasticsearch 7.0+ domyślnie tworzy jeden shard, co jest dobrym wyborem dla większości przypadków.
Nie, multi-field zwiększa rozmiar indeksu i czas indexowania. Używaj tylko gdy potrzebujesz różnych sposobów wyszukiwania tego samego pola. Na przykład: pełnotekstowe wyszukiwanie (text), exact match (keyword), i sortowanie (keyword z normalizerem).
Sprawdź: 1) rozmiar batcha (5-15MB optymalnie), 2) refresh_interval (ustaw na 30s lub -1 podczas indexowania), 3) liczbę replik (ustaw na 0 podczas masowego indexowania), 4) hot threads API: GET /_nodes/hot_threads
Używaj nested gdy potrzebujesz wyszukiwać po związanych polach w tablicy obiektów. Object „spłaszcza” strukturę, więc nie możesz zapytać o „kolor: czerwony AND rozmiar: L” w jednym produkcie z wieloma wariantami.
Force merge tylko na indeksach read-only (np. stare logi). Dla aktywnie indexowanych indeksów Elasticsearch automatycznie zarządza segmentami. Force merge na aktywnym indeksie może spowodować problemy wydajnościowe i zwiększone zużycie dysku.
Przydatne zasoby
- Oficjalna dokumentacja Elasticsearch Mapping
- Bulk API Reference
- Text Analysis and Analyzers Guide
- Index Settings Documentation
- Index Templates Guide
🚀 Zadanie dla Ciebie
Stwórz mapping dla e-commerce z następującymi wymaganiami:
- Produkty z nazwą (wyszukiwanie full-text + exact match), ceną (scaled_float), kategorią (keyword), opisem (text z polish analyzer)
- Skonfiguruj bulk indexing dla 10,000 produktów
- Zmierz czas indexowania przed i po optymalizacji ustawień
- Przetestuj różne rozmiary batcha i znajdź optymalny dla twojego środowiska
- Porównaj wydajność wyszukiwania z domyślnym a custom analyzerem
Podziel się wynikami w komentarzach – ile udało ci się przyspieszyć indexowanie?