Cara Membuat Fitur Pencarian (Search) di Laravel

Fitur pencarian hampir selalu dibutuhkan di aplikasi web, entah untuk mencari artikel, produk, pesanan, atau data user di panel admin. Di Laravel, membuatnya cukup sederhana dengan...

Cara Membuat Fitur Pencarian (Search) di Laravel
Iklan

Fitur pencarian hampir selalu dibutuhkan di aplikasi web, entah untuk mencari artikel, produk, pesanan, atau data user di panel admin. Di Laravel, membuatnya cukup sederhana dengan Eloquent. Masalahnya, versi "asal jalan" sering menyimpan bug yang baru ketahuan belakangan: filter status ikut bocor, kata kunci hilang saat pindah halaman, atau query jadi lambat ketika data sudah puluhan ribu baris.

Artikel ini memandu Anda membuat fitur pencarian yang rapi dari nol: form, query di controller, pencarian di banyak kolom dengan aman, menjaga kata kunci saat pagination, mencari di tabel relasi, sampai opsi fulltext index dan Laravel Scout untuk data besar. Semua contoh berlaku untuk Laravel 10, 11, dan 12 dengan PHP 8.x.

Persiapan: Model, Route, dan Data Contoh

Sebagai contoh, kita pakai tabel posts dengan kolom title, content, status, dan category_id. Kalau Anda belum punya CRUD dasarnya, siapkan dulu model dan migration-nya. Berikut migration minimal yang dipakai sepanjang artikel:

Iklan
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('category_id')->constrained();
    $table->string('title');
    $table->longText('content');
    $table->string('status')->default('draft');
    $table->timestamps();
});

Lalu daftarkan route di routes/web.php. Pencarian cukup memakai route index yang sama, tidak perlu route khusus:

use App\Http\Controllers\PostController;

Route::get('/artikel', [PostController::class, 'index'])->name('posts.index');

Langkah 1: Form Pencarian

Buat form sederhana dengan method GET agar kata kunci muncul di URL (bagus untuk berbagi & SEO):

<form action="/artikel" method="GET">
    <input type="text" name="q" value="{{ request('q') }}" placeholder="Cari...">
    <button type="submit">Cari</button>
</form>

Kenapa GET dan bukan POST? Pencarian tidak mengubah data apa pun, jadi secara konsep memang termasuk request baca. Dengan GET, URL hasilnya seperti /artikel?q=laravel bisa di-bookmark, dibagikan, dan tombol Back di browser bekerja normal. Form GET juga tidak memerlukan token @csrf. Kalau ingin memahami bedanya lebih dalam, baca perbedaan GET dan POST di PHP.

Versi yang sedikit lebih rapi memakai helper route() dan atribut type="search", sehingga di browser mobile keyboard menampilkan tombol cari dan ada ikon hapus di beberapa browser:

Iklan
<form action="{{ route('posts.index') }}" method="GET" role="search">
    <input type="search" name="q" value="{{ request('q') }}"
           placeholder="Cari artikel..." maxlength="100" aria-label="Kata kunci">
    <button type="submit">Cari</button>

    @if (request()->filled('q'))
        <a href="{{ route('posts.index') }}">Reset</a>
    @endif
</form>

Perhatikan value="{{ request('q') }}". Bagian ini membuat kata kunci tetap tampil di kotak input setelah halaman dimuat ulang. Karena memakai {{ }}, Blade otomatis meng-escape isinya, jadi aman dari XSS walaupun user mengetik tag HTML.

Langkah 2: Query Pencarian di Controller

Ini versi paling dasar yang sering ditemukan di tutorial:

public function index(Request $request)
{
    $query = Post::query();

    if ($request->filled('q')) {
        $keyword = $request->q;
        $query->where('title', 'like', "%{$keyword}%")
              ->orWhere('content', 'like', "%{$keyword}%");
    }

    $posts = $query->latest()->paginate(10);

    return view('posts.index', compact('posts'));
}

