Elasticsearch mapping i indexing – Optymalizacja dla wydajności

TL;DR: Elasticsearch mapping definiuje strukturę dokumentów i sposób ich indексowania. Właściwa konfiguracja mapping i strategii indexing może poprawić wydajność wyszukiwania o 200-300%. W tym artykule dowiesz się jak tworzyć efektywne mapowania, optymalizować proces indeksowania i unikać typowych pułapek wydajnościowych.

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.

Kontekst biznesowy: E-commerce z 100k produktów może zaoszczędzić 60% kosztów infrastruktury poprzez optymalizację mappingu, a sklep internetowy zyskuje 15% większą konwersję gdy wyszukiwarka odpowiada poniżej 100ms.

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"
      }
    }
  }
}
Pro tip: Używaj scaled_float zamiast float dla cen – zajmuje mniej miejsca i jest bardziej precyzyjny dla obliczeń finansowych.

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"
          }
        }
      }
    }
  }
}
Uwaga: Multi-field zwiększa rozmiar indeksu i czas indexowania. Używaj tylko gdy rzeczywiście potrzebujesz różnych sposobów wyszukiwania tego samego pola.

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"
          ]
        }
      }
    }
  }
}
Praktyczny przykład: Dzięki temu analizatorowi zapytanie „programowania” znajdzie też dokumenty zawierające „programowanie”, „programować”, „programowac” i „programuje”.

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"}
"
Pułapka: Bulk request ma limit domyślnie 100MB. Większe batche mogą spowodować OutOfMemoryError. Optymalna wielkość to 5-15MB per batch.

Optymalizacja bulk indexing w Javie

// Elasticsaerch Java Client (v6.x)
BulkRequestBuilder bulkRequest = client.prepareBulk();

// Batch processing
List products = 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"
      }
    }
  }
}
SettingIndexing optymalneSearch optymalneOpis
refresh_interval30s lub -11sCzęstotliwość odświeżania segmentów
number_of_replicas01+Liczba kopii dla HA i performance
translog.sync_interval30s5sCzęstotliwość synchronizacji translog
Pro tip: Podczas masowego indexowania ustaw refresh_interval na -1 i number_of_replicas na 0. Po zakończeniu przywróć normalne wartości.

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

Typowy błąd: Indexowanie po jednym dokumencie z domyślnymi ustawieniami może dać tylko 10-50 docs/sec zamiast 1000+.

**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(List documents) {
        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
}'
Czy powinienem zawsze używać explicit mapping?

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.

Jak często mogę zmieniać mapping istniejącego indeksu?

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.

Ile shardów powinienem używać dla mojego indeksu?

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.

Czy multi-field zawsze poprawia wydajność wyszukiwania?

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).

Jak debugować wolne indexowanie?

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

Kiedy używać nested vs object field type?

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.

Jak często powinienem uruchamiać force merge?

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

🚀 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?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry