Elasticsearch #

Elasticsearch adalah search engine dan analytics engine terdistribusi yang dibangun di atas Apache Lucene. Ia sangat unggul untuk full-text search, log analytics (ELK Stack), dan pencarian data yang kompleks — jauh melampaui kemampuan LIKE '%keyword%' di SQL. Go mendukung Elasticsearch melalui client resmi github.com/elastic/go-elasticsearch.

Instalasi #

# Client resmi Elasticsearch — pilih versi yang sesuai dengan server ES kamu
go get github.com/elastic/go-elasticsearch/v8  # untuk ES 8.x
go get github.com/elastic/go-elasticsearch/v7  # untuk ES 7.x

Koneksi ke Elasticsearch #

import (
    "github.com/elastic/go-elasticsearch/v8"
    "github.com/elastic/go-elasticsearch/v8/esapi"
)

func connectES() (*elasticsearch.Client, error) {
    cfg := elasticsearch.Config{
        Addresses: []string{
            "http://localhost:9200",
            // tambahkan node lain untuk cluster
        },
        // Untuk ES dengan autentikasi
        Username: "elastic",
        Password: "password",

        // Atau dengan API key
        // APIKey: "base64encodedkey",

        // Retry configuration
        RetryOnStatus: []int{502, 503, 504},
        MaxRetries:    3,
    }

    client, err := elasticsearch.NewClient(cfg)
    if err != nil {
        return nil, fmt.Errorf("buat client: %w", err)
    }

    // Ping untuk verifikasi
    res, err := client.Info()
    if err != nil {
        return nil, fmt.Errorf("ping: %w", err)
    }
    defer res.Body.Close()

    if res.IsError() {
        return nil, fmt.Errorf("info error: %s", res.Status())
    }

    fmt.Println("✓ Terhubung ke Elasticsearch")
    return client, nil
}

Index Mapping #

Mapping mendefinisikan tipe field dalam index — penting untuk search dan aggregation:

const productMapping = `{
    "settings": {
        "number_of_shards": 1,
        "number_of_replicas": 0,
        "analysis": {
            "analyzer": {
                "indonesian_analyzer": {
                    "type": "standard",
                    "stopwords": "_indonesian_"
                }
            }
        }
    },
    "mappings": {
        "properties": {
            "name": {
                "type": "text",
                "analyzer": "indonesian_analyzer",
                "fields": {
                    "keyword": { "type": "keyword" }
                }
            },
            "description": {
                "type": "text",
                "analyzer": "indonesian_analyzer"
            },
            "category": { "type": "keyword" },
            "brand":    { "type": "keyword" },
            "tags":     { "type": "keyword" },
            "price":    { "type": "double" },
            "stock":    { "type": "integer" },
            "rating":   { "type": "float" },
            "is_active": { "type": "boolean" },
            "created_at": { "type": "date" },
            "specs": { "type": "object" }
        }
    }
}`

func createIndex(es *elasticsearch.Client, indexName string) error {
    res, err := es.Indices.Create(
        indexName,
        es.Indices.Create.WithBody(strings.NewReader(productMapping)),
    )
    if err != nil {
        return err
    }
    defer res.Body.Close()

    if res.IsError() {
        var e map[string]interface{}
        json.NewDecoder(res.Body).Decode(&e)
        // Index sudah ada — tidak masalah
        if e["error"].(map[string]interface{})["type"] == "resource_already_exists_exception" {
            return nil
        }
        return fmt.Errorf("create index: %s", res.Status())
    }
    return nil
}

Indexing — Menyimpan Dokumen #

type Product struct {
    ID          string            `json:"id"`
    Name        string            `json:"name"`
    Description string            `json:"description"`
    Category    string            `json:"category"`
    Brand       string            `json:"brand"`
    Tags        []string          `json:"tags"`
    Price       float64           `json:"price"`
    Stock       int               `json:"stock"`
    Rating      float64           `json:"rating"`
    IsActive    bool              `json:"is_active"`
    Specs       map[string]string `json:"specs,omitempty"`
    CreatedAt   time.Time         `json:"created_at"`
}

