Elasticsearch #

Elasticsearch adalah mesin pencari dan analitik terdistribusi yang dibangun di atas Apache Lucene. Berbeda dengan database pada umumnya, Elasticsearch dirancang khusus untuk satu hal: mencari data dengan sangat cepat dan relevan, bahkan dalam skala miliaran dokumen. Ia menyimpan data dalam format JSON yang diindeks secara penuh, sehingga setiap field bisa dijadikan kriteria pencarian tanpa perlu query yang lambat. Di ekosistem TypeScript, official client @elastic/elasticsearch sudah dilengkapi dengan tipe yang komprehensif, sehingga kamu bisa menulis query Elasticsearch dengan autocomplete dan type checking penuh — meminimalkan kesalahan yang baru terlihat saat runtime.

Instalasi #

npm install @elastic/elasticsearch

Untuk TypeScript, tidak perlu paket @types tambahan karena tipe sudah tersedia di dalam paket utama.

# verifikasi instalasi
npx tsc --version  # pastikan TypeScript >= 4.5

Konsep Dasar Elasticsearch #

Sebelum menulis kode, penting memahami terminologi Elasticsearch dan padanannya dengan konsep database relasional:

ElasticsearchDatabase RelasionalKeterangan
IndexTabelKumpulan dokumen dengan tipe data serupa
DocumentRow / barisSatu unit data dalam format JSON
FieldKolomProperti dalam dokumen
MappingSchemaDefinisi tipe data untuk setiap field
ShardPartisiPotongan index untuk distribusi data
ReplicaBackupSalinan shard untuk fault tolerance

Konsep yang paling berbeda dari database biasa adalah inverted index — cara Elasticsearch menyimpan data untuk pencarian. Alih-alih menyimpan “dokumen A berisi kata X”, Elasticsearch menyimpan “kata X ada di dokumen A, B, C”. Struktur inilah yang membuat pencarian teks begitu cepat.

flowchart LR
    A["Dokumen masuk:\n'Laptop Gaming Asus'"] --> B[Analyzer]
    B --> C["Token:\n'laptop', 'gaming', 'asus'"]
    C --> D[(Inverted Index)]
    D --> E["'laptop' → doc1, doc5, doc9\n'gaming' → doc1, doc3, doc7\n'asus' → doc1, doc8"]

Koneksi ke Elasticsearch #

import { Client } from "@elastic/elasticsearch";

// koneksi ke Elasticsearch lokal
const client = new Client({
  node: "http://localhost:9200",
});

// koneksi ke Elastic Cloud atau dengan autentikasi
const clientCloud = new Client({
  node: "https://my-deployment.es.us-east-1.aws.found.io",
  auth: {
    apiKey: process.env.ELASTIC_API_KEY!,
  },
  // atau gunakan username/password
  // auth: {
  //   username: "elastic",
  //   password: process.env.ELASTIC_PASSWORD!,
  // },
  tls: {
    rejectUnauthorized: false, // hanya untuk development
  },
});

// verifikasi koneksi
async function cekKoneksi(): Promise<void> {
  try {
    const info = await client.info();
    console.log(`Terhubung ke Elasticsearch ${info.version.number}`);
  } catch (error) {
    console.error("Gagal terhubung:", error);
    throw error;
  }
}
Untuk development lokal, jalankan Elasticsearch via Docker: docker run -d --name elasticsearch -p 9200:9200 -e "discovery.type=single-node" -e "xpack.security.enabled=false" elasticsearch:8.13.0

Mapping dan Index #

Mapping mendefinisikan bagaimana setiap field dalam dokumen disimpan dan diindeks. Mendefinisikan mapping secara eksplisit — alih-alih membiarkan Elasticsearch menebak tipenya — adalah praktik yang wajib di production.

Tipe Field yang Sering Digunakan #

