✦ Selamat Idul Fitri 1447 H πŸŒ™ Taqabbalallahu minna wa minkum. Mohon maaf lahir dan batin. ✦
Oprek Blog

Menambahkan Fitur Pencarian di Blog PHP Tanpa Library Tambahan

Β· 0 komentar Β· Β± 3 menit baca Β· πŸ‘ 78 dilihat

Fitur pencarian itu kelihatannya sederhana. Pengguna ketik sesuatu, artikel yang relevan muncul. Tapi ketika saya pertama coba implementasikan di blog ini, ada beberapa hal yang tidak saya pikirkan sebelumnya.

Ini catatan dari proses membangunnya β€” dengan PHP murni dan MySQL, tanpa Elasticsearch, tanpa Algolia, tanpa library pencarian apapun.

Pendekatan Pertama: LIKE yang Naif

Implementasi pertama saya adalah yang paling sederhana yang bisa dibayangkan:

$keyword = $_GET['q'] ?? '';
$keyword = '%' . $keyword . '%';
$stmt = $pdo->prepare(
"SELECT id, title, slug, created_at
FROM posts
WHERE title LIKE ? OR content LIKE ?"
);
$stmt->execute([$keyword, $keyword]);

Ini bekerja. Tapi ada masalah yang langsung kelihatan: LIKE '%keyword%' tidak bisa menggunakan index. MySQL harus membaca setiap baris dan setiap karakter konten untuk mencari kecocokan. Untuk database kecil tidak masalah, tapi tidak skalabel.

Ada juga masalah lain: pencarian ini case-sensitive di beberapa konfigurasi MySQL, dan tidak menangani typo atau variasi kata.

Pindah ke FULLTEXT Search

MySQL punya fitur FULLTEXT search yang jauh lebih efisien untuk pencarian teks. Langkah pertama adalah menambahkan FULLTEXT index:

ALTER TABLE posts ADD FULLTEXT INDEX idx_fulltext_search (title, content);

Lalu query-nya berubah jadi:

$keyword = $_GET['q'] ?? '';
$keyword = htmlspecialchars(strip_tags($keyword));
if (strlen(trim($keyword)) < 3) {
// FULLTEXT search tidak efektif untuk keyword kurang dari 3 karakter
respond(['results' => [], 'message' => 'Masukkan minimal 3 karakter']);
}
$stmt = $pdo->prepare(
"SELECT id, title, slug, created_at,
MATCH(title, content) AGAINST(? IN BOOLEAN MODE) AS relevance_score
FROM posts
WHERE MATCH(title, content) AGAINST(? IN BOOLEAN MODE)
ORDER BY relevance_score DESC
LIMIT 10"
);
$stmt->execute([$keyword, $keyword]);

FULLTEXT search menggunakan index, jauh lebih efisien dari LIKE. Ia juga memberikan skor relevansi β€” artikel yang keywordnya muncul di judul mendapat skor lebih tinggi dari yang hanya muncul di konten.

Boolean Mode: Fleksibilitas Lebih

Dengan IN BOOLEAN MODE, pengguna bisa menggunakan operator pencarian:

  • +flutter β€” kata ini harus ada
  • -wordpress β€” kata ini tidak boleh ada
  • "flutter dart" β€” frasa persis ini
  • flutter* β€” prefix search (flutter, flutterwave, dll)

Kebanyakan pengguna tidak akan tahu operator-operator ini, tapi untuk pengguna yang familiar dengan pencarian canggih, ini berguna. Dan untuk pencarian normal tanpa operator, hasilnya tetap baik.

Highlight Keyword di Hasil Pencarian

Setelah dapat hasil pencarian, saya tambahkan highlight untuk menunjukkan di mana keyword muncul:

function highlightKeyword(string $text, string $keyword): string {
if (empty($keyword)) return $text;
$keywords = explode(' ', trim($keyword));
foreach ($keywords as $word) {
if (strlen($word) < 2) continue;
$text = preg_replace(
'/(' . preg_quote($word, '/') . ')/iu',
'<mark>$1</mark>',
$text
);
}
return $text;
}
// Penggunaan:
$snippet = substr(strip_tags($article['content']), 0, 200);
$highlightedSnippet = highlightKeyword($snippet, $keyword);

Fungsi ini mengganti setiap kemunculan keyword dengan tag HTML <mark> yang secara default menampilkan highlight kuning di browser. Flag i untuk case-insensitive, u untuk dukungan Unicode (penting untuk teks berbahasa Indonesia dengan karakter khusus).

Sanitasi Input: Wajib, Tidak Bisa Ditawar

Ini yang paling kritis dan sering diabaikan tutorial yang fokus ke fitur:

function sanitizeSearchKeyword(string $input): string {
// Hapus tag HTML
$input = strip_tags($input);
// Konversi karakter khusus HTML
$input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
// Batasi panjang
$input = substr($input, 0, 100);
// Hapus karakter yang bisa mengganggu FULLTEXT operator
$input = preg_replace('/[+\-><()~*"@]+/', ' ', $input);
return trim($input);
}

Dengan PDO dan prepared statement yang sudah digunakan, SQL injection sudah terlindungi. Tapi sanitasi tetap diperlukan untuk mencegah XSS dan input yang bisa merusak query FULLTEXT.

Caching Hasil Pencarian

Untuk keyword yang sama yang dicari berulang kali, saya tambahkan cache sederhana menggunakan file:

function getCachedResult(string $keyword): ?array {
$cacheFile = sys_get_temp_dir() . '/search_' . md5($keyword) . '.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 3600) {
return json_decode(file_get_contents($cacheFile), true);
}
return null;
}
function cacheResult(string $keyword, array $results): void {
$cacheFile = sys_get_temp_dir() . '/search_' . md5($keyword) . '.json';
file_put_contents($cacheFile, json_encode($results));
}

Cache berlaku satu jam. Untuk blog yang isinya tidak berubah setiap menit, ini cukup. Kalau artikel baru dipublish, cache lama untuk keyword yang terkait memang akan memberikan hasil lama sampai expired β€” tradeoff yang acceptable.

Keterbatasan dan Alternatif

FULLTEXT MySQL bagus untuk kasus ini, tapi punya keterbatasan: tidak menangani sinonim, tidak ada fuzzy search untuk typo, dan untuk konten yang sangat besar butuh tuning yang lebih serius.

Kalau kebutuhan pencariannya lebih kompleks β€” misalnya aplikasi dengan ratusan ribu dokumen β€” solusi dedicated seperti Elasticsearch atau Meilisearch lebih tepat. Tapi untuk blog personal atau aplikasi UMKM sederhana, FULLTEXT MySQL sudah sangat cukup.

Punya cara lain implementasi pencarian di PHP yang menarik? Atau ada pertanyaan soal pendekatan di atas? Diskusi di komentar selalu saya baca dan saya balas.


Komentar

Belum ada komentar. Jadilah yang pertama menulis.

Tulis Komentar

↑