func indexProduct(es *elasticsearch.Client, indexName string, p Product) error {
    data, err := json.Marshal(p)
    if err != nil {
        return err
    }

    res, err := es.Index(
        indexName,
        bytes.NewReader(data),
        es.Index.WithDocumentID(p.ID),
        es.Index.WithRefresh("true"), // langsung visible — hanya untuk dev/test
    )
    if err != nil {
        return fmt.Errorf("index: %w", err)
    }
    defer res.Body.Close()

    if res.IsError() {
        return fmt.Errorf("index error: %s", res.Status())
    }
    return nil
}

Bulk Indexing — Insert Banyak Dokumen Efisien #

Untuk indexing data dalam jumlah besar, gunakan Bulk API:

func bulkIndex(es *elasticsearch.Client, indexName string, products []Product) error {
    var buf bytes.Buffer

    for _, p := range products {
        // Setiap dokumen butuh dua baris: action dan data
        meta := fmt.Sprintf(`{"index":{"_index":%q,"_id":%q}}%s`,
            indexName, p.ID, "\n")
        buf.WriteString(meta)

        data, _ := json.Marshal(p)
        buf.Write(data)
        buf.WriteByte('\n')
    }

    res, err := es.Bulk(bytes.NewReader(buf.Bytes()),
        es.Bulk.WithIndex(indexName),
        es.Bulk.WithRefresh("true"),
    )
    if err != nil {
        return fmt.Errorf("bulk: %w", err)
    }
    defer res.Body.Close()

    if res.IsError() {
        return fmt.Errorf("bulk error: %s", res.Status())
    }

    var result map[string]interface{}
    json.NewDecoder(res.Body).Decode(&result)

    if result["errors"].(bool) {
        return fmt.Errorf("ada error dalam bulk indexing")
    }

    items := result["items"].([]interface{})
    fmt.Printf("Bulk index: %d dokumen diproses\n", len(items))
    return nil
}

Search — Mencari Dokumen #

Full-Text Search Dasar #

func search(es *elasticsearch.Client, indexName, keyword string) ([]Product, error) {
    query := map[string]interface{}{
        "query": map[string]interface{}{
            "multi_match": map[string]interface{}{
                "query":  keyword,
                "fields": []string{"name^3", "description", "tags^2"},
                // ^3 = boosts nama 3x lebih penting
            },
        },
        "highlight": map[string]interface{}{
            "fields": map[string]interface{}{
                "name":        map[string]interface{}{},
                "description": map[string]interface{}{},
            },
        },
    }

    return executeSearch(es, indexName, query)
}

Bool Query — Filter Kompleks #