// tipe field di mapping:
// text      → full-text search, dianalisis oleh analyzer
// keyword   → exact match, untuk filter/sort/aggregation
// integer, float, double → angka
// boolean   → true/false
// date      → tanggal dan waktu
// object    → nested object sederhana
// nested    → array of object dengan query independen
// geo_point → koordinat lat/lon

Membuat Index dengan Mapping #

interface Produk {
  nama: string;
  deskripsi: string;
  kategori: string;
  harga: number;
  stok: number;
  tags: string[];
  aktif: boolean;
  createdAt: string; // ISO 8601
}

async function buatIndexProduk(): Promise<void> {
  const indexAda = await client.indices.exists({ index: "produk" });

  if (indexAda) {
    console.log("Index 'produk' sudah ada");
    return;
  }

  await client.indices.create({
    index: "produk",
    body: {
      settings: {
        number_of_shards: 1,    // cukup untuk development/skala kecil
        number_of_replicas: 1,
        analysis: {
          analyzer: {
            // analyzer kustom untuk bahasa Indonesia
            analyzer_indonesia: {
              type: "custom",
              tokenizer: "standard",
              filter: ["lowercase", "asciifolding"],
            },
          },
        },
      },
      mappings: {
        properties: {
          nama: {
            type: "text",
            analyzer: "analyzer_indonesia",
            fields: {
              keyword: { type: "keyword" }, // untuk exact match & sort
            },
          },
          deskripsi: {
            type: "text",
            analyzer: "analyzer_indonesia",
          },
          kategori: {
            type: "keyword", // exact match, tidak dianalisis
          },
          harga: { type: "float" },
          stok: { type: "integer" },
          tags: { type: "keyword" },
          aktif: { type: "boolean" },
          createdAt: {
            type: "date",
            format: "strict_date_optional_time",
          },
        },
      },
    },
  });

  console.log("Index 'produk' berhasil dibuat");
}

Indexing Dokumen #

“Indexing” di Elasticsearch berarti menyimpan dokumen ke dalam index — bukan membuat index seperti di database. Setiap dokumen yang disimpan langsung tersedia untuk dicari.

Simpan Satu Dokumen #

async function simpanProduk(produk: Produk): Promise<string> {
  const hasil = await client.index({
    index: "produk",
    document: produk,
    // id bisa ditentukan manual atau dibiarkan di-generate otomatis
    // id: "produk-001",
  });

  return hasil._id; // ID yang di-generate Elasticsearch
}

// dengan ID yang sudah ditentukan — berguna untuk sinkronisasi dari database lain
async function simpanProdukDenganId(id: string, produk: Produk): Promise<void> {
  await client.index({
    index: "produk",
    id,
    document: produk,
    // refresh: "wait_for" — tunggu sampai dokumen bisa dicari sebelum return
    // gunakan hanya di test, bukan production (mahal)
  });
}

Bulk Indexing — Menyimpan Banyak Dokumen Sekaligus #

Untuk menyimpan banyak dokumen, selalu gunakan bulk API. Mengirim satu dokumen per request adalah anti-pattern yang sangat mahal — setiap request memiliki overhead jaringan dan index refresh.

// ANTI-PATTERN: loop dengan index satu per satu
async function simpanProdukSatuSatu(produkList: Produk[]): Promise<void> {
  for (const produk of produkList) {
    await client.index({ index: "produk", document: produk }); // ✗ N request untuk N dokumen
  }
}

// BENAR: gunakan bulk API
async function bulkSimpanProduk(produkList: Array<{ id: string; data: Produk }>): Promise<{
  berhasil: number;
  gagal: number;
}> {
  const operations = produkList.flatMap(({ id, data }) => [
    { index: { _index: "produk", _id: id } },
    data,
  ]);

  const hasil = await client.bulk({
    operations,
    refresh: false, // jangan tunggu refresh — biarkan Elasticsearch refresh secara periodik
  });

  const gagal = hasil.items.filter((item) => item.index?.error).length;
  const berhasil = hasil.items.length - gagal;

  if (hasil.errors) {
    const errorItems = hasil.items
      .filter((item) => item.index?.error)
      .slice(0, 5); // log maksimal 5 error pertama
    console.error("Sebagian dokumen gagal diindex:", errorItems);
  }

  return { berhasil, gagal };
}

