Dalam Laravel Scout, method toSearchableArray() adalah titik paling penting untuk menentukan shape dokumen yang dikirim ke mesin pencari seperti Algolia, Meilisearch, Typesense, atau driver lain yang mendukung Scout. Banyak implementasi hanya mengekspor seluruh atribut model, padahal pendekatan itu sering menghasilkan index yang gemuk, relevansi yang buruk, dan sinkronisasi yang sulit dipelihara.
Artikel ini membahas cara menyusun dokumen index yang efisien melalui pemilihan field yang tepat, normalisasi data, casting angka dan boolean, penggabungan relasi secara terukur, serta strategi versioning schema index saat struktur data berubah. Fokus utamanya bukan sekadar agar data “masuk ke index”, tetapi agar pencarian cepat, akurat, dan tetap mudah dirawat dalam jangka panjang.
Mengapa Desain Index Sangat Penting di Laravel Scout
Search engine bekerja berdasarkan dokumen yang Anda kirim, bukan langsung dari struktur tabel relasional aplikasi. Karena itu, kualitas hasil pencarian sangat dipengaruhi oleh bagaimana Anda membentuk dokumen index.
Jika Anda memasukkan terlalu banyak field:
- ukuran index membesar,
- proses indexing dan reindexing menjadi lebih lambat,
- relevansi hasil dapat terganggu karena banyak kata yang sebenarnya tidak penting ikut dicari,
- sinkronisasi data antar model dan relasi menjadi lebih kompleks.
Sebaliknya, jika field yang dimasukkan terlalu sedikit atau salah bentuk:
- hasil pencarian menjadi miskin konteks,
- fitur filter/sort sulit dilakukan,
- dokumen perlu transformasi tambahan di sisi aplikasi.
Prinsip dasarnya adalah: index hanya data yang benar-benar dibutuhkan untuk pencarian, filtering, sorting, dan tampilan ringkas hasil.
Prinsip Dasar Menyusun toSearchableArray
1. Pilih field berdasarkan kebutuhan pencarian nyata
Jangan mulai dari “apa saja field yang ada di model”, tetapi dari “fitur pencarian apa yang dibutuhkan pengguna”. Misalnya untuk artikel, biasanya pengguna mencari berdasarkan judul, ringkasan, nama penulis, kategori, tag, dan status publish. Mereka jarang perlu mencari berdasarkan kolom internal seperti created_by_ip, updated_by, atau payload metadata mentah.
2. Simpan field terstruktur untuk filter dan sort
Selain field teks untuk pencarian penuh, simpan juga field terstruktur seperti:
statusuntuk filter,published_at_timestampuntuk sorting,category_idataucategory_sluguntuk facet/filter,is_featuredsebagai boolean asli, bukan string.
Jangan paksa semua kebutuhan ke satu field teks besar.
3. Normalisasi data sebelum diindex
Normalisasi membantu konsistensi hasil pencarian. Contohnya:
- hapus HTML dari body artikel,
- ringkas whitespace berlebih,
- pastikan string kosong menjadi
nullbila lebih tepat, - ubah nilai numerik ke integer/float,
- ubah flag ke boolean murni.
Normalisasi ini penting karena banyak driver search mengandalkan tipe data yang konsisten untuk filter, ranking, dan sorting.
4. Gabungkan relasi secukupnya
Menggabungkan data relasi seperti author, category, atau tags sangat berguna, tetapi jangan membawa seluruh objek relasi ke index. Ambil hanya bagian yang dipakai untuk pencarian atau tampilan hasil ringkas.
5. Hindari field berlebihan dan field yang sering berubah tanpa nilai pencarian
Setiap field tambahan berarti ukuran dokumen lebih besar dan update index lebih sering. Kolom seperti view_count atau last_seen_at mungkin berubah sangat sering. Jika tidak benar-benar dipakai sebagai filter atau ranking, lebih baik jangan dimasukkan.
Contoh Implementasi: Model Article
Studi kasus yang umum adalah pencarian artikel dengan author, tag, kategori, dan status publish. Di sini, kita ingin hasil relevan sekaligus mendukung filter yang praktis.
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use Illuminate\Support\Str;
class Article extends Model
{
use Searchable;
public function author()
{
return $this->belongsTo(User::class, 'author_id');
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
public function toSearchableArray(): array
{
$this->loadMissing(['author:id,name,username', 'category:id,name,slug', 'tags:id,name,slug']);
return [
'id' => (string) $this->getKey(),
'schema_version' => 2,
'title' => trim($this->title),
'slug' => $this->slug,
'excerpt' => Str::limit(trim(strip_tags((string) $this->excerpt)), 280, ''),
'content_plain' => Str::limit(
preg_replace('/\s+/', ' ', trim(strip_tags((string) $this->content))),
5000,
''
),
'status' => $this->status,
'is_published' => (bool) ($this->status === 'published'),
'published_at' => optional($this->published_at)?->toAtomString(),
'published_at_timestamp' => optional($this->published_at)?->timestamp,
'author' => [
'id' => (string) optional($this->author)->id,
'name' => optional($this->author)->name,
'username' => optional($this->author)->username,
],
'author_name' => optional($this->author)->name,
'category' => [
'id' => (string) optional($this->category)->id,
'name' => optional($this->category)->name,
'slug' => optional($this->category)->slug,
],
'category_name' => optional($this->category)->name,
'category_slug' => optional($this->category)->slug,
'tags' => $this->tags->map(fn ($tag) => [
'id' => (string) $tag->id,
'name' => $tag->name,
'slug' => $tag->slug,
])->values()->all(),
'tag_names' => $this->tags->pluck('name')->values()->all(),
];
}
}Ada beberapa keputusan penting pada contoh di atas:
content_plaindisimpan dalam bentuk teks bersih, bukan HTML mentah.is_publishedadalah boolean asli agar filter lebih konsisten.published_at_timestampdisimpan sebagai angka agar mudah dipakai untuk sorting.- Relasi author, category, dan tags diringkas hanya ke field yang dibutuhkan.
tag_namesditambahkan sebagai array sederhana karena banyak mesin pencari lebih mudah bekerja dengan field array datar untuk pencarian/facet.
Jika hasil pencarian hanya menampilkan judul, excerpt, author, kategori, dan tanggal publish, jangan kirim body artikel penuh tanpa alasan. Simpan versi ringkas atau terpotong bila memang cukup untuk relevansi.
Kapan artikel tidak perlu diindex?
Sering kali tidak semua record layak masuk index. Artikel draft, soft-deleted, atau unpublished mungkin sebaiknya tidak ikut.
public function shouldBeSearchable(): bool
{
return $this->status === 'published' && is_null($this->deleted_at);
}Pendekatan ini membantu menjaga index tetap bersih dan relevan.
Contoh Implementasi: Model Product
Pada produk, kebutuhan pencarian biasanya menekankan kombinasi teks dan data terstruktur: nama, SKU, brand, kategori, harga, stok, dan status aktif.
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Product extends Model
{
use Searchable;
public function brand()
{
return $this->belongsTo(Brand::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function toSearchableArray(): array
{
$this->loadMissing(['brand:id,name,slug', 'category:id,name,slug']);
return [
'id' => (string) $this->getKey(),
'schema_version' => 1,
'name' => trim($this->name),
'sku' => strtoupper(trim((string) $this->sku)),
'description' => trim(strip_tags((string) $this->short_description)),
'brand_name' => optional($this->brand)->name,
'brand_slug' => optional($this->brand)->slug,
'category_name' => optional($this->category)->name,
'category_slug' => optional($this->category)->slug,
'price' => (float) $this->price,
'stock' => (int) $this->stock,
'is_active' => (bool) $this->is_active,
'is_in_stock' => (bool) ($this->stock > 0),
];
}
}Di domain e-commerce, kesalahan umum adalah menyimpan harga sebagai string seperti "199000" atau bahkan "Rp 199.000". Format seperti itu buruk untuk sorting dan filter rentang harga. Simpan angka mentah, lalu format tampilan di layer presentasi.
Contoh Implementasi: Model User
Tidak semua model membutuhkan dokumen yang kompleks. Untuk pencarian user internal, index yang sederhana sering kali lebih baik.
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class User extends Model
{
use Searchable;
public function toSearchableArray(): array
{
return [
'id' => (string) $this->getKey(),
'schema_version' => 1,
'name' => trim($this->name),
'username' => strtolower(trim((string) $this->username)),
'email' => strtolower(trim((string) $this->email)),
'job_title' => trim((string) $this->job_title),
'department' => trim((string) $this->department),
'is_active' => (bool) $this->is_active,
];
}
}Perhatikan bahwa field sensitif seperti password hash, token, alamat lengkap, atau catatan internal tidak ikut diindex. Search index bukan tempat untuk menyalin semua data model.
Normalisasi Data: Detail yang Sering Diabaikan
Membersihkan HTML dan whitespace
Artikel atau deskripsi produk sering berasal dari editor WYSIWYG. Jika HTML mentah diindex, token pencarian dapat tercampur dengan tag yang tidak relevan. Gunakan strip_tags() dan rapikan whitespace dengan regex sederhana.
Menyeragamkan huruf besar-kecil untuk identifier tertentu
Untuk field seperti username, email, atau sku, normalisasi huruf besar-kecil membantu konsistensi. Namun, jangan memaksa semua field teks bebas ke lowercase jika search engine sudah menangani case-insensitivity dengan baik. Gunakan normalisasi hanya bila memang berguna.
Pastikan tipe data stabil
Jangan campur nilai integer dan string pada field yang sama. Misalnya hari ini stock adalah integer, besok jadi string kosong. Inkonstistensi seperti ini sering memicu masalah filter atau index mapping di engine tertentu.
Studi Kasus Nyata: Pencarian Artikel dengan Author, Tag, Kategori, dan Status Publish
Bayangkan kebutuhan aplikasi media atau blog perusahaan:
- pengguna mencari artikel berdasarkan judul, isi ringkas, nama author, kategori, dan tag,
- hanya artikel berstatus
publishedyang muncul, - hasil dapat difilter berdasarkan kategori atau tag,
- artikel terbaru mendapat prioritas dalam pengurutan sekunder.
Untuk kebutuhan seperti ini, dokumen index sebaiknya memuat:
- Field pencarian utama:
title,excerpt,content_plain,author_name,tag_names,category_name. - Field filter:
status,is_published,category_slug, mungkintag_namesatau tag slug. - Field sort:
published_at_timestamp. - Field tampilan ringkas:
slug,author,category.
Kesalahan umum pada studi kasus ini adalah mengirim seluruh relasi tag lengkap beserta timestamp pivot, deskripsi kategori panjang, avatar author resolusi tinggi, atau body artikel penuh tanpa batasan. Semua itu memperbesar dokumen tetapi belum tentu menambah kualitas hasil.
Jika Anda perlu menampilkan detail lengkap setelah user membuka halaman artikel, ambil dari database utama berdasarkan ID atau slug. Search index sebaiknya dipakai sebagai alat pencarian, bukan replika penuh database relasional.
Dampak Desain Index terhadap Performa, Ukuran, Akurasi, dan Maintainability
Performa indexing dan query
Dokumen yang lebih kecil umumnya lebih cepat dikirim, diproses, dan diperbarui. Ini penting saat ada banyak event model atau proses reindex massal. Relasi yang di-load berlebihan juga bisa memicu masalah N+1 atau konsumsi memori saat impor besar.
Ukuran index
Semakin banyak field teks panjang yang Anda simpan, semakin besar index. Ukuran ini berdampak pada biaya penyimpanan, waktu sinkronisasi, dan kecepatan rebuild index. Ringkas teks besar seperlunya.
Akurasi hasil
Relevansi tidak selalu meningkat jika data lebih banyak. Kadang justru menurun karena token dari field yang kurang penting ikut bersaing. Field yang fokus dan bersih cenderung menghasilkan ranking yang lebih masuk akal.
Maintainability
Dokumen index yang eksplisit lebih mudah dipahami dibanding return $this->toArray();. Saat tim perlu menambah filter baru atau mengubah perilaku pencarian, mereka tahu persis field mana yang tersedia dan mengapa field itu ada.
Schema Versioning untuk Perubahan Struktur Index
Ketika struktur dokumen berubah, misalnya mengganti category dari string menjadi objek, menambah field tag_slugs, atau menghapus content_plain, Anda perlu pendekatan yang aman. Salah satu pola paling sederhana adalah menambahkan schema_version ke dokumen.
public function searchableAs(): string
{
return 'articles_v2';
}
public function toSearchableArray(): array
{
return [
'id' => (string) $this->getKey(),
'schema_version' => 2,
'title' => $this->title,
// field lain...
];
}Ada dua pola umum:
- Version di nama index, misalnya
articles_v1,articles_v2. Ini cocok jika perubahan mapping besar dan Anda ingin migrasi bertahap. - Version di field dokumen, misalnya
schema_version. Ini berguna untuk debugging, audit, atau kompatibilitas sementara.
Untuk perubahan besar, praktik yang aman biasanya:
- buat index baru,
- ubah
searchableAs()atau konfigurasi target index, - jalankan reimport penuh,
- arahkan query aplikasi ke index baru setelah validasi selesai,
- hapus index lama jika sudah tidak dipakai.
Pendekatan ini mengurangi risiko dokumen lama bercampur dengan format baru.
Kesalahan Umum dan Tips Debugging
Kesalahan umum
- Menggunakan
toArray()mentah sebagai dokumen index. - Tidak melakukan eager loading relasi saat membentuk dokumen, sehingga impor menjadi lambat.
- Menyimpan angka sebagai string dan boolean sebagai
"0"/"1". - Mengindex field sensitif yang tidak seharusnya keluar dari database utama.
- Membiarkan dokumen terlalu besar karena memasukkan semua relasi dan teks panjang.
Tips debugging
- Log hasil
toSearchableArray()untuk beberapa record contoh dan pastikan tipenya konsisten. - Uji record dengan relasi kosong: author null, category null, tag kosong.
- Bandingkan hasil pencarian sebelum dan sesudah menambah field baru. Kadang field tambahan menurunkan relevansi.
- Pastikan model yang berubah memang di-sync ulang ke index, terutama jika perubahan berasal dari relasi.
Penutup
toSearchableArray() bukan sekadar method teknis untuk mengirim data ke Laravel Scout. Ia adalah tempat Anda merancang kontrak antara model aplikasi dan mesin pencari. Desain index yang baik berarti memilih field yang tepat, menjaga tipe data tetap konsisten, merangkum relasi secara efisien, dan menghindari field yang tidak memberi nilai nyata pada pencarian.
Untuk kasus artikel, produk, dan user, pola yang sama selalu berlaku: index secukupnya, strukturkan dengan jelas, dan pikirkan kebutuhan filter, sort, serta relevansi sejak awal. Saat skema berubah, gunakan versioning index agar migrasi lebih aman. Dengan pendekatan ini, Laravel Scout tidak hanya berfungsi, tetapi juga tetap cepat, akurat, dan mudah dipelihara saat aplikasi berkembang.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!