func advancedSearch(es *elasticsearch.Client, indexName string, params SearchParams) (SearchResult, error) {
    // Bool query: must (AND), should (OR), must_not (NOT), filter (AND, tidak scoring)
    boolQuery := map[string]interface{}{
        "must": []interface{}{},
        "filter": []interface{}{
            map[string]interface{}{"term": map[string]interface{}{"is_active": true}},
        },
        "must_not": []interface{}{},
        "should":   []interface{}{},
    }

    // Full-text search pada nama dan deskripsi
    if params.Keyword != "" {
        boolQuery["must"] = append(boolQuery["must"].([]interface{}),
            map[string]interface{}{
                "multi_match": map[string]interface{}{
                    "query":  params.Keyword,
                    "fields": []string{"name^3", "description", "tags^2"},
                    "type":   "best_fields",
                    "fuzziness": "AUTO", // toleransi typo
                },
            },
        )
    }

    // Filter kategori (exact match)
    if params.Category != "" {
        boolQuery["filter"] = append(boolQuery["filter"].([]interface{}),
            map[string]interface{}{"term": map[string]interface{}{"category": params.Category}},
        )
    }

    // Filter range harga
    if params.MinPrice > 0 || params.MaxPrice > 0 {
        priceRange := map[string]interface{}{}
        if params.MinPrice > 0 {
            priceRange["gte"] = params.MinPrice
        }
        if params.MaxPrice > 0 {
            priceRange["lte"] = params.MaxPrice
        }
        boolQuery["filter"] = append(boolQuery["filter"].([]interface{}),
            map[string]interface{}{"range": map[string]interface{}{"price": priceRange}},
        )
    }

    // Filter in-stock
    if params.InStockOnly {
        boolQuery["filter"] = append(boolQuery["filter"].([]interface{}),
            map[string]interface{}{"range": map[string]interface{}{"stock": map[string]interface{}{"gt": 0}}},
        )
    }

    // Sort
    sortField := "created_at"
    sortOrder := "desc"
    if params.SortBy == "price_asc" {
        sortField, sortOrder = "price", "asc"
    } else if params.SortBy == "price_desc" {
        sortField, sortOrder = "price", "desc"
    } else if params.SortBy == "rating" {
        sortField, sortOrder = "rating", "desc"
    }

    query := map[string]interface{}{
        "query": map[string]interface{}{"bool": boolQuery},
        "sort":  []interface{}{map[string]interface{}{sortField: sortOrder}},
        "from":  (params.Page - 1) * params.PerPage,
        "size":  params.PerPage,
        "highlight": map[string]interface{}{
            "fields": map[string]interface{}{
                "name":        map[string]interface{}{},
                "description": map[string]interface{}{"fragment_size": 150},
            },
            "pre_tags":  []string{"<mark>"},
            "post_tags": []string{"</mark>"},
        },
        "aggs": map[string]interface{}{
            "by_category": map[string]interface{}{
                "terms": map[string]interface{}{"field": "category", "size": 20},
            },
            "price_range": map[string]interface{}{
                "range": map[string]interface{}{
                    "field": "price",
                    "ranges": []interface{}{
                        map[string]interface{}{"key": "< 500rb", "to": 500_000},
                        map[string]interface{}{"key": "500rb - 2jt", "from": 500_000, "to": 2_000_000},
                        map[string]interface{}{"key": "2jt - 10jt", "from": 2_000_000, "to": 10_000_000},
                        map[string]interface{}{"key": "> 10jt", "from": 10_000_000},
                    },
                },
            },
            "avg_price": map[string]interface{}{
                "avg": map[string]interface{}{"field": "price"},
            },
        },
    }

    return executeAdvancedSearch(es, indexName, query)
}

Aggregation — Analitik #

func productAnalytics(es *elasticsearch.Client, indexName string) error {
    query := map[string]interface{}{
        "size": 0, // tidak perlu dokumen, hanya agregasi
        "aggs": map[string]interface{}{
            "categories": map[string]interface{}{
                "terms": map[string]interface{}{
                    "field": "category",
                    "size":  10,
                },
                "aggs": map[string]interface{}{
                    "avg_price": map[string]interface{}{
                        "avg": map[string]interface{}{"field": "price"},
                    },
                    "total_stock": map[string]interface{}{
                        "sum": map[string]interface{}{"field": "stock"},
                    },
                    "avg_rating": map[string]interface{}{
                        "avg": map[string]interface{}{"field": "rating"},
                    },
                },
            },
            "price_stats": map[string]interface{}{
                "extended_stats": map[string]interface{}{"field": "price"},
            },
            "products_per_day": map[string]interface{}{
                "date_histogram": map[string]interface{}{
                    "field":             "created_at",
                    "calendar_interval": "day",
                    "format":            "yyyy-MM-dd",
                },
            },
        },
    }

    data, _ := json.Marshal(query)
    res, err := es.Search(
        es.Search.WithIndex(indexName),
        es.Search.WithBody(bytes.NewReader(data)),
    )
    if err != nil {
        return err
    }
    defer res.Body.Close()

    var result map[string]interface{}
    json.NewDecoder(res.Body).Decode(&result)

    aggs := result["aggregations"].(map[string]interface{})
    categories := aggs["categories"].(map[string]interface{})["buckets"].([]interface{})

    fmt.Println("=== Analitik per Kategori ===")
    for _, bucket := range categories {
        b := bucket.(map[string]interface{})
        fmt.Printf("  %-15s: %v produk, avg Rp%.0f, stok %v, rating %.1f\n",
            b["key"],
            b["doc_count"],
            b["avg_price"].(map[string]interface{})["value"],
            b["total_stock"].(map[string]interface{})["value"],
            b["avg_rating"].(map[string]interface{})["value"],
        )
    }
    return nil
}

Helper Functions #

type SearchParams struct {
    Keyword     string
    Category    string
    MinPrice    float64
    MaxPrice    float64
    InStockOnly bool
    SortBy      string
    Page        int
    PerPage     int
}