Update dan Delete Dokumen #

// update parsial — hanya field yang disertakan yang berubah
async function updateProduk(id: string, perubahan: Partial<Produk>): Promise<void> {
  await client.update({
    index: "produk",
    id,
    doc: perubahan,
    doc_as_upsert: false, // false = error jika dokumen tidak ada
  });
}

// update menggunakan script — untuk operasi atomik
async function tambahStok(id: string, jumlah: number): Promise<void> {
  await client.update({
    index: "produk",
    id,
    script: {
      source: "ctx._source.stok += params.jumlah",
      params: { jumlah },
    },
  });
}

// hapus dokumen
async function hapusProduk(id: string): Promise<boolean> {
  try {
    await client.delete({ index: "produk", id });
    return true;
  } catch (error: any) {
    if (error?.meta?.statusCode === 404) return false;
    throw error;
  }
}

// hapus berdasarkan query
async function hapusProdukTidakAktif(): Promise<number> {
  const hasil = await client.deleteByQuery({
    index: "produk",
    body: {
      query: {
        term: { aktif: false },
      },
    },
  });
  return hasil.deleted ?? 0;
}

Query DSL — Mencari Dokumen #

Query DSL (Domain Specific Language) adalah cara utama berinteraksi dengan Elasticsearch. Semua query direpresentasikan sebagai objek JSON.

flowchart TD
    A[Query DSL] --> B[Query Context]
    A --> C[Filter Context]
    B --> D["Menghitung relevance score\nDokumen lebih relevan = score lebih tinggi\nContoh: match, multi_match, fuzzy"]
    C --> E["Ya atau Tidak — tidak ada score\nLebih cepat & bisa di-cache\nContoh: term, range, exists"]

Memahami perbedaan query context dan filter context sangat penting untuk performa. Gunakan filter context sebisa mungkin untuk kondisi yang bersifat binary (aktif/tidak aktif, range harga, dll), dan simpan query context untuk pencarian teks yang memerlukan ranking relevansi.

interface HasilCari<T> {
  hits: Array<{
    _id: string;
    _score: number;
    _source: T;
  }>;
  total: number;
}

async function cariProduk(keyword: string): Promise<HasilCari<Produk>> {
  const hasil = await client.search<Produk>({
    index: "produk",
    body: {
      query: {
        match: {
          nama: {
            query: keyword,
            fuzziness: "AUTO", // toleransi typo otomatis
          },
        },
      },
    },
  });

  return {
    hits: hasil.hits.hits.map((hit) => ({
      _id: hit._id!,
      _score: hit._score ?? 0,
      _source: hit._source as Produk,
    })),
    total:
      typeof hasil.hits.total === "number"
        ? hasil.hits.total
        : (hasil.hits.total?.value ?? 0),
  };
}

// multi_match — cari di beberapa field sekaligus
async function cariMultiField(keyword: string): Promise<HasilCari<Produk>> {
  const hasil = await client.search<Produk>({
    index: "produk",
    body: {
      query: {
        multi_match: {
          query: keyword,
          fields: [
            "nama^3",       // bobot 3x — lebih relevan jika ada di nama
            "deskripsi^1",  // bobot 1x
            "tags^2",       // bobot 2x
          ],
          type: "best_fields", // ambil score terbaik dari semua field
          fuzziness: "AUTO",
        },
      },
    },
  });

  return {
    hits: hasil.hits.hits.map((hit) => ({
      _id: hit._id!,
      _score: hit._score ?? 0,
      _source: hit._source as Produk,
    })),
    total:
      typeof hasil.hits.total === "number"
        ? hasil.hits.total
        : (hasil.hits.total?.value ?? 0),
  };
}

