โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 9 ยท Migrasi dari CodeIgniter 3

Dari MySQL FULLTEXT ke mesin pencari

Pencarian adalah titik di mana satu request bisa menahan koneksi selama detik-detik dan menyeret seluruh portal. Ini urutan perbaikannya, dari yang gratis sampai yang butuh infrastruktur.

Sumber asli dev.mysql.com Resmi Rangkuman ~10 menit baca

Intisari

  • LIKE '%kata%' tidak bisa memakai index โ€” ia memindai seluruh tabel, selalu.
  • MySQL FULLTEXT jauh lebih baik dan gratis, tapi punya batas nyata untuk bahasa Indonesia.
  • Batas terbesarnya: tidak ada stemming โ€” "memberitakan" tidak cocok dengan "berita".
  • Pindah ke OpenSearch atau Meilisearch saat kamu butuh stemming, toleransi salah ketik, atau faset.
  • Apa pun mesinnya, pencarian harus ke replica dan hasilnya harus di-cache.

Kenapa LIKE '%kata%' berbahaya di trafikmu

EXPLAIN SELECT id, judul FROM artikel
WHERE judul LIKE '%rupiah%' OR isi LIKE '%rupiah%'
LIMIT 20;

-- type: ALL
-- key:  NULL
-- rows: 203847        โ† seluruh tabel, tiap kali
AkibatDi trafikmu
Memindai seluruh tabelRatusan milidetik sampai beberapa detik per kueri
Menahan koneksi selama ituSepuluh pencarian bersamaan bisa menghabiskan pool
Mengisi buffer poolData panas terusir; kueri lain ikut melambat
Tidak bisa di-cacheTiap kata kunci berbeda adalah kueri baru

Ini vektor penolakan layanan yang tidak sengaja โ€” dan bisa disengaja. Siapa pun bisa membuka halaman pencarianmu dan mengirim seratus kata kunci acak dalam semenit. Tidak ada satu pun yang terlihat seperti serangan, dan database-mu akan tersendat. Rate limit pencarian dari Fase 5 adalah pertahanan minimum; memperbaiki kuerinya adalah perbaikan sesungguhnya.

Langkah 1: FULLTEXT (gratis, cepat dipasang)

ALTER TABLE artikel
  ADD FULLTEXT INDEX ft_artikel (judul, ringkasan, isi)
  WITH PARSER ngram;
export async function cariArtikel(q: string, batas = 20) {
  return sql<{ id: number; judul: string; slug: string; skor: number }>`
    SELECT id, judul, slug,
           MATCH(judul, ringkasan, isi) AGAINST (${q} IN NATURAL LANGUAGE MODE) AS skor
    FROM artikel
    WHERE MATCH(judul, ringkasan, isi) AGAINST (${q} IN NATURAL LANGUAGE MODE)
      AND status = 'terbit'
    ORDER BY skor DESC
    LIMIT ${batas}
  `.execute(dbBaca);
}
ModePerilaku
NATURAL LANGUAGE MODEPeringkat relevansi; mengabaikan kata yang terlalu umum
BOOLEAN MODEMendukung +wajib -kecuali "frasa persis"
WITH QUERY EXPANSIONCari dua kali, perluas dari hasil pertama โ€” mahal

Batasnya untuk bahasa Indonesia

BatasContohDampak
Tidak ada stemming"memberitakan" tidak cocok dengan "berita"Besar โ€” imbuhan sangat produktif di bahasa Indonesia
Panjang token minimumngram_token_size default 2Kata pendek terlewat
Tidak ada toleransi salah ketik"ekonimi" tidak menemukan apa punSedang
Daftar kata umum bawaan berbahasa Inggris"yang", "dan", "di" tetap diindeksIndeks lebih besar, relevansi menurun
Tidak ada fasetFilter kategori + tanggal + penulisHarus WHERE tambahan, memperlambat

Ketiadaan stemming adalah batas yang paling terasa untuk portal berbahasa Indonesia. Imbuhan di bahasa Indonesia mengubah bentuk kata secara drastis: beritakan, memberitakan, diberitakan, pemberitaan semuanya berakar pada berita, dan FULLTEXT memperlakukannya sebagai lima kata yang tidak berhubungan. Pembaca yang mencari "pemberitaan rupiah" tidak akan menemukan artikel berjudul "Berita rupiah hari ini".