type SearchResult struct {
    Total    int64
    Products []ProductHit
    Aggs     map[string]interface{}
}

type ProductHit struct {
    Product    Product
    Score      float64
    Highlights map[string][]string
}

func executeSearch(es *elasticsearch.Client, indexName string, query map[string]interface{}) ([]Product, error) {
    data, _ := json.Marshal(query)
    res, err := es.Search(
        es.Search.WithIndex(indexName),
        es.Search.WithBody(bytes.NewReader(data)),
    )
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    if res.IsError() {
        return nil, fmt.Errorf("search error: %s", res.Status())
    }

    var result struct {
        Hits struct {
            Total struct{ Value int64 }
            Hits  []struct {
                Source Product `json:"_source"`
                Score  float64 `json:"_score"`
            }
        }
    }
    json.NewDecoder(res.Body).Decode(&result)

    products := make([]Product, len(result.Hits.Hits))
    for i, hit := range result.Hits.Hits {
        products[i] = hit.Source
    }
    return products, nil
}

func executeAdvancedSearch(es *elasticsearch.Client, indexName string, query map[string]interface{}) (SearchResult, error) {
    data, _ := json.Marshal(query)
    res, err := es.Search(
        es.Search.WithIndex(indexName),
        es.Search.WithBody(bytes.NewReader(data)),
    )
    if err != nil {
        return SearchResult{}, err
    }
    defer res.Body.Close()

    var raw map[string]interface{}
    json.NewDecoder(res.Body).Decode(&raw)

    hits := raw["hits"].(map[string]interface{})
    total := int64(hits["total"].(map[string]interface{})["value"].(float64))

    var products []ProductHit
    for _, hit := range hits["hits"].([]interface{}) {
        h := hit.(map[string]interface{})
        sourceBytes, _ := json.Marshal(h["_source"])
        var p Product
        json.Unmarshal(sourceBytes, &p)

        ph := ProductHit{
            Product: p,
            Score:   h["_score"].(float64),
        }

        // Extract highlights
        if hl, ok := h["highlight"].(map[string]interface{}); ok {
            ph.Highlights = make(map[string][]string)
            for field, frags := range hl {
                for _, frag := range frags.([]interface{}) {
                    ph.Highlights[field] = append(ph.Highlights[field], frag.(string))
                }
            }
        }
        products = append(products, ph)
    }

    return SearchResult{
        Total:    total,
        Products: products,
        Aggs:     raw["aggregations"].(map[string]interface{}),
    }, nil
}

Contoh Program Lengkap #

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "log"
    "strings"
    "time"

    "github.com/elastic/go-elasticsearch/v8"
)

const indexName = "products_demo"