Bool Query — Kombinasi Kondisi #

Bool query adalah cara menggabungkan beberapa query. Ini adalah query yang paling sering digunakan di production karena hampir selalu ada lebih dari satu kondisi pencarian.

// must      → harus cocok, mempengaruhi score
// should    → bagus jika cocok, meningkatkan score
// must_not  → tidak boleh cocok, tidak mempengaruhi score
// filter    → harus cocok, tidak mempengaruhi score (lebih cepat)

async function cariProdukLanjutan(params: {
  keyword?: string;
  kategori?: string;
  hargaMin?: number;
  hargaMaks?: number;
  tags?: string[];
  halaman?: number;
  perHalaman?: number;
}): Promise<HasilCari<Produk> & { halaman: number; totalHalaman: number }> {
  const { keyword, kategori, hargaMin, hargaMaks, tags, halaman = 1, perHalaman = 10 } = params;
  const from = (halaman - 1) * perHalaman;

  const must: any[] = [];
  const filter: any[] = [];

  // full-text search masuk ke must (mempengaruhi relevance)
  if (keyword) {
    must.push({
      multi_match: {
        query: keyword,
        fields: ["nama^3", "deskripsi", "tags^2"],
        fuzziness: "AUTO",
      },
    });
  }

  // kondisi binary masuk ke filter (tidak mempengaruhi score, bisa di-cache)
  filter.push({ term: { aktif: true } });

  if (kategori) {
    filter.push({ term: { kategori } });
  }

  if (hargaMin !== undefined || hargaMaks !== undefined) {
    const range: any = {};
    if (hargaMin !== undefined) range.gte = hargaMin;
    if (hargaMaks !== undefined) range.lte = hargaMaks;
    filter.push({ range: { harga: range } });
  }

  if (tags && tags.length > 0) {
    filter.push({ terms: { tags } }); // dokumen mengandung salah satu tag
  }

  const hasil = await client.search<Produk>({
    index: "produk",
    from,
    size: perHalaman,
    body: {
      query: {
        bool: {
          must: must.length > 0 ? must : [{ match_all: {} }],
          filter,
        },
      },
      sort: keyword
        ? [{ _score: "desc" }]                         // jika ada keyword, sort by relevance
        : [{ createdAt: "desc" }],                     // jika tidak, sort by terbaru
    },
  });

  const total =
    typeof hasil.hits.total === "number"
      ? hasil.hits.total
      : (hasil.hits.total?.value ?? 0);

  return {
    hits: hasil.hits.hits.map((hit) => ({
      _id: hit._id!,
      _score: hit._score ?? 0,
      _source: hit._source as Produk,
    })),
    total,
    halaman,
    totalHalaman: Math.ceil(total / perHalaman),
  };
}

Query Tambahan yang Sering Digunakan #

// term — exact match untuk keyword field
{ term: { kategori: "elektronik" } }

// terms — exact match salah satu dari beberapa nilai
{ terms: { kategori: ["elektronik", "komputer"] } }

// range — filter berdasarkan rentang nilai
{ range: { harga: { gte: 100000, lte: 5000000 } } }
{ range: { createdAt: { gte: "2024-01-01", lte: "2024-12-31" } } }

// exists — dokumen yang memiliki field tertentu
{ exists: { field: "deskripsi" } }

// wildcard — pattern matching (lambat, hindari di production)
{ wildcard: { "nama.keyword": "*gaming*" } }

// prefix — dokumen yang field-nya dimulai dengan prefix tertentu
{ prefix: { "nama.keyword": "laptop" } }

Aggregation #

Aggregation di Elasticsearch memungkinkan kamu menghitung statistik, membuat histogram, dan mengelompokkan data — mirip dengan GROUP BY di SQL tapi jauh lebih ekspresif. Aggregation berjalan di atas hasil query, sehingga kamu bisa menggabungkan pencarian dan analitik dalam satu request.