Langkah 2: mesin pencari terpisah

OpenSearchMeilisearch
Stemming bahasa IndonesiaYa (analyzer indonesian)Ya
Toleransi salah ketikBisa dikonfigurasiBawaan, bagus
Faset & filterSangat lengkapCukup
Kesulitan operasionalTinggiRendah
Layanan terkelola AWSYa (OpenSearch Service)Tidak
Kebutuhan memoriBesarSedang
Cocok kalauButuh agregasi & analitik log jugaHanya butuh pencarian yang bagus

Untuk portal berita yang butuh pencarian artikel yang bagus dan tidak butuh analitik log, Meilisearch biasanya pilihan yang lebih tepat: satu binari, konfigurasi sederhana, dan hasil yang langsung terasa lebih baik. OpenSearch masuk akal kalau kamu sudah memakainya untuk hal lain.

Menjaga indeks tetap sinkron

// src/lib/indeks.ts
export async function indeksArtikel(id: number) {
  const a = await dbBaca
    .selectFrom("artikel as a")
    .leftJoin("kategori as k", "k.id", "a.kategori_id")
    .select(["a.id", "a.judul", "a.ringkasan", "a.isi", "a.slug",
             "a.terbit_pada", "a.premium", "k.slug as kategori"])
    .where("a.id", "=", id)
    .executeTakeFirst();

  if (!a || a.status !== "terbit") {
    await mesin.index("artikel").deleteDocument(String(id));
    return;
  }

  await mesin.index("artikel").addDocuments([{
    id: a.id,
    judul: a.judul,
    ringkasan: a.ringkasan,
    // Indeks teks bersih, bukan HTML
    isi: bersihkanTeks(a.isi).slice(0, 20_000),
    slug: a.slug,
    kategori: a.kategori,
    premium: a.premium === 1,
    terbitPada: Math.floor(a.terbit_pada!.getTime() / 1000),
  }]);
}
Kapan mengindeksCara
Artikel diterbitkan / disuntingPanggil di action, setelah commit โ€” sama seperti purge cache
Artikel ditarikHapus dari indeks
Muat awal 200 ribu artikelSkrip batch, per 1.000 dokumen
RekonsiliasiTugas harian: bandingkan jumlah dokumen dengan jumlah baris

Indeks pencarian akan menyimpang dari database. Pengindeksan gagal, deploy memotong proses di tengah, atau seseorang menyunting langsung di database. Tugas rekonsiliasi harian yang membandingkan jumlah dokumen dan menemukan selisihnya jauh lebih murah daripada pembaca yang melaporkan "artikelnya tidak ketemu di pencarian" berbulan-bulan kemudian.

Yang berlaku untuk mesin apa pun

AturanKenapa
Pencarian membaca replicaJangan pernah mengganggu writer
Cache hasil untuk kata kunci populerSepuluh kata kunci teratas biasanya sebagian besar trafik pencarian
Batasi panjang kata kunciz.string().max(100)
Rate limit per IPFase 5
Halaman hasil noindex, followMencegah konten tipis terindeks
Jangan indeks isi premium penuhCuplikan hasil pencarian bisa membocorkan isi berbayar
Timeout ketat + fallbackMesin pencari mati โ‰  portal mati
// Fallback saat mesin pencari tidak tersedia
export async function cari(q: string) {
  try {
    return await mesin.index("artikel").search(q, { limit: 20 });
  } catch (e) {
    console.error(JSON.stringify({ level: "error", pesan: "mesin cari mati", e: String(e) }));
    // Jatuh ke FULLTEXT MySQL โ€” lebih buruk, tapi hidup
    return { hits: await cariArtikelFulltext(q, 20), degradasi: true };
  }
}

Latihan: jalankan EXPLAIN pada kueri pencarian CI3-mu yang sebenarnya dan catat rows-nya. Tambahkan index FULLTEXT dan bandingkan. Lalu uji batasnya: cari "pemberitaan" dan lihat apakah artikel berjudul "berita" muncul. Kalau tidak โ€” dan kemungkinan besar tidak โ€” kamu baru saja mengukur sendiri kenapa mesin pencari terpisah dibutuhkan.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.