$request->filled('q') bernilai true hanya jika parameter q ada dan tidak kosong, jadi submit form tanpa isi tidak menjalankan pencarian. Tanda % di kiri dan kanan berarti "mengandung kata ini di posisi mana saja". Kode ini berjalan, tapi punya satu jebakan yang dibahas di langkah berikutnya.

Langkah 3: Pencarian di Banyak Kolom dengan Aman

Saat menggabungkan pencarian dengan filter lain, bungkus kondisi orWhere dalam grup agar logikanya tidak bocor:

Iklan
$query->where(function ($q) use ($keyword) {
    $q->where('title', 'like', "%{$keyword}%")
      ->orWhere('content', 'like', "%{$keyword}%");
});

Tanpa pembungkus function, kondisi orWhere bisa membatalkan filter status/kategori Anda. Supaya jelas, bandingkan SQL yang dihasilkan. Misalnya Anda hanya ingin menampilkan artikel berstatus published:

-- TANPA grup (salah)
select * from posts
where status = 'published' and title like '%laravel%' or content like '%laravel%'

-- DENGAN grup (benar)
select * from posts
where status = 'published' and (title like '%laravel%' or content like '%laravel%')

Di SQL, AND dievaluasi lebih dulu daripada OR. Pada versi tanpa grup, artikel draft yang kontennya mengandung kata "laravel" ikut tampil ke publik. Bug seperti ini jarang ketahuan saat testing karena datanya masih sedikit.

Sejak Laravel 10.47, ada cara yang lebih ringkas: method whereAny(). Method ini otomatis membungkus kondisinya dalam tanda kurung, jadi hasilnya sama dengan versi closure di atas:

$query->whereAny(['title', 'content'], 'like', "%{$keyword}%");

Pasangannya, whereAll(), mensyaratkan semua kolom cocok. Kalau proyek Anda masih di Laravel versi lebih lama, tetap gunakan closure.

Iklan

Langkah 4: Pertahankan Kata Kunci Saat Pagination

Agar kata kunci tidak hilang saat pindah halaman, tambahkan query string ke link pagination:

{{ $posts->withQueryString()->links() }}

Tanpa withQueryString(), link halaman 2 hanya berisi ?page=2, sehingga user tiba-tiba melihat semua artikel, bukan hasil pencariannya. Anda juga bisa memanggilnya langsung di controller agar view tetap bersih:

$posts = $query->latest()->paginate(10)->withQueryString();

Alternatifnya, ->appends(['q' => $request->q]) hanya menambahkan parameter tertentu. Ini berguna kalau URL Anda membawa parameter lain yang tidak ingin ikut. Penjelasan konsep pagination dari sisi SQL bisa Anda baca di tutorial pagination PHP dan MySQL.

Langkah 5: Menampilkan Hasil

@forelse ($posts as $post)
    <h3>{{ $post->title }}</h3>
@empty
    <p>Tidak ada hasil untuk "{{ request('q') }}".</p>
@endforelse

Directive @forelse menggabungkan perulangan dan kondisi kosong sekaligus. Supaya lebih informatif, tampilkan juga jumlah hasil. Method total() tersedia pada hasil paginate():

Iklan
@if (request()->filled('q'))
    <p>Ditemukan {{ $posts->total() }} hasil untuk "{{ request('q') }}"</p>
@endif

Catatan: total() tidak ada pada simplePaginate() karena paginator jenis itu sengaja tidak menghitung total baris.

Versi Lengkap Controller yang Siap Pakai

Setelah memahami bagian-bagiannya, berikut controller yang menggabungkan validasi, pencarian, filter status, dan pagination. Saya memakai method when() dari query builder, yang hanya menjalankan closure jika nilai pertamanya terisi, sehingga tidak perlu banyak if:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index(Request $request)
    {
        $validated = $request->validate([
            'q' => ['nullable', 'string', 'max:100'],
        ]);

        $keyword = trim($validated['q'] ?? '');

        $posts = Post::query()
            ->with('category')
            ->where('status', 'published')
            ->when($keyword !== '', function ($query) use ($keyword) {
                $query->where(function ($q) use ($keyword) {
                    $q->where('title', 'like', "%{$keyword}%")
                      ->orWhere('content', 'like', "%{$keyword}%");
                });
            })
            ->latest()
            ->paginate(10)
            ->withQueryString();

        return view('posts.index', compact('posts', 'keyword'));
    }
}