interface AggregasiProduk {
  perKategori: Array<{
    key: string;
    jumlah: number;
    rataRataHarga: number;
    minHarga: number;
    maxHarga: number;
  }>;
  distribusiHarga: Array<{
    key: number;
    jumlah: number;
  }>;
  totalProdukAktif: number;
}

async function statistikProduk(keyword?: string): Promise<AggregasiProduk> {
  const hasil = await client.search({
    index: "produk",
    size: 0, // kita hanya butuh aggregasi, bukan dokumen
    body: {
      query: keyword
        ? { match: { nama: keyword } }
        : { match_all: {} },

      aggs: {
        // terms aggregation — kelompokkan berdasarkan nilai field
        per_kategori: {
          terms: {
            field: "kategori",
            size: 20, // maksimal 20 kategori
            order: { _count: "desc" },
          },
          // sub-aggregation — hitung statistik di dalam setiap bucket
          aggs: {
            rata_rata_harga: { avg: { field: "harga" } },
            min_harga: { min: { field: "harga" } },
            max_harga: { max: { field: "harga" } },
          },
        },

        // histogram — distribusi berdasarkan rentang nilai
        distribusi_harga: {
          histogram: {
            field: "harga",
            interval: 1000000, // setiap 1 juta
            min_doc_count: 1,
          },
        },

        // filter aggregation — hitung subset tertentu
        produk_aktif: {
          filter: { term: { aktif: true } },
        },
      },
    },
  });

  const aggs = hasil.aggregations as any;

  return {
    perKategori: (aggs.per_kategori.buckets as any[]).map((bucket) => ({
      key: bucket.key,
      jumlah: bucket.doc_count,
      rataRataHarga: Math.round(bucket.rata_rata_harga.value ?? 0),
      minHarga: bucket.min_harga.value ?? 0,
      maxHarga: bucket.max_harga.value ?? 0,
    })),

    distribusiHarga: (aggs.distribusi_harga.buckets as any[]).map((bucket) => ({
      key: bucket.key,
      jumlah: bucket.doc_count,
    })),

    totalProdukAktif: aggs.produk_aktif.doc_count,
  };
}

Date Histogram — Analitik Berdasarkan Waktu #

async function trendProdukBaruPerBulan(): Promise<
  Array<{ bulan: string; jumlah: number }>
> {
  const hasil = await client.search({
    index: "produk",
    size: 0,
    body: {
      aggs: {
        per_bulan: {
          date_histogram: {
            field: "createdAt",
            calendar_interval: "month",
            format: "yyyy-MM",
            order: { _key: "asc" },
          },
        },
      },
    },
  });

  const aggs = hasil.aggregations as any;
  return (aggs.per_bulan.buckets as any[]).map((bucket) => ({
    bulan: bucket.key_as_string,
    jumlah: bucket.doc_count,
  }));
}

Highlight dan Suggest #

Highlight — Tandai Kata yang Cocok #

Highlight mengembalikan potongan teks dengan kata yang cocok ditandai — fitur yang sangat berguna untuk menampilkan hasil pencarian ke pengguna.

async function cariDenganHighlight(keyword: string) {
  const hasil = await client.search<Produk>({
    index: "produk",
    body: {
      query: {
        multi_match: {
          query: keyword,
          fields: ["nama", "deskripsi"],
        },
      },
      highlight: {
        pre_tags: ["<mark>"],   // tag pembuka highlight
        post_tags: ["</mark>"], // tag penutup highlight
        fields: {
          nama: { number_of_fragments: 0 },        // tampilkan seluruh field nama
          deskripsi: {
            number_of_fragments: 2,  // maksimal 2 potongan
            fragment_size: 150,       // panjang setiap potongan (karakter)
          },
        },
      },
    },
  });

  return hasil.hits.hits.map((hit) => ({
    id: hit._id,
    produk: hit._source,
    highlight: {
      nama: hit.highlight?.nama?.[0],
      deskripsi: hit.highlight?.deskripsi?.join(" ... "),
    },
  }));
}

