Laravel Scout mempermudah integrasi full-text search ke aplikasi Laravel, tetapi untuk sistem produksi, tantangan utamanya bukan sekadar “membuat search berjalan”. Tantangan yang lebih penting adalah relevansi hasil, konsistensi data saat indexing, biaya operasional, dan kemampuan melakukan perubahan schema/index settings tanpa downtime. Pada artikel ini, kita akan membahas pendekatan yang lebih matang untuk menggabungkan Laravel Scout dengan Algolia pada kasus nyata katalog produk dengan filter kategori, harga, dan stok.
Contoh yang dibahas mengasumsikan kita memiliki model Product dengan atribut seperti nama, SKU, brand, kategori, harga, stok, status publikasi, dan tenant/store. Fokus artikel bukan pada instalasi dasar, melainkan pada keputusan desain yang memengaruhi kualitas search di produksi.
Arsitektur Dasar dan Prinsip Desain Index
Pada katalog produk, Algolia sebaiknya diperlakukan sebagai search-optimized read model, bukan sebagai sumber data utama. Database relasional tetap menjadi source of truth, sedangkan index Algolia berisi representasi data yang sudah diringkas dan dioptimalkan untuk query pencarian.
Prinsip pentingnya:
- Simpan hanya field yang diperlukan untuk search, ranking, dan filtering.
- Flatten data dari relasi Eloquent jika memang dipakai dalam pencarian.
- Hindari index field yang sensitif atau tidak dibutuhkan oleh klien.
- Pisahkan concern ranking dan filtering: field untuk relevansi belum tentu cocok untuk faceting.
Untuk katalog produk, representasi record yang umum di Algolia dapat mencakup:
idnameskubrandcategory_namesdescriptionringkaspricein_stockstock_qtyis_activetenant_idpopularity_scorecreated_at_timestamp
Field-field ini lalu dipakai untuk mengatur searchable attributes, facet, ranking, dan sorting.
Integrasi Eloquent Model yang Siap Produksi
Mendefinisikan Model Scout
Contoh model Product berikut menunjukkan penggunaan Scout dengan kontrol yang lebih realistis.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laravel\Scout\Searchable;
class Product extends Model
{
use Searchable, SoftDeletes;
protected $fillable = [
'tenant_id',
'name',
'sku',
'brand',
'description',
'price',
'stock_qty',
'is_active',
'category_id',
'popularity_score',
];
public function category()
{
return $this->belongsTo(Category::class);
}
public function searchableAs(): string
{
return config('scout.prefix').'products';
}
public function shouldBeSearchable(): bool
{
return $this->is_active && !is_null($this->name);
}
public function toSearchableArray(): array
{
$this->loadMissing('category');
return [
'objectID' => (string) $this->getKey(),
'id' => $this->getKey(),
'tenant_id' => $this->tenant_id,
'name' => $this->name,
'sku' => $this->sku,
'brand' => $this->brand,
'description' => strip_tags((string) $this->description),
'category_name' => optional($this->category)->name,
'price' => (float) $this->price,
'in_stock' => $this->stock_qty > 0,
'stock_qty' => (int) $this->stock_qty,
'is_active' => (bool) $this->is_active,
'popularity_score' => (int) ($this->popularity_score ?? 0),
'created_at_timestamp' => optional($this->created_at)?->timestamp,
'__soft_deleted' => $this->trashed() ? 1 : 0,
];
}
}Beberapa catatan penting:
shouldBeSearchable()berguna untuk conditional indexing, misalnya hanya mengindeks produk aktif.toSearchableArray()sebaiknya mengembalikan tipe data yang eksplisit: integer, boolean, float. Ini penting untuk filter dan ranking numerik.objectIDdibuat stabil dan unik. Biasanya sama dengan primary key jika satu index hanya berisi satu entitas.
Soft Delete
Soft delete perlu dipikirkan secara eksplisit. Ada dua pendekatan umum:
- Hapus record dari index saat model di-soft-delete.
- Simpan record dengan flag seperti
__soft_deletedlalu filter di query.
Untuk kebanyakan katalog publik, pendekatan pertama lebih sederhana: produk terhapus tidak muncul sama sekali. Namun pada admin search, pendekatan kedua kadang berguna agar data yang dihapus tetap bisa dicari oleh staf internal.
Kesalahan umum: record sudah di-soft-delete di database, tetapi masih muncul di Algolia karena event sinkronisasi tidak berjalan atau queue indexing tertunda.
Multi-Tenant Scoping
Pada aplikasi multi-tenant, ada dua pola utama:
- Satu index global dengan field
tenant_idlalu query memakai filter. - Satu index per tenant.
Untuk jumlah tenant besar, satu index global biasanya lebih mudah dikelola. Contoh query dengan scoping tenant:
use App\Models\Product;
$results = Product::search('sepatu running', function ($algolia, $query, $options) use ($tenantId) {
$options['filters'] = 'tenant_id:'.$tenantId.' AND is_active:true AND __soft_deleted:0';
return $algolia->search($query, $options);
})->get();Pendekatan ini lebih hemat operasional dibanding membuat banyak index, tetapi Anda harus disiplin agar setiap query selalu menerapkan filter tenant. Jika ada risiko kebocoran data antar tenant, desain per-tenant index bisa lebih aman meski kompleksitas manajemennya lebih tinggi.
Desain Index Settings untuk Relevansi dan Filtering
searchableAttributes
searchableAttributes menentukan field mana yang dicari dan urutan prioritasnya. Untuk katalog produk, urutan biasanya sangat berpengaruh:
searchableAttributes: [
'unordered(name)',
'unordered(brand)',
'unordered(category_name)',
'sku',
'description'
]Alasannya:
- Nama produk umumnya sinyal utama relevansi.
- Brand dan kategori penting tetapi di bawah nama.
- SKU harus bisa dicari secara presisi, terutama untuk user internal atau pelanggan yang mencari kode barang.
- Description diletakkan belakangan agar tidak terlalu “berisik”.
Penggunaan unordered(...) mengurangi penalti urutan kata. Ini berguna untuk query seperti “running nike shoes” vs “nike running shoes”.
attributesForFaceting
Field untuk filter harus dimasukkan ke attributesForFaceting. Untuk kasus kita:
attributesForFaceting: [
'filterOnly(tenant_id)',
'filterOnly(is_active)',
'filterOnly(__soft_deleted)',
'searchable(category_name)',
'filterOnly(in_stock)'
]Beberapa hal penting:
filterOnlycocok untuk field yang hanya dipakai sebagai filter, sehingga overhead index lebih rendah.searchable(category_name)berguna jika daftar kategori panjang dan user perlu mencari facet tertentu.- Harga numerik biasanya tidak perlu masuk facet string; cukup gunakan numeric filtering pada field
price.
Contoh filter kategori, harga, dan stok:
$results = Product::search('laptop', function ($algolia, $query, $options) use ($tenantId) {
$options['filters'] = 'tenant_id:'.$tenantId.' AND is_active:true AND __soft_deleted:0 AND in_stock:true';
$options['numericFilters'] = ['price >= 5000000', 'price <= 15000000'];
return $algolia->search($query, $options);
})->get();customRanking
Setelah text relevance, Anda biasanya ingin hasil diurutkan lagi menggunakan sinyal bisnis. Contoh:
customRanking: [
'desc(in_stock)',
'desc(popularity_score)',
'asc(price)'
]Interpretasinya:
- Produk yang tersedia lebih diprioritaskan.
- Produk populer lebih tinggi.
- Jika faktor lain setara, harga lebih murah bisa diutamakan.
Trade-off-nya adalah jangan terlalu agresif memasukkan sinyal bisnis sampai mengorbankan relevansi teks. Jika user mencari SKU spesifik, hasil exact match harus tetap di atas produk populer yang tidak relevan.
Typo Tolerance dan Synonyms
Typo tolerance membantu query yang salah ketik, misalnya “samasung” untuk “samsung”. Ini sangat berguna untuk user publik, tetapi ada pengecualian:
- Untuk SKU atau kode produk, typo tolerance kadang justru menimbulkan false positive.
- Untuk katalog kecil dengan nama produk mirip, typo tolerance yang terlalu longgar dapat membuat hasil terasa tidak akurat.
Synonyms berguna untuk istilah domain, misalnya “hp” dan “handphone”, atau “tv” dan “televisi”. Pastikan sinonim didasarkan pada data query nyata, bukan asumsi. Sinonim yang berlebihan dapat menurunkan presisi.
Jangan menambahkan sinonim massal tanpa pengujian. Sinonim adalah alat relevansi yang kuat, tetapi salah konfigurasi bisa membuat hasil search terasa acak.
distinct
Jika satu produk memiliki banyak varian dan setiap varian diindex sebagai record terpisah, distinct bisa dipakai agar hasil yang tampil hanya satu per grup produk.
Misalnya Anda menambahkan product_group_id ke setiap record, lalu mengaktifkan distinct berdasarkan field itu. Ini cocok jika tujuan UI adalah menampilkan produk utama, bukan semua variasi ukuran/warna sekaligus. Namun jika user memang perlu mencari varian spesifik, distinct bisa menyembunyikan hasil yang seharusnya terlihat.
Replicas untuk Sorting
Sorting seperti “harga termurah”, “harga tertinggi”, atau “produk terbaru” sebaiknya menggunakan replica indices, bukan mencoba memaksakan satu ranking untuk semua kebutuhan.
Contoh pola replica:
productsuntuk ranking default berbasis relevansiproducts_price_ascproducts_price_descproducts_newest
Setiap replica mewarisi data utama, tetapi memiliki ranking berbeda. Keuntungannya adalah sorting tetap efisien dan konsisten dengan model Algolia. Kekurangannya: ada tambahan biaya index/settings dan kompleksitas pengelolaan.
Queue Indexing, Sinkronisasi Data, dan Deploy
Mengaktifkan Queue untuk Indexing
Pada aplikasi produksi, indexing sebaiknya tidak dijalankan sinkron dalam request web. Gunakan queue agar update index diproses di background.
// config/scout.php
'queue' => true,Manfaatnya:
- Request create/update produk lebih cepat.
- Retry lebih mudah jika Algolia API gagal sementara.
- Beban indexing dapat dipisah ke worker khusus.
Tetapi ada trade-off: hasil search menjadi eventually consistent. Artinya perubahan data bisa muncul beberapa detik lebih lambat dari database.
Praktik yang baik:
- Gunakan queue worker yang dipantau.
- Buat alert jika backlog indexing meningkat.
- Pastikan update massal tidak mengganggu job lain yang kritikal.
Common Mistake: N+1 Saat Import
Saat menjalankan import penuh, toSearchableArray() yang memanggil relasi tanpa eager loading dapat menyebabkan N+1 query. Solusinya, override query yang dipakai untuk import atau pastikan relasi penting di-load lebih dulu sebelum chunk indexing berjalan.
Strategi Zero-Downtime Reindex
Perubahan pada searchableAttributes, ranking, struktur record, atau penambahan field facet sering kali membutuhkan reindex penuh. Jika dilakukan sembarangan, user bisa menerima hasil tidak konsisten atau bahkan index kosong sementara.
Pola yang aman adalah blue-green indexing:
- Buat index baru, misalnya
products_v2. - Terapkan settings lengkap pada index baru.
- Import seluruh data ke
products_v2. - Sinkronkan perubahan data yang terjadi selama proses import.
- Alihkan aplikasi agar membaca index baru, idealnya melalui alias atau konfigurasi terpusat.
- Setelah stabil, hapus index lama bila sudah tidak diperlukan.
Sinkronisasi Data Saat Reindex Berjalan
Masalah terbesar reindex bukan import awal, melainkan delta data yang berubah saat proses berlangsung. Beberapa strategi:
- Dual write sementara: update produk dikirim ke index lama dan baru selama masa transisi.
- Catat changed IDs: setiap perubahan produk masuk ke tabel/log, lalu setelah import selesai lakukan replay ke index baru.
- Freeze write untuk window singkat, hanya cocok untuk sistem kecil.
Untuk sistem produksi aktif, dual write biasanya paling aman walaupun implementasinya lebih rumit. Intinya, jangan anggap import penuh cukup jika aplikasi terus menerima perubahan data.
Pergantian Index dengan Risiko Minimal
Jika Anda memakai nama index langsung di model melalui searchableAs(), penggantian index bisa dilakukan lewat konfigurasi yang dibaca dari environment atau service terpusat. Dengan begitu, deploy aplikasi cukup mengganti nama index aktif tanpa memodifikasi banyak kode.
Uji skenario rollback. Jika relevansi di index baru ternyata buruk, Anda harus bisa cepat kembali ke index lama.
Mengukur Relevansi dan Debugging Hasil Pencarian
Apa yang Harus Diukur
Relevansi tidak cukup dinilai dari “kelihatannya bagus”. Beberapa sinyal yang layak dipantau:
- CTR hasil pencarian.
- Conversion after search.
- Zero-result rate.
- Refinement rate, misalnya user terus mengganti query atau filter.
- Top queries dengan complaint dari support atau tim bisnis.
Idealnya, Anda mengumpulkan sampel query nyata dan membuat evaluasi manual untuk query-query penting: nama brand, SKU, typo umum, istilah sinonim, dan query kategori.
Mendiagnosis Hasil yang Meleset
Jika hasil pencarian terasa salah, telusuri secara sistematis:
- Cek record di index: apakah field yang diharapkan benar-benar ada dan up to date?
- Cek settings index: apakah field masuk
searchableAttributesatauattributesForFacetingsesuai kebutuhan? - Cek filter runtime: apakah query aplikasi menambahkan filter yang terlalu ketat?
- Cek typo tolerance dan synonyms: apakah hasil melebar terlalu jauh?
- Cek custom ranking: apakah sinyal bisnis menenggelamkan exact match?
Kasus yang sering terjadi pada katalog produk:
- User mencari SKU, tetapi produk lain muncul lebih atas karena popularity score terlalu dominan.
- Produk out-of-stock tetap muncul teratas karena ranking text bagus, tetapi tidak ada penalti ranking stok.
- Filter tenant atau status aktif terlupa diterapkan di salah satu endpoint.
- Data harga di index tidak bertipe numerik sehingga numeric filter tidak bekerja seperti yang diharapkan.
Praktik Debug yang Efektif
Buat query diagnostik yang merekam:
- query string
- filters dan numericFilters
- nama index/replica yang dipakai
- ID hasil teratas
Dengan log ini, Anda bisa mereproduksi anomali dengan jauh lebih cepat dibanding menebak-nebak dari UI.
Trade-off Biaya dan Performa
Algolia sangat cepat untuk pencarian dan filtering, tetapi ada trade-off yang perlu dipahami sejak awal:
- Semakin banyak field, replica, synonym, dan faceting, semakin besar kompleksitas dan biaya.
- Reindex penuh pada katalog besar bisa memakan waktu dan resource operasional yang signifikan.
- One global index lebih hemat daripada per-tenant index, tetapi butuh disiplin filtering.
- Queue indexing meningkatkan performa aplikasi, tetapi menambah eventual consistency dan kebutuhan observability.
Strategi efisien biasanya adalah:
- Index hanya field yang benar-benar dipakai.
- Gunakan replica hanya untuk sorting yang memang dibutuhkan UI.
- Tinjau ulang synonyms secara berkala.
- Pisahkan use case publik dan admin jika kebutuhan relevansinya berbeda jauh.
Penutup
Menggunakan Laravel Scout dengan Algolia di produksi bukan hanya soal menghubungkan model ke mesin pencarian. Kualitas implementasi sangat ditentukan oleh desain index, disiplin filtering, strategi ranking, cara Anda mengelola reindex, dan kemampuan mengukur relevansi dari data nyata.
Untuk katalog produk, kombinasi yang biasanya sehat adalah: searchableAttributes yang ketat, faceting yang seperlunya, customRanking yang mendukung tujuan bisnis tanpa merusak exact match, queue indexing, dan strategi zero-downtime reindex berbasis index baru + sinkronisasi delta. Dengan pendekatan ini, search tidak hanya cepat, tetapi juga lebih dapat diprediksi, lebih aman saat deploy, dan lebih mudah di-debug ketika hasil mulai meleset.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!