Validasi max:100 mencegah orang mengirim string ribuan karakter yang membebani database. with('category') memuat relasi sekaligus (eager loading) supaya tidak terjadi masalah N+1 query saat view menampilkan nama kategori.

Merapikan dengan Local Scope

Kalau logika pencarian dipakai di beberapa tempat, misalnya halaman publik dan panel admin, pindahkan ke local scope di model. Controller jadi lebih pendek dan aturannya cukup diubah di satu tempat:

Iklan
// app/Models/Post.php
use Illuminate\Database\Eloquent\Builder;

public function scopeSearch(Builder $query, ?string $keyword): Builder
{
    if (blank($keyword)) {
        return $query;
    }

    return $query->where(function ($q) use ($keyword) {
        $q->where('title', 'like', "%{$keyword}%")
          ->orWhere('content', 'like', "%{$keyword}%");
    });
}

Pemakaiannya cukup satu baris:

$posts = Post::search($request->q)->latest()->paginate(10)->withQueryString();

Hati-hati jika nanti Anda memasang Laravel Scout: trait Searchable juga menyediakan method statis search(). Supaya tidak bentrok, beri nama scope yang berbeda, misalnya scopeKeyword.

Mencari di Tabel Relasi

Sering kali user mengetik nama kategori atau nama penulis, bukan judul. Untuk itu gunakan orWhereHas() di dalam grup yang sama:

Dapatkan Update Terbaru

Berlangganan gratis. Artikel & tutorial coding terbaru langsung ke email kamu. Tanpa spam.

$query->where(function ($q) use ($keyword) {
    $q->where('title', 'like', "%{$keyword}%")
      ->orWhere('content', 'like', "%{$keyword}%")
      ->orWhereHas('category', function ($c) use ($keyword) {
          $c->where('name', 'like', "%{$keyword}%");
      });
});

whereHas menghasilkan subquery EXISTS, jadi pastikan relasi category() sudah didefinisikan di model. Jika belum paham cara mendefinisikan relasi, pelajari dulu Eloquent Relationship di Laravel. Pada data besar, subquery seperti ini cukup berat, jadi pakai seperlunya.

Iklan

Mencari Beberapa Kata Sekaligus

Dengan satu LIKE, kata kunci "laravel pencarian" hanya cocok jika kedua kata muncul berurutan persis. Agar lebih fleksibel, pecah kata kunci lalu syaratkan setiap kata ada di judul atau konten:

$words = array_filter(explode(' ', $keyword));

foreach ($words as $word) {
    $query->where(function ($q) use ($word) {
        $q->where('title', 'like', "%{$word}%")
          ->orWhere('content', 'like', "%{$word}%");
    });
}

Setiap kata menjadi satu grup yang digabung dengan AND, sehingga hasilnya hanya artikel yang mengandung semua kata, di urutan mana pun. Batasi jumlah kata (misalnya maksimal 5) agar query tidak membengkak.

Menangani Karakter % dan _ di Kata Kunci

Di LIKE, tanda % berarti "karakter apa saja berapa pun panjangnya" dan _ berarti "tepat satu karakter apa saja". Kalau user mengetik %, semua baris akan cocok. Ini bukan celah SQL injection karena Eloquent memakai parameter binding, tapi hasilnya jadi tidak sesuai harapan. Escape dulu sebelum dipakai:

$safe = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $keyword);

$query->where('title', 'like', "%{$safe}%");

Di MySQL dan MariaDB, backslash adalah karakter escape bawaan untuk LIKE, jadi kode di atas langsung bekerja.

Iklan

Menyorot Kata Kunci di Hasil (Highlight)

Menebalkan kata yang dicari membantu user memindai hasil. Kuncinya: escape teks dulu, baru sisipkan tag <mark>, supaya tidak membuka celah XSS:

@php
    $title = e($post->title);
    if ($keyword !== '') {
        $pattern = '/(' . preg_quote(e($keyword), '/') . ')/iu';
        $title = preg_replace($pattern, '<mark>$1</mark>', $title);
    }
@endphp

<h3>{!! $title !!}</h3>

Jangan pernah menaruh $post->title mentah di dalam {!! !!}. Selalu lewati e() lebih dulu.

Tips Performa

  • Untuk tabel besar, LIKE %kata% bisa lambat karena tidak memakai index. Pertimbangkan fulltext index MySQL untuk pencarian teks.
  • Untuk pencarian skala besar, gunakan Laravel Scout dengan driver seperti Meilisearch atau Algolia.
  • Pola LIKE 'kata%' (tanpa % di depan) masih bisa memakai index B-tree biasa. Cocok untuk pencarian kode produk, SKU, atau nomor invoice yang diketik dari awal.
  • Jangan memakai select * jika kolom content sangat panjang dan tidak ditampilkan di daftar. Pilih kolom yang perlu saja dengan select().

Fulltext index di Laravel bisa dibuat langsung dari migration, lalu dipakai dengan whereFullText():

// migration
Schema::table('posts', function (Blueprint $table) {
    $table->fullText(['title', 'content']);
});

// controller
$posts = Post::whereFullText(['title', 'content'], $keyword)->paginate(10);

Kolom di whereFullText() harus sama dengan kolom index fulltext-nya. Perlu diingat, fulltext MySQL bekerja per kata, bukan potongan kata: mencari "lara" tidak akan menemukan "laravel". Kata yang sangat pendek juga diabaikan, karena InnoDB punya batas minimal panjang token (pengaturan innodb_ft_min_token_size).

Iklan

Untuk Laravel Scout, pasang paketnya dan tambahkan trait Searchable ke model:

composer require laravel/scout
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
use Laravel\Scout\Searchable;

class Post extends Model
{
    use Searchable;
}

// pemakaian
$posts = Post::search($request->q)->paginate(10);

Scout juga punya driver database (atur SCOUT_DRIVER=database di .env) yang tidak butuh server tambahan, cocok sebagai langkah awal sebelum pindah ke Meilisearch.

Memilih Metode Pencarian yang Tepat

MetodeCocok untukKelebihanKeterbatasan
LIKE %kata%Data kecil sampai menengah, panel adminPaling mudah, cocok potongan kataTidak memakai index, melambat di tabel besar
Fulltext indexArtikel atau konten teks panjangCepat, ada skor relevansiPer kata utuh, khusus MySQL/MariaDB/PostgreSQL
Laravel Scout + Meilisearch/AlgoliaToko online, data besar, butuh typo toleranceSangat cepat, fitur lengkapPerlu server/layanan tambahan dan sinkronisasi index

Kesalahan Umum dan Solusinya

  • Filter status ikut bocor. Penyebabnya orWhere tanpa grup. Solusi: bungkus dengan closure atau pakai whereAny().
  • Halaman 2 menampilkan semua data. Lupa withQueryString() pada paginator.
  • Pencarian tidak menemukan apa pun di PostgreSQL padahal datanya ada. LIKE di PostgreSQL membedakan huruf besar-kecil. Gunakan operator ilike. Di MySQL dengan collation _ci (bawaan Laravel), pencarian sudah tidak peka huruf besar-kecil.
  • Memakai whereRaw dengan variabel langsung. Contoh whereRaw("title like '%$keyword%'") rawan SQL injection. Jika butuh raw, pakai binding: whereRaw('title like ?', ["%{$keyword}%"]).
  • Halaman hasil lambat karena N+1 query. Tambahkan with() untuk relasi yang ditampilkan di daftar hasil.

Kalau Anda juga butuh dropdown kategori, rentang tanggal, atau urutan harga di samping kotak pencarian, polanya mirip dan dibahas di fitur filter data di Laravel.

FAQ

Apakah pencarian dengan LIKE di Laravel aman dari SQL injection?

Ya, selama Anda memakai method query builder seperti where('title', 'like', ...). Laravel mengirim nilainya sebagai parameter binding PDO, bukan menempelkannya ke string SQL. Risiko muncul ketika memakai whereRaw atau DB::select dengan variabel yang ditempel langsung.

Iklan

Kenapa hasil pencarian saya tidak peka huruf besar-kecil, atau justru sebaliknya?

Itu ditentukan oleh database, bukan Laravel. MySQL dan MariaDB dengan collation berakhiran _ci mengabaikan huruf besar-kecil, sedangkan LIKE di PostgreSQL peka huruf besar-kecil sehingga perlu ilike.

Kapan sebaiknya pindah dari LIKE ke fulltext atau Scout?

Saat query pencarian mulai terasa lambat di log atau monitoring, atau saat user butuh fitur seperti toleransi salah ketik dan urutan berdasarkan relevansi. Untuk blog kecil atau panel admin dengan beberapa ribu baris, LIKE biasanya masih memadai.

Bagaimana membuat pencarian live tanpa reload halaman?

Buat endpoint yang mengembalikan JSON (misalnya return response()->json($posts)), lalu panggil dengan fetch() dari JavaScript saat user mengetik. Beri jeda (debounce) sekitar beberapa ratus milidetik agar tidak mengirim request di setiap ketukan tombol. Alternatif lain adalah Livewire.

Apakah form pencarian perlu token CSRF?

Tidak, jika memakai method GET. Middleware CSRF Laravel hanya memeriksa request yang mengubah data seperti POST, PUT, PATCH, dan DELETE.

Iklan

Kesimpulan

Membuat fitur pencarian di Laravel cukup dengan where('kolom', 'like', "%kata%"), dibungkus grup kondisi bila digabung filter lain, dan withQueryString() agar kata kunci bertahan saat pagination. Tambahkan validasi panjang kata kunci, escape karakter wildcard, dan eager loading supaya hasilnya aman dan ringan. Untuk data yang sangat besar, tingkatkan ke fulltext index atau Laravel Scout. Untuk memahami query relasi, lihat panduan Eloquent Relationship kami yang ditautkan di atas.

Iklan
fitur pencarian laravel search laravel cara membuat search laravel laravel where like pencarian data laravel
Rekomendasi

Hosting Cepat untuk Website & Laravel

Butuh hosting yang ngebut dan stabil untuk deploy website atau aplikasi Laravel-mu? Ini rekomendasi yang saya pakai.

Lihat Rekomendasi Hosting
Yudhi
Ditulis oleh

Yudhi

Web Developer

Web developer yang sehari-hari berkutat dengan PHP, Laravel, JavaScript, dan MySQL. Terbiasa membangun aplikasi web dari nol โ€” merancang database, menulis fitur, memburu bug, hingga deploy ke server โ€” lalu menuangkan solusi dan tutorialnya di DhieCoderWeb agar lebih mudah diikuti developer lain.

Bagikan artikel
Kembali

Komentar (0)

Punya pertanyaan atau tambahan? Tulis di bawah โ€” tak perlu login.

Membalas komentarโ€ฆ batal

Belum ada komentar. Jadilah yang pertama!

๐Ÿš€ Partner Recommendation

Butuh Source Code & Aplikasi Premium?

Download aplikasi Laravel, POS, Sekolah, Klinik, ERP, dan source code siap pakai di GudangCode.

GudangCode
  • โœ” Source Code Premium
  • โœ” Sistem Siap Pakai
  • โœ” Lifetime Update
  • โœ” Membership Lifetime
  • โœ” Update Aplikasi Harian
Daftar Membership โ†’
Iklan
Iklan