Autocomplete Suggest #

// untuk autocomplete, gunakan completion suggester
// mapping-nya harus didefinisikan terlebih dahulu:
// suggest: { type: "completion" }

async function autocomplete(prefix: string): Promise<string[]> {
  const hasil = await client.search({
    index: "produk",
    body: {
      suggest: {
        nama_suggest: {
          prefix,
          completion: {
            field: "suggest", // field dengan type "completion" di mapping
            size: 5,
            skip_duplicates: true,
          },
        },
      },
    },
    _source: false, // tidak perlu source dokumen, hanya suggestion
  });

  const suggest = hasil.suggest as any;
  return (suggest?.nama_suggest?.[0]?.options ?? []).map(
    (opt: any) => opt.text as string
  );
}

Pola Integrasi: Elasticsearch sebagai Search Layer #

Elasticsearch jarang berdiri sendiri sebagai database utama. Pola yang paling umum adalah menggunakan database relasional atau MongoDB sebagai source of truth, dan Elasticsearch sebagai search layer yang diisi secara sinkron.

sequenceDiagram
    participant Client
    participant API
    participant DB as Database Utama
    participant ES as Elasticsearch

    Client->>API: POST /produk (create)
    API->>DB: INSERT INTO produk
    DB-->>API: id dokumen baru
    API->>ES: index dokumen ke ES
    ES-->>API: acknowledged
    API-->>Client: 201 Created

    Client->>API: GET /produk/search?q=laptop
    API->>ES: search query
    ES-->>API: hasil pencarian + ID
    API-->>Client: hasil pencarian
// service yang menjaga sinkronisasi antara DB dan Elasticsearch
class ProdukService {
  constructor(
    private readonly db: any, // koneksi database utama
    private readonly es: Client
  ) {}

  async buat(data: Omit<Produk, "createdAt">): Promise<string> {
    // 1. simpan ke database utama terlebih dahulu
    const id = await this.db.insert("produk", {
      ...data,
      createdAt: new Date().toISOString(),
    });

    // 2. index ke Elasticsearch
    // jika ES gagal, dokumen tetap ada di DB — bisa di-retry
    try {
      await this.es.index({
        index: "produk",
        id,
        document: { ...data, createdAt: new Date().toISOString() },
        refresh: false,
      });
    } catch (error) {
      console.error(`Gagal index produk ${id} ke Elasticsearch:`, error);
      // simpan ke antrian retry (Redis, RabbitMQ, dll)
    }

    return id;
  }

  async cari(params: { keyword?: string; kategori?: string }) {
    // pencarian selalu lewat Elasticsearch
    return cariProdukLanjutan(params);
  }

  async ambilById(id: string): Promise<Produk | null> {
    // get by ID bisa langsung dari database utama (lebih konsisten)
    return this.db.findById("produk", id);
  }
}
Jangan gunakan Elasticsearch sebagai database utama. Elasticsearch tidak menjamin konsistensi data seperti database ACID — dokumen yang baru diindex belum tentu langsung bisa dicari (tergantung refresh interval, default 1 detik). Selalu gunakan Elasticsearch sebagai lapisan pencarian di atas database utama yang lebih konsisten.

Penanganan Error #

import { errors } from "@elastic/elasticsearch";

async function cariAman(keyword: string): Promise<HasilCari<Produk>> {
  try {
    return await cariProduk(keyword);
  } catch (error) {
    if (error instanceof errors.ResponseError) {
      // error dari Elasticsearch (status code 4xx atau 5xx)
      const statusCode = error.meta.statusCode;

      if (statusCode === 404) {
        // index tidak ditemukan
        throw new Error(`Index 'produk' tidak ditemukan. Pastikan sudah dibuat.`);
      }

      if (statusCode === 400) {
        // query tidak valid
        const detail = error.meta.body?.error?.reason ?? "Query tidak valid";
        throw new Error(`Query error: ${detail}`);
      }

      if (statusCode === 429) {
        // too many requests — Elasticsearch kelebihan beban
        throw new Error("Elasticsearch sedang sibuk, coba lagi sebentar");
      }
    }

    if (error instanceof errors.ConnectionError) {
      throw new Error("Tidak bisa terhubung ke Elasticsearch");
    }

    if (error instanceof errors.TimeoutError) {
      throw new Error("Request ke Elasticsearch timeout");
    }

    throw error;
  }
}