func main() {
    es, err := elasticsearch.NewDefaultClient()
    if err != nil {
        log.Fatal(err)
    }

    // Cek koneksi
    res, err := es.Info()
    if err != nil {
        log.Fatal("Tidak bisa terhubung ke Elasticsearch:", err)
    }
    defer res.Body.Close()
    fmt.Println("✓ Terhubung ke Elasticsearch")

    // Hapus dan buat ulang index untuk demo
    es.Indices.Delete([]string{indexName})
    if err := createIndex(es, indexName); err != nil {
        log.Fatal("Create index:", err)
    }
    fmt.Println("✓ Index dibuat:", indexName)

    // Bulk index produk
    products := []Product{
        {
            ID: "1", Name: "Laptop Pro Gaming",
            Description: "Laptop gaming high-end dengan GPU RTX 4090",
            Category: "elektronik", Brand: "ASUS",
            Tags: []string{"laptop", "gaming", "rtx"}, Price: 35_000_000, Stock: 5, Rating: 4.8,
            IsActive: true, CreatedAt: time.Now(),
        },
        {
            ID: "2", Name: "Laptop Ultrabook",
            Description: "Laptop tipis dan ringan untuk profesional",
            Category: "elektronik", Brand: "Dell",
            Tags: []string{"laptop", "ultrabook", "tipis"}, Price: 18_000_000, Stock: 12, Rating: 4.5,
            IsActive: true, CreatedAt: time.Now(),
        },
        {
            ID: "3", Name: "Mouse Gaming RGB",
            Description: "Mouse gaming dengan sensor presisi tinggi dan lampu RGB",
            Category: "elektronik", Brand: "Logitech",
            Tags: []string{"mouse", "gaming", "rgb"}, Price: 750_000, Stock: 45, Rating: 4.6,
            IsActive: true, CreatedAt: time.Now(),
        },
        {
            ID: "4", Name: "Keyboard Mechanical TKL",
            Description: "Keyboard mechanical tenkeyless dengan switch red linear",
            Category: "elektronik", Brand: "Keychron",
            Tags: []string{"keyboard", "mechanical"}, Price: 1_200_000, Stock: 30, Rating: 4.7,
            IsActive: true, CreatedAt: time.Now(),
        },
        {
            ID: "5", Name: "Kaos Gaming Oversize",
            Description: "Kaos gaming oversize bahan premium 100% cotton",
            Category: "fashion", Brand: "GameWear",
            Tags: []string{"kaos", "gaming", "oversize"}, Price: 150_000, Stock: 200, Rating: 4.3,
            IsActive: true, CreatedAt: time.Now(),
        },
        {
            ID: "6", Name: "Monitor 4K 144Hz",
            Description: "Monitor gaming 4K dengan refresh rate 144Hz dan HDR",
            Category: "elektronik", Brand: "LG",
            Tags: []string{"monitor", "4k", "gaming"}, Price: 12_000_000, Stock: 8, Rating: 4.9,
            IsActive: true, CreatedAt: time.Now(),
        },
    }

    if err := bulkIndex(es, indexName, products); err != nil {
        log.Fatal("Bulk index:", err)
    }

    // Tunggu sebentar agar data terindeks
    time.Sleep(1 * time.Second)

    // Search: full-text
    fmt.Println("\n=== Full-Text Search: 'laptop gaming' ===")
    results, err := search(es, indexName, "laptop gaming")
    if err != nil {
        log.Println(err)
    } else {
        for _, p := range results {
            fmt.Printf("  %-25s Rp%10.0f ★%.1f\n", p.Name, p.Price, p.Rating)
        }
    }

    // Advanced search: filter + sort
    fmt.Println("\n=== Advanced Search: elektronik, Rp500rb-Rp15jt, sort harga ===")
    adv, err := advancedSearch(es, indexName, SearchParams{
        Category: "elektronik",
        MinPrice: 500_000,
        MaxPrice: 15_000_000,
        SortBy:   "price_asc",
        Page:     1,
        PerPage:  10,
    })
    if err != nil {
        log.Println(err)
    } else {
        fmt.Printf("  Total: %d hasil\n", adv.Total)
        for _, hit := range adv.Products {
            fmt.Printf("  %-25s Rp%10.0f (score: %.2f)\n",
                hit.Product.Name, hit.Product.Price, hit.Score)
            for field, frags := range hit.Highlights {
                fmt.Printf("    [%s] %s\n", field, strings.Join(frags, " | "))
            }
        }
    }

    // Aggregation analytics
    fmt.Println()
    if err := productAnalytics(es, indexName); err != nil {
        log.Println(err)
    }

    // Delete index setelah demo
    es.Indices.Delete([]string{indexName})
}

Ringkasan #

  • elasticsearch.NewClient(cfg) untuk koneksi; selalu Ping/cek Info() untuk verifikasi koneksi aktif.
  • Mapping mendefinisikan tipe field sebelum indexing — text untuk full-text search, keyword untuk filter/aggregation exact match.
  • Bulk API untuk indexing data besar — jauh lebih efisien dari indexing satu per satu.
  • multi_match untuk search di banyak field sekaligus; gunakan ^ untuk boost (misalnya "name^3").
  • Bool query dengan must, should, filter, must_notfilter tidak mempengaruhi relevance score.
  • fuzziness: "AUTO" untuk toleransi typo — sangat berguna untuk search box produk.
  • Highlighting untuk menampilkan konteks match kepada pengguna — tampilkan teks yang cocok dengan markup.
  • Aggregation untuk faceted search (filter by category, price range) dan analytics tanpa query terpisah.
  • "size": 0 untuk query analytics-only — tidak perlu mengembalikan dokumen, hanya agregasi.
  • Index alias untuk zero-downtime reindexing — tambahkan alias ke index baru, lalu switch alias dari lama ke baru.

← Sebelumnya: MongoDB   Berikutnya: Kafka →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact