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; selaluPing/cekInfo()untuk verifikasi koneksi aktif.- Mapping mendefinisikan tipe field sebelum indexing —
textuntuk full-text search,keyworduntuk filter/aggregation exact match.- Bulk API untuk indexing data besar — jauh lebih efisien dari indexing satu per satu.
multi_matchuntuk search di banyak field sekaligus; gunakan^untuk boost (misalnya"name^3").- Bool query dengan
must,should,filter,must_not—filtertidak 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": 0untuk 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.