// re-index — berguna untuk sinkronisasi ulang jika ES tertinggal dari DB
async function reindexSemua(
  ambilSemua: () => AsyncGenerator<Array<{ id: string; data: Produk }>>
): Promise<void> {
  let totalBerhasil = 0;
  let totalGagal = 0;

  for await (const batch of ambilSemua()) {
    const { berhasil, gagal } = await bulkSimpanProduk(batch);
    totalBerhasil += berhasil;
    totalGagal += gagal;
    console.log(`Progress: ${totalBerhasil} berhasil, ${totalGagal} gagal`);
  }

  console.log(`Reindex selesai: ${totalBerhasil} dokumen`);
}

Kapan Menggunakan Elasticsearch #

Elasticsearch bukan solusi untuk semua masalah. Memahami batasannya sama pentingnya dengan memahami kemampuannya.

Gunakan Elasticsearch jika:
  ✓ Aplikasi memerlukan full-text search yang relevan (e-commerce, blog, portal berita)
  ✓ Perlu pencarian dengan toleransi typo (fuzziness)
  ✓ Butuh highlight kata yang cocok di hasil pencarian
  ✓ Perlu analitik real-time dari data yang terus masuk (log, event)
  ✓ Autocomplete dan suggest pada kolom pencarian
  ✓ Skala data sangat besar dan perlu horizontal scaling untuk search

Jangan gunakan Elasticsearch sebagai pengganti jika:
  ✗ Data memerlukan konsistensi ACID yang kuat — gunakan PostgreSQL/MySQL
  ✗ Query sederhana seperti "cari by ID" atau "filter by status" — overkill
  ✗ Data memiliki relasi kompleks yang memerlukan join — gunakan database relasional
  ✗ Butuh transaksi multi-dokumen yang atomik — Elasticsearch tidak mendukung ini
  ✗ Tim belum punya kapasitas untuk maintain cluster — pertimbangkan PostgreSQL full-text search

Ringkasan #

  • Elasticsearch bukan database utama — gunakan sebagai search layer di atas database yang lebih konsisten (PostgreSQL, MongoDB). Database utama adalah source of truth.
  • Definisikan mapping secara eksplisit sebelum menyimpan data. Mapping otomatis dari Elasticsearch sering tidak optimal — text vs keyword harus dipilih secara sadar sesuai kebutuhan query.
  • Query context vs filter context — gunakan filter untuk kondisi binary (aktif/tidak, range, exact match) karena lebih cepat dan bisa di-cache; gunakan must hanya untuk kondisi yang mempengaruhi relevance score.
  • Bool query adalah fondasi — hampir semua query production menggunakan kombinasi must, filter, should, dan must_not di dalam bool query.
  • Bulk API wajib untuk indexing massal — jangan pernah loop index() satu per satu; kirim dalam batch menggunakan bulk() untuk performa yang jauh lebih baik.
  • Aggregation menggantikan SQL GROUP BY — manfaatkan terms, histogram, date_histogram, dan sub-aggregation untuk laporan analitik langsung dari Elasticsearch.
  • Highlight dan fuzziness adalah fitur yang langsung meningkatkan UX — tampilkan kata yang cocok dengan highlight dan toleransi typo dengan fuzziness: "AUTO".
  • Tangkap errors.ResponseError untuk error dari server dan errors.ConnectionError untuk masalah jaringan — keduanya perlu penanganan yang berbeda.

← Sebelumnya: MongoDB   Berikutnya: Redis →

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