Pencarian yang terasa cepat belum tentu relevan, dan pencarian yang relevan sering kali gagal jika arsitektur indeks, query, dan pengalaman frontend tidak dirancang dengan benar. Pada aplikasi Nuxt 3, Elasticsearch sering dipilih ketika kebutuhan pencarian sudah melewati batas LIKE SQL atau full-text search bawaan database relasional, terutama untuk katalog produk besar, dokumentasi, artikel, atau knowledge base.
Artikel ini membahas cara membangun fitur pencarian advance dengan Nuxt 3 dan Elasticsearch secara praktis: mulai dari proses indexing dari backend, desain mapping, analyzer Bahasa Indonesia, autocomplete, typo tolerance, boosting, faceted search, pagination efisien, hingga implementasi endpoint pencarian dan composable frontend dengan debounce dan loading state. Di bagian akhir, kita juga akan membahas trade-off Elasticsearch dibanding SQL full-text.
Arsitektur Dasar: Pisahkan Sumber Data dan Search Index
Pola yang umum dan aman adalah menjadikan database utama, misalnya PostgreSQL atau MySQL, sebagai source of truth, sedangkan Elasticsearch dipakai sebagai search index. Data ditulis ke database utama terlebih dahulu, lalu disinkronkan ke Elasticsearch secara sinkron atau asinkron.
Alur yang direkomendasikan
- Aplikasi backend menerima perubahan data produk atau dokumen.
- Data disimpan ke database utama.
- Event atau job antrean dipicu untuk melakukan upsert dokumen ke Elasticsearch.
- Endpoint search membaca langsung dari Elasticsearch.
Pendekatan ini penting karena Elasticsearch bukan pengganti database transaksi. Ia unggul untuk indexing dan retrieval teks, tetapi tidak dirancang sebagai tempat utama untuk relasi kompleks, transaksi ketat, atau konsistensi absolut per operasi.
Catatan: Untuk katalog produk, indeks pencarian sebaiknya menyimpan field yang memang dibutuhkan untuk ranking dan rendering hasil. Jangan memindahkan seluruh model mentah jika tidak diperlukan.
Desain Index, Mapping, dan Analyzer Bahasa Indonesia
Hasil pencarian yang relevan dimulai dari mapping yang tepat. Kesalahan umum adalah membiarkan Elasticsearch menebak tipe field secara dinamis. Untuk sistem pencarian serius, definisikan mapping secara eksplisit agar perilaku pencarian stabil.
Contoh struktur dokumen
Untuk katalog produk, sebuah dokumen bisa berisi:
- id
- title
- description
- category
- brand
- tags
- price
- status
- popularity
- created_at
Mapping dan analyzer
Untuk Bahasa Indonesia, kita perlu memperhatikan stemming, stopwords, dan normalisasi huruf. Elasticsearch menyediakan analyzer bahasa tertentu di beberapa distribusi, tetapi dalam praktiknya Anda sering tetap perlu analyzer kustom agar perilakunya lebih terkontrol.
PUT /products_v1
{
"settings": {
"analysis": {
"filter": {
"id_stop": {
"type": "stop",
"stopwords": "_indonesian_"
},
"id_stemmer": {
"type": "stemmer",
"language": "indonesian"
},
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 20
}
},
"analyzer": {
"id_text_analyzer": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "id_stop", "id_stemmer"]
},
"id_autocomplete_index": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "autocomplete_filter"]
},
"id_autocomplete_search": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding"]
}
}
}
},
"mappings": {
"properties": {
"id": { "type": "keyword" },
"title": {
"type": "text",
"analyzer": "id_text_analyzer",
"fields": {
"keyword": { "type": "keyword" },
"autocomplete": {
"type": "text",
"analyzer": "id_autocomplete_index",
"search_analyzer": "id_autocomplete_search"
}
}
},
"description": {
"type": "text",
"analyzer": "id_text_analyzer"
},
"category": {
"type": "keyword"
},
"brand": {
"type": "keyword"
},
"tags": {
"type": "keyword"
},
"status": {
"type": "keyword"
},
"price": {
"type": "scaled_float",
"scaling_factor": 100
},
"popularity": {
"type": "float"
},
"created_at": {
"type": "date"
}
}
}
}Mengapa beberapa field memakai text dan yang lain keyword? Field text dianalisis untuk full-text search, sedangkan keyword cocok untuk filter, agregasi facet, dan sorting exact value. Untuk field seperti category, brand, dan status, keyword hampir selalu lebih tepat.
Kesalahan umum pada analyzer Indonesia
- Menganggap stemming selalu meningkatkan kualitas hasil. Pada beberapa istilah produk, stemming justru mengaburkan kata penting.
- Menggunakan stopwords terlalu agresif sehingga istilah pendek menjadi hilang.
- Tidak menguji query nyata dari pengguna, misalnya singkatan, salah ketik, atau campuran bahasa Inggris-Indonesia.
Karena itu, analyzer perlu diuji memakai data dan query aktual, bukan hanya teori linguistik.
Indexing Data dari Backend
Indexing sebaiknya dilakukan dari backend, bukan langsung dari frontend. Tujuannya adalah menjaga keamanan, konsistensi, dan kontrol terhadap bentuk dokumen yang masuk ke indeks.
Contoh endpoint backend untuk indexing
Misalnya backend menyediakan endpoint internal atau job worker yang melakukan sinkronisasi dokumen ke Elasticsearch:
POST /internal/search/index-product
Content-Type: application/json
Authorization: Bearer <internal-token>
{
"id": "prd_1001",
"title": "Sepatu Lari Pria Ringan",
"description": "Sepatu lari untuk latihan harian dengan bantalan empuk dan bobot ringan.",
"category": "sepatu",
"brand": "fastrun",
"tags": ["lari", "pria", "sport"],
"price": 799000,
"status": "active",
"popularity": 8.7,
"created_at": "2026-03-01T10:00:00Z"
}Di sisi implementasi, backend lalu melakukan upsert ke indeks. Jika volume perubahan tinggi, gunakan antrean agar request utama tidak menunggu Elasticsearch.
Strategi sinkronisasi
- Synchronous indexing: sederhana, tetapi menambah latensi saat write.
- Asynchronous indexing: lebih tahan skala, tetapi ada jeda konsistensi antara database dan indeks.
- Bulk indexing: penting untuk reindex data besar atau migrasi mapping.
Untuk perubahan mapping besar, praktik terbaik adalah membuat indeks baru, melakukan reindex, lalu mengganti alias indeks. Ini mengurangi downtime dan memudahkan rollback.
Membangun Query Search yang Relevan
Relevansi tidak datang dari satu query tunggal yang “ajaib”. Biasanya Anda perlu menggabungkan beberapa teknik: exact match, prefix match, typo tolerance, boosting, filter, dan fungsi ranking tambahan.
Endpoint pencarian
GET /api/search?q=sepatu%20lari&category=sepatu&brand=fastrun&page=1&size=12&sort=relevanceEndpoint ini sebaiknya menerima:
- q: kata kunci
- filters: category, brand, price range, status
- pagination: page, size atau cursor/search_after
- sort: relevance, newest, price_asc, price_desc
Contoh query Elasticsearch
POST /products_v1/_search
{
"size": 12,
"from": 0,
"track_total_hits": true,
"query": {
"function_score": {
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "sepatu lari",
"fields": [
"title^4",
"title.autocomplete^2",
"description^1"
],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
],
"filter": [
{ "term": { "status": "active" } },
{ "term": { "category": "sepatu" } }
]
}
},
"field_value_factor": {
"field": "popularity",
"factor": 1.2,
"modifier": "log1p",
"missing": 0
},
"boost_mode": "sum"
}
},
"aggs": {
"brands": {
"terms": { "field": "brand", "size": 20 }
},
"categories": {
"terms": { "field": "category", "size": 20 }
}
},
"sort": [
"_score",
{ "created_at": "desc" },
{ "id": "asc" }
]
}Ada beberapa hal penting pada query di atas:
- multi_match mencari di beberapa field sekaligus.
- boost pada title^4 membuat kecocokan judul lebih penting daripada deskripsi.
- fuzziness: AUTO memberi toleransi salah ketik ringan.
- filter dipisahkan dari scoring agar efisien dan cache-friendly.
- function_score menambahkan sinyal bisnis seperti popularitas.
- aggs menghasilkan facet untuk filter di UI.
Autocomplete dan typo tolerance
Autocomplete biasanya tidak sama dengan search utama. Untuk autocomplete, Anda ingin hasil cepat dengan input pendek. Field title.autocomplete yang memakai edge_ngram membantu prefix matching, misalnya “sepa” cocok ke “sepatu”.
Namun edge_ngram punya trade-off: ukuran indeks membesar dan noise bisa meningkat. Jika kebutuhan autocomplete sangat besar, pertimbangkan pendekatan seperti completion suggester, tetapi tetap uji apakah ia sesuai dengan kebutuhan filtering dan ranking Anda.
Untuk typo tolerance, fuzziness berguna tetapi jangan dipakai terlalu agresif. Query pendek seperti 2-3 karakter mudah menghasilkan banyak hasil tidak relevan. Umumnya Anda perlu aturan: aktifkan fuzziness hanya setelah panjang query tertentu atau hanya pada endpoint search penuh, bukan pada autocomplete awal.
Boosting: gabungkan relevansi teks dan sinyal bisnis
Pada katalog produk, hasil yang cocok secara teks belum tentu paling berguna. Sering kali perlu mempertimbangkan:
- popularitas produk,
- stok aktif,
- rating,
- produk terbaru,
- margin atau prioritas bisnis tertentu.
Gunakan boosting secara hati-hati. Terlalu besar akan membuat hasil “relevan secara bisnis” tetapi buruk secara teks. Prinsip yang aman: kecocokan teks tetap menjadi sinyal utama, lalu faktor bisnis menjadi penyesuaian sekunder.
Faceted Search dan Pagination yang Efisien
Faceted search
Facet memungkinkan pengguna menyaring hasil berdasarkan kategori, brand, atau rentang harga. Elasticsearch cocok untuk ini karena agregasi dapat dijalankan bersamaan dengan query utama.
Untuk katalog besar, berhati-hatilah pada facet dengan cardinality sangat tinggi, misalnya ribuan brand atau tag. Agregasi semacam ini bisa mahal. Solusinya bisa berupa:
- membatasi facet yang ditampilkan,
- menggunakan ukuran agregasi yang masuk akal,
- menggunakan facet terpisah untuk field tertentu jika memang perlu.
Pagination: from/size vs search_after
from/size mudah dipakai, tetapi menjadi mahal pada offset besar karena Elasticsearch tetap perlu memproses dokumen sebelum halaman yang diminta. Untuk halaman awal, ini masih masuk akal. Untuk pagination dalam hasil yang sangat dalam, gunakan search_after.
POST /products_v1/_search
{
"size": 12,
"query": { "match": { "title": "sepatu lari" } },
"sort": [
{ "_score": "desc" },
{ "created_at": "desc" },
{ "id": "asc" }
],
"search_after": [12.3456, "2026-03-01T10:00:00Z", "prd_1001"]
}search_after lebih efisien untuk infinite scroll atau navigasi lanjut. Syaratnya, urutan sort harus stabil dan menyertakan tie-breaker, misalnya id.
Implementasi di Nuxt 3: Endpoint Server, Composable, Debounce, dan Loading State
Pada Nuxt 3, pola yang bersih adalah frontend memanggil endpoint server Nuxt, lalu server Nuxt meneruskan request ke backend API atau langsung ke service pencarian internal. Ini membantu menyembunyikan kredensial, menerapkan rate limit, dan menjaga bentuk response tetap konsisten.
Contoh endpoint server Nuxt
// server/api/search.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const res = await $fetch('http://backend-internal/api/search', {
query: {
q: query.q,
category: query.category,
brand: query.brand,
page: query.page || 1,
size: query.size || 12,
sort: query.sort || 'relevance'
},
headers: {
'x-internal-token': useRuntimeConfig().searchApiToken
}
})
return res
})Composable pencarian
// composables/useSearch.ts
export function useSearch() {
const q = ref('')
const filters = reactive({ category: '', brand: '' })
const results = ref([])
const facets = ref({ brands: [], categories: [] })
const total = ref(0)
const loading = ref(false)
const error = ref(null)
let timer: ReturnType<typeof setTimeout> | null = null
const search = async () => {
loading.value = true
error.value = null
try {
const data = await $fetch('/api/search', {
query: {
q: q.value,
category: filters.category,
brand: filters.brand,
page: 1,
size: 12
}
})
results.value = data.items
facets.value = data.facets
total.value = data.total
} catch (err: any) {
error.value = err
} finally {
loading.value = false
}
}
const debouncedSearch = () => {
if (timer) clearTimeout(timer)
timer = setTimeout(search, 300)
}
watch([q, () => filters.category, () => filters.brand], debouncedSearch)
return {
q,
filters,
results,
facets,
total,
loading,
error,
search,
debouncedSearch
}
}Debounce 300 ms cukup umum untuk input pencarian. Tujuannya mengurangi lonjakan request saat pengguna mengetik cepat. Jika endpoint mahal atau traffic tinggi, debounce bisa diperbesar sedikit. Jika autocomplete harus terasa sangat responsif, gunakan nilai lebih kecil tetapi imbangi dengan caching dan query yang ringan.
Praktik frontend yang penting
- Tampilkan loading state yang jelas, tetapi hindari layout shift berlebihan.
- Batalkan request lama bila memungkinkan agar hasil tidak tertimpa respons yang terlambat.
- Sinkronkan query ke URL agar hasil bisa dibagikan dan di-refresh.
- Pisahkan endpoint autocomplete dan endpoint search penuh jika pola query-nya berbeda.
Mengukur Relevansi Hasil, Bukan Hanya Kecepatan
Kesalahan yang sering terjadi adalah menganggap pencarian sudah baik hanya karena respons cepat. Padahal metrik utamanya adalah apakah pengguna menemukan yang dicari.
Cara evaluasi yang praktis
- Buat daftar query nyata dari log pencarian.
- Untuk tiap query, definisikan hasil yang dianggap relevan.
- Bandingkan perubahan query, analyzer, dan boosting terhadap kumpulan query tersebut.
- Amati metrik produk seperti click-through rate, refinement rate, dan zero-result rate.
Beberapa sinyal yang berguna:
- Zero-result rate: terlalu tinggi menandakan analyzer atau typo tolerance belum memadai.
- Refinement rate: jika pengguna sering mengubah query, hasil awal mungkin kurang relevan.
- CTR posisi atas: membantu mengevaluasi kualitas ranking pada halaman pertama.
Untuk debugging, gunakan API _explain atau profile saat perlu memahami mengapa sebuah dokumen mendapat skor tertentu atau mengapa query lambat. Tetapi jangan menyalakan profiling pada traffic produksi secara sembarangan karena biayanya tinggi.
Trade-off Elasticsearch vs SQL Full-Text
Kapan Elasticsearch lebih tepat
- Dokumen atau produk sangat banyak.
- Butuh kombinasi full-text, typo tolerance, boosting, dan facet.
- Relevansi perlu dikustomisasi.
- Autocomplete dan ranking lintas banyak field dibutuhkan.
Kapan SQL full-text masih cukup
- Ukuran data masih moderat.
- Kebutuhan query relatif sederhana.
- Infrastruktur ingin sesederhana mungkin.
- Tim belum siap mengelola cluster, reindex, dan tuning search.
Elasticsearch memberi fleksibilitas jauh lebih besar untuk pencarian modern, tetapi trade-off-nya nyata: infrastruktur tambahan, kompleksitas sinkronisasi, kebutuhan observability, dan risiko relevansi yang buruk jika mapping dan query tidak dirancang dengan hati-hati. SQL full-text lebih sederhana secara operasional, tetapi biasanya lebih terbatas untuk typo tolerance, ranking lanjutan, dan faceted search skala besar.
Tips Produksi dan Penutup
- Gunakan alias indeks agar reindex dan rollback lebih aman.
- Jangan berikan akses Elasticsearch langsung ke browser.
- Cache query populer di layer API bila pola traffic berulang.
- Log query pencarian dan klik hasil untuk evaluasi relevansi.
- Uji dengan data nyata, bukan hanya contoh kecil.
Membangun pencarian advance di Nuxt 3 dengan Elasticsearch bukan hanya soal menghubungkan frontend ke mesin search. Kunci utamanya ada pada desain indeks, analyzer yang sesuai dengan bahasa dan domain, query yang menggabungkan relevansi teks dengan sinyal bisnis, serta pengalaman frontend yang responsif dan stabil.
Jika Anda membangun katalog produk atau dokumentasi besar, Elasticsearch biasanya layak dipertimbangkan ketika kebutuhan sudah mencakup autocomplete, typo tolerance, boosting, facet, dan pagination efisien. Namun, adopsinya sebaiknya dilakukan dengan pemahaman yang jelas: semakin kuat fitur pencarian, semakin penting disiplin pada indexing, evaluasi relevansi, dan observability sistem.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!