Tutorial: Membangun Sistem Full-Text Search (FTS) di Cloudflare D1 🔍
Punya blog statis berbasis HTML murni tapi ingin punya fitur pencarian pintar layaknya web dinamis besar? Menggunakan skrip pencarian pihak ketiga terkadang penuh iklan, dan menyewa server backend sendiri jelas bisa menguras dompet. Solusi modern paling elegan saat ini adalah memanfaatkan Cloudflare D1, yaitu database SQLite serverless yang berjalan secepat kilat langsung di jaringan edge Cloudflare.

Dalam tutorial kali ini, kita akan membangun sebuah skrip otomatisasi berbasis TypeScript dan Bun untuk memindai artikel HTML lokal kamu, membersihkan kontennya dari "sampah" kode yang tidak perlu, lalu mengunggahnya ke tabel pencarian khusus (FTS5) di Cloudflare D1. Yuk, siapkan kopimu dan mari kita mulai ngoding santai. ☕
🛠️ Prasyarat & Persiapan Awal
Sebelum mulai bertempur dengan barisan kode, pastikan kamu sudah menyiapkan beberapa amunisi berikut di komputermu:
- Bun atau Node.js sudah terinstal.
- Akun Cloudflare yang aktif.
- Wrangler CLI terinstal (bisa lewat
npm install -g wranglerataubun add -g wrangler).
Langkah 1: Setup Database D1 dan Tabel FTS5
Hal pertama yang wajib kita lakukan adalah membuat ruang penyimpanan D1 baru melalui terminal. Cukup jalankan perintah sakti ini:
wrangler d1 create layarkosong-dbCloudflare akan merespons dengan memberikan detail konfigurasi. Jangan lupa masukkan detail tersebut ke dalam file wrangler.toml kamu.
Setelah database terbuat, kita butuh tabel pencarian. Untungnya, SQLite memiliki modul bawaan yang luar biasa ngebut bernama FTS5 (Full-Text Search). Buatlah sebuah file SQL bernama schema.sql dan masukkan struktur berikut:
-- Membuat tabel virtual berbasis FTS5 untuk pencarian cepat
CREATE VIRTUAL TABLE articles_fts USING fts5(
title,
content,
id UNINDEXED,
category UNINDEXED,
image UNINDEXED,
date UNINDEXED
);Pro-tip: Atribut UNINDEXED memberi tahu SQLite bahwa kolom tersebut cukup disimpan saja sebagai data pelengkap (misalnya untuk menampilkan gambar thumbnail di hasil pencarian), tanpa perlu repot-repot diindeks kata kuncinya. Ini menghemat ukuran database kamu secara signifikan!
Sekarang, tembakkan skema tersebut ke database remote kamu dengan perintah:
wrangler d1 execute layarkosong-db --remote --file=./schema.sqlLangkah 2: Inisialisasi Proyek & Dependencies
Buat folder baru untuk rumah skrip sinkronisasi ini, lalu instal *library*Cheerio. Cheerio ini fungsinya semacam pisau bedah untuk membongkar struktur file HTML layaknya jQuery, tapi berjalan murni di sisi server.
mkdir d1-sync-script && cd d1-sync-script
bun init -y
bun add cheerio
bun add -d typescript @types/node
Langkah 3: Menulis Script Pipeline (build-d1.ts)
Buat file baru bernama build-d1.ts. Kita akan merakit skrip ini menjadi 3 bagian utama: Modul Sanitasi, Ekstraktor HTML, dan Orkestrator Eksekusi. Seluruh baris kode di bawah disadur langsung dari sumber repositori resmi rawgithub proyek Layar Kosong.
3.1. Modul Sanitasi Teks (superCleanText)
Database pencarian itu pemilih, ia butuh teks yang benar-benar bersih. Jika ada simbol markdown nyasar, emoji berlebih, atau tanda petik tunggal yang luput dari escape, kueri SQL kamu bisa meledak (error) atau hasil pencariannya jadi kacau balau.
export function superCleanText(text: string): string {
if (!text) return "";
return text
// Tahap 1: Hapus simbol Markdown yang mungkin nyasar
.replace(/(\*\*|__)(.*?)\1/g, '$2')
.replace(/(\*|_)(.*?)\1/g, '$2')
.replace(/#+\s+/g, '')
.replace(/\[(.*?)\]\(.*?\)/g, '$1')
.replace(/>\s+/g, '')
.replace(/`{1,3}.*?`{1,3}/g, '')
// Tahap 2: Lenyapkan Emoji dan Unicode spesial agar FTS SQLite stabil
.replace(/[\u{1F300}-\u{1F9FF}]/gu, '')
.replace(/[\u{1F600}-\u{1F64F}]/gu, '')
.replace(/[\u{1F680}-\u{1F6FF}]/gu, '')
.replace(/[\u{2600}-\u{26FF}]/gu, '')
// Tahap 3: Netralisir SQL Keywords krusial untuk mencegah malfungsi kueri
.replace(/BEGIN\s+TRANSACTION/gi, 'BEGIN_TRANSACTION')
.replace(/COMMIT/gi, 'COMMIT_DONE')
.replace(/ROLLBACK/gi, 'ROLLBACK_DONE')
// Tahap 4: Satukan spasi berlebih & jalankan SQL Escape untuk tanda kutip (')
.replace(/\s+/g, ' ')
.replace(/'/g, "''")
.trim();
}3.2. Ekstraktor Konten HTML Menggunakan Cheerio
Lanjut ke fungsi untuk membaca file HTML, menyedot metadata (seperti judul dan gambar OpenGraph), lalu mengambil teks murni artikel dengan secara sadis membuang komponen noise seperti navigasi, *header, dan footer.
import { readFileSync } from "fs";
import * as cheerio from "cheerio";
export function extractArticleData(filePath: string, filename: string, category: string) {
const html = readFileSync(filePath, "utf-8");
const $ = cheerio.load(html);
// Ambil data metadata penting
const title = superCleanText($('title').text() || filename);
const image = $('meta[property="og:image"]').attr('content') || "/thumbnail.webp";
const dateISO = $('meta[property="article:published_time"]').attr('content') || new Date().toISOString();
// Singkirkan elemen sampah yang mengganggu akurasi pencarian
$('script, style, meta, link, noscript, i, header, footer, nav, aside').remove();
// Tentukan area konten utama secara berjenjang
const articleArea = $('article').length ? $('article')
: $('main').length ? $('main')
: $('body');
const bodyContent = superCleanText(articleArea.text());
return { title, image, dateISO, bodyContent, file: filename, category };
}3.3. Orkestrator Eksekusi Utama
Ini adalah bos dari skrip kita. Tugasnya memindai seluruh folder kategori blog, menyusun kueri SQL massal ke dalam satu file sementara (temp_sync.sql), dan memerintahkan Wrangler CLI untuk melakukan upload ke cloud secara otomatis.
import { readdirSync, writeFileSync, unlinkSync } from "fs";
import { join } from "path";
import { execSync } from "child_process";
const ROOT_DIR = "./deploy_dir"; // Sesuaikan dengan folder output HTML kamu
const ARTICLE_DIRS = ["gaya-hidup", "warta-tekno", "jejak-sejarah"];
const SQL_FILE = "./temp_sync.sql";
function main() {
console.log("Menginisialisasi pipeline sinkronisasi Cloudflare D1...");
// Perintah awal: Bersihkan database lama untuk fresh-build indeks baru
let sqlCommands: string[] = ["DELETE FROM articles_fts;"];
for (const cat of ARTICLE_DIRS) {
const fullPath = join(ROOT_DIR, cat);
const files = readdirSync(fullPath).filter(f => f.endsWith(".html"));
for (const file of files) {
// Abaikan file indeks utama halaman kategori jika ada
if (file === "index.html" || file.startsWith("agregat")) continue;
const data = extractArticleData(join(fullPath, file), file, cat);
// Susun kueri INSERT INTO ke tabel virtual FTS5
const sql = `INSERT INTO articles_fts (title, content, id, category, image, date) VALUES ('${data.title}', '${data.bodyContent}', '${data.file}', '${data.category}', '${data.image}', '${data.dateISO}');`;
sqlCommands.push(sql);
}
}
// Tulis kumpulan kueri ke file sementara
writeFileSync(SQL_FILE, sqlCommands.join("\n"));
try {
console.log("Sedang mengunggah data pencarian ke Cloudflare D1 secara remote...");
execSync(`bunx wrangler d1 execute layarkosong-db --remote --file=${SQL_FILE}`, { stdio: "inherit" });
console.log("Sinkronisasi sukses! Seluruh artikel berhasil masuk indeks FTS.");
} catch (error) {
console.error("Gagal mengeksekusi sinkronisasi ke Cloudflare:", error);
} finally {
// Apapun yang terjadi, hapus berkas kueri sementara demi keamanan data
unlinkSync(SQL_FILE);
}
}
main();Langkah 4: Menjalankan Otomatisasi & Implementasi Pencarian
Bagian terseru! Kamu tinggal mengeksekusi script di atas menggunakan Bun setiap kali selesai menulis artikel baru atau melakukan build situs statis:
bun build-d1.tsDi sisi frontend web, kamu cukup membuat sebuah fungsi Cloudflare Worker (sebagai API endpoint ringan) yang menerima query string dari kolom pencarian pembaca (misal: ?q=teknologi), lalu menjalankan kueri SQLite FTS secepat kilat ini:
SELECT id, title, category, image FROM articles_fts WHERE articles_fts MATCH 'teknologi' ORDER BY rank;Fitur ORDER BY rank adalah fungsi ajaib dari FTS5 yang otomatis mengurutkan artikel berdasarkan tingkat relevansi tertinggi terhadap kata kunci yang diketik. Canggih, kan?
Sebagai pengingat, waspadai saat menempatkan skrip ini jika ukuran database artikelmu sudah menyentuh ribuan baris, karena proses eksekusi CLI sekaligus (tanpa batching) bisa sedikit membebani memori lokal. Tapi untuk skala blog personal, skrip ini sudah sangat tangguh dan bisa diandalkan.
Artikel oleh Frijal, pengembang web yang berdomisili di Balikpapan. Tulisan ini diulas berdasarkan pengalaman langsung meracik otomatisasi serverless untuk mempercepat performa blog Layar Kosong. Ingin sharing atau ngobrol soal optimasi web statis? Sapa saya di Twitter x.com/responaja, atau intip dan oprek langsung keseluruhan kode sumber proyek ini di repositori GitHub github.com/frijal.