Elasticsearch #
Elasticsearch adalah mesin pencari dan analitik terdistribusi yang dibangun di atas Apache Lucene. Berbeda dengan database pada umumnya, Elasticsearch dirancang khusus untuk satu hal: mencari data dengan sangat cepat dan relevan, bahkan dalam skala miliaran dokumen. Ia menyimpan data dalam format JSON yang diindeks secara penuh, sehingga setiap field bisa dijadikan kriteria pencarian tanpa perlu query yang lambat. Di ekosistem TypeScript, official client @elastic/elasticsearch sudah dilengkapi dengan tipe yang komprehensif, sehingga kamu bisa menulis query Elasticsearch dengan autocomplete dan type checking penuh — meminimalkan kesalahan yang baru terlihat saat runtime.
Instalasi #
npm install @elastic/elasticsearch
Untuk TypeScript, tidak perlu paket @types tambahan karena tipe sudah tersedia di dalam paket utama.
# verifikasi instalasi
npx tsc --version # pastikan TypeScript >= 4.5
Konsep Dasar Elasticsearch #
Sebelum menulis kode, penting memahami terminologi Elasticsearch dan padanannya dengan konsep database relasional:
| Elasticsearch | Database Relasional | Keterangan |
|---|---|---|
| Index | Tabel | Kumpulan dokumen dengan tipe data serupa |
| Document | Row / baris | Satu unit data dalam format JSON |
| Field | Kolom | Properti dalam dokumen |
| Mapping | Schema | Definisi tipe data untuk setiap field |
| Shard | Partisi | Potongan index untuk distribusi data |
| Replica | Backup | Salinan shard untuk fault tolerance |
Konsep yang paling berbeda dari database biasa adalah inverted index — cara Elasticsearch menyimpan data untuk pencarian. Alih-alih menyimpan “dokumen A berisi kata X”, Elasticsearch menyimpan “kata X ada di dokumen A, B, C”. Struktur inilah yang membuat pencarian teks begitu cepat.
flowchart LR
A["Dokumen masuk:\n'Laptop Gaming Asus'"] --> B[Analyzer]
B --> C["Token:\n'laptop', 'gaming', 'asus'"]
C --> D[(Inverted Index)]
D --> E["'laptop' → doc1, doc5, doc9\n'gaming' → doc1, doc3, doc7\n'asus' → doc1, doc8"]Koneksi ke Elasticsearch #
import { Client } from "@elastic/elasticsearch";
// koneksi ke Elasticsearch lokal
const client = new Client({
node: "http://localhost:9200",
});
// koneksi ke Elastic Cloud atau dengan autentikasi
const clientCloud = new Client({
node: "https://my-deployment.es.us-east-1.aws.found.io",
auth: {
apiKey: process.env.ELASTIC_API_KEY!,
},
// atau gunakan username/password
// auth: {
// username: "elastic",
// password: process.env.ELASTIC_PASSWORD!,
// },
tls: {
rejectUnauthorized: false, // hanya untuk development
},
});
// verifikasi koneksi
async function cekKoneksi(): Promise<void> {
try {
const info = await client.info();
console.log(`Terhubung ke Elasticsearch ${info.version.number}`);
} catch (error) {
console.error("Gagal terhubung:", error);
throw error;
}
}
Untuk development lokal, jalankan Elasticsearch via Docker: docker run -d --name elasticsearch -p 9200:9200 -e "discovery.type=single-node" -e "xpack.security.enabled=false" elasticsearch:8.13.0Mapping dan Index #
Mapping mendefinisikan bagaimana setiap field dalam dokumen disimpan dan diindeks. Mendefinisikan mapping secara eksplisit — alih-alih membiarkan Elasticsearch menebak tipenya — adalah praktik yang wajib di production.
Tipe Field yang Sering Digunakan #
// tipe field di mapping:
// text → full-text search, dianalisis oleh analyzer
// keyword → exact match, untuk filter/sort/aggregation
// integer, float, double → angka
// boolean → true/false
// date → tanggal dan waktu
// object → nested object sederhana
// nested → array of object dengan query independen
// geo_point → koordinat lat/lon
Membuat Index dengan Mapping #
interface Produk {
nama: string;
deskripsi: string;
kategori: string;
harga: number;
stok: number;
tags: string[];
aktif: boolean;
createdAt: string; // ISO 8601
}
async function buatIndexProduk(): Promise<void> {
const indexAda = await client.indices.exists({ index: "produk" });
if (indexAda) {
console.log("Index 'produk' sudah ada");
return;
}
await client.indices.create({
index: "produk",
body: {
settings: {
number_of_shards: 1, // cukup untuk development/skala kecil
number_of_replicas: 1,
analysis: {
analyzer: {
// analyzer kustom untuk bahasa Indonesia
analyzer_indonesia: {
type: "custom",
tokenizer: "standard",
filter: ["lowercase", "asciifolding"],
},
},
},
},
mappings: {
properties: {
nama: {
type: "text",
analyzer: "analyzer_indonesia",
fields: {
keyword: { type: "keyword" }, // untuk exact match & sort
},
},
deskripsi: {
type: "text",
analyzer: "analyzer_indonesia",
},
kategori: {
type: "keyword", // exact match, tidak dianalisis
},
harga: { type: "float" },
stok: { type: "integer" },
tags: { type: "keyword" },
aktif: { type: "boolean" },
createdAt: {
type: "date",
format: "strict_date_optional_time",
},
},
},
},
});
console.log("Index 'produk' berhasil dibuat");
}
Indexing Dokumen #
“Indexing” di Elasticsearch berarti menyimpan dokumen ke dalam index — bukan membuat index seperti di database. Setiap dokumen yang disimpan langsung tersedia untuk dicari.
Simpan Satu Dokumen #
async function simpanProduk(produk: Produk): Promise<string> {
const hasil = await client.index({
index: "produk",
document: produk,
// id bisa ditentukan manual atau dibiarkan di-generate otomatis
// id: "produk-001",
});
return hasil._id; // ID yang di-generate Elasticsearch
}
// dengan ID yang sudah ditentukan — berguna untuk sinkronisasi dari database lain
async function simpanProdukDenganId(id: string, produk: Produk): Promise<void> {
await client.index({
index: "produk",
id,
document: produk,
// refresh: "wait_for" — tunggu sampai dokumen bisa dicari sebelum return
// gunakan hanya di test, bukan production (mahal)
});
}
Bulk Indexing — Menyimpan Banyak Dokumen Sekaligus #
Untuk menyimpan banyak dokumen, selalu gunakan bulk API. Mengirim satu dokumen per request adalah anti-pattern yang sangat mahal — setiap request memiliki overhead jaringan dan index refresh.
// ANTI-PATTERN: loop dengan index satu per satu
async function simpanProdukSatuSatu(produkList: Produk[]): Promise<void> {
for (const produk of produkList) {
await client.index({ index: "produk", document: produk }); // ✗ N request untuk N dokumen
}
}
// BENAR: gunakan bulk API
async function bulkSimpanProduk(produkList: Array<{ id: string; data: Produk }>): Promise<{
berhasil: number;
gagal: number;
}> {
const operations = produkList.flatMap(({ id, data }) => [
{ index: { _index: "produk", _id: id } },
data,
]);
const hasil = await client.bulk({
operations,
refresh: false, // jangan tunggu refresh — biarkan Elasticsearch refresh secara periodik
});
const gagal = hasil.items.filter((item) => item.index?.error).length;
const berhasil = hasil.items.length - gagal;
if (hasil.errors) {
const errorItems = hasil.items
.filter((item) => item.index?.error)
.slice(0, 5); // log maksimal 5 error pertama
console.error("Sebagian dokumen gagal diindex:", errorItems);
}
return { berhasil, gagal };
}
Update dan Delete Dokumen #
// update parsial — hanya field yang disertakan yang berubah
async function updateProduk(id: string, perubahan: Partial<Produk>): Promise<void> {
await client.update({
index: "produk",
id,
doc: perubahan,
doc_as_upsert: false, // false = error jika dokumen tidak ada
});
}
// update menggunakan script — untuk operasi atomik
async function tambahStok(id: string, jumlah: number): Promise<void> {
await client.update({
index: "produk",
id,
script: {
source: "ctx._source.stok += params.jumlah",
params: { jumlah },
},
});
}
// hapus dokumen
async function hapusProduk(id: string): Promise<boolean> {
try {
await client.delete({ index: "produk", id });
return true;
} catch (error: any) {
if (error?.meta?.statusCode === 404) return false;
throw error;
}
}
// hapus berdasarkan query
async function hapusProdukTidakAktif(): Promise<number> {
const hasil = await client.deleteByQuery({
index: "produk",
body: {
query: {
term: { aktif: false },
},
},
});
return hasil.deleted ?? 0;
}
Query DSL — Mencari Dokumen #
Query DSL (Domain Specific Language) adalah cara utama berinteraksi dengan Elasticsearch. Semua query direpresentasikan sebagai objek JSON.
flowchart TD
A[Query DSL] --> B[Query Context]
A --> C[Filter Context]
B --> D["Menghitung relevance score\nDokumen lebih relevan = score lebih tinggi\nContoh: match, multi_match, fuzzy"]
C --> E["Ya atau Tidak — tidak ada score\nLebih cepat & bisa di-cache\nContoh: term, range, exists"]Memahami perbedaan query context dan filter context sangat penting untuk performa. Gunakan filter context sebisa mungkin untuk kondisi yang bersifat binary (aktif/tidak aktif, range harga, dll), dan simpan query context untuk pencarian teks yang memerlukan ranking relevansi.
Match Query — Full-Text Search #
interface HasilCari<T> {
hits: Array<{
_id: string;
_score: number;
_source: T;
}>;
total: number;
}
async function cariProduk(keyword: string): Promise<HasilCari<Produk>> {
const hasil = await client.search<Produk>({
index: "produk",
body: {
query: {
match: {
nama: {
query: keyword,
fuzziness: "AUTO", // toleransi typo otomatis
},
},
},
},
});
return {
hits: hasil.hits.hits.map((hit) => ({
_id: hit._id!,
_score: hit._score ?? 0,
_source: hit._source as Produk,
})),
total:
typeof hasil.hits.total === "number"
? hasil.hits.total
: (hasil.hits.total?.value ?? 0),
};
}
// multi_match — cari di beberapa field sekaligus
async function cariMultiField(keyword: string): Promise<HasilCari<Produk>> {
const hasil = await client.search<Produk>({
index: "produk",
body: {
query: {
multi_match: {
query: keyword,
fields: [
"nama^3", // bobot 3x — lebih relevan jika ada di nama
"deskripsi^1", // bobot 1x
"tags^2", // bobot 2x
],
type: "best_fields", // ambil score terbaik dari semua field
fuzziness: "AUTO",
},
},
},
});
return {
hits: hasil.hits.hits.map((hit) => ({
_id: hit._id!,
_score: hit._score ?? 0,
_source: hit._source as Produk,
})),
total:
typeof hasil.hits.total === "number"
? hasil.hits.total
: (hasil.hits.total?.value ?? 0),
};
}
Bool Query — Kombinasi Kondisi #
Bool query adalah cara menggabungkan beberapa query. Ini adalah query yang paling sering digunakan di production karena hampir selalu ada lebih dari satu kondisi pencarian.
// must → harus cocok, mempengaruhi score
// should → bagus jika cocok, meningkatkan score
// must_not → tidak boleh cocok, tidak mempengaruhi score
// filter → harus cocok, tidak mempengaruhi score (lebih cepat)
async function cariProdukLanjutan(params: {
keyword?: string;
kategori?: string;
hargaMin?: number;
hargaMaks?: number;
tags?: string[];
halaman?: number;
perHalaman?: number;
}): Promise<HasilCari<Produk> & { halaman: number; totalHalaman: number }> {
const { keyword, kategori, hargaMin, hargaMaks, tags, halaman = 1, perHalaman = 10 } = params;
const from = (halaman - 1) * perHalaman;
const must: any[] = [];
const filter: any[] = [];
// full-text search masuk ke must (mempengaruhi relevance)
if (keyword) {
must.push({
multi_match: {
query: keyword,
fields: ["nama^3", "deskripsi", "tags^2"],
fuzziness: "AUTO",
},
});
}
// kondisi binary masuk ke filter (tidak mempengaruhi score, bisa di-cache)
filter.push({ term: { aktif: true } });
if (kategori) {
filter.push({ term: { kategori } });
}
if (hargaMin !== undefined || hargaMaks !== undefined) {
const range: any = {};
if (hargaMin !== undefined) range.gte = hargaMin;
if (hargaMaks !== undefined) range.lte = hargaMaks;
filter.push({ range: { harga: range } });
}
if (tags && tags.length > 0) {
filter.push({ terms: { tags } }); // dokumen mengandung salah satu tag
}
const hasil = await client.search<Produk>({
index: "produk",
from,
size: perHalaman,
body: {
query: {
bool: {
must: must.length > 0 ? must : [{ match_all: {} }],
filter,
},
},
sort: keyword
? [{ _score: "desc" }] // jika ada keyword, sort by relevance
: [{ createdAt: "desc" }], // jika tidak, sort by terbaru
},
});
const total =
typeof hasil.hits.total === "number"
? hasil.hits.total
: (hasil.hits.total?.value ?? 0);
return {
hits: hasil.hits.hits.map((hit) => ({
_id: hit._id!,
_score: hit._score ?? 0,
_source: hit._source as Produk,
})),
total,
halaman,
totalHalaman: Math.ceil(total / perHalaman),
};
}
Query Tambahan yang Sering Digunakan #
// term — exact match untuk keyword field
{ term: { kategori: "elektronik" } }
// terms — exact match salah satu dari beberapa nilai
{ terms: { kategori: ["elektronik", "komputer"] } }
// range — filter berdasarkan rentang nilai
{ range: { harga: { gte: 100000, lte: 5000000 } } }
{ range: { createdAt: { gte: "2024-01-01", lte: "2024-12-31" } } }
// exists — dokumen yang memiliki field tertentu
{ exists: { field: "deskripsi" } }
// wildcard — pattern matching (lambat, hindari di production)
{ wildcard: { "nama.keyword": "*gaming*" } }
// prefix — dokumen yang field-nya dimulai dengan prefix tertentu
{ prefix: { "nama.keyword": "laptop" } }
Aggregation #
Aggregation di Elasticsearch memungkinkan kamu menghitung statistik, membuat histogram, dan mengelompokkan data — mirip dengan GROUP BY di SQL tapi jauh lebih ekspresif. Aggregation berjalan di atas hasil query, sehingga kamu bisa menggabungkan pencarian dan analitik dalam satu request.
interface AggregasiProduk {
perKategori: Array<{
key: string;
jumlah: number;
rataRataHarga: number;
minHarga: number;
maxHarga: number;
}>;
distribusiHarga: Array<{
key: number;
jumlah: number;
}>;
totalProdukAktif: number;
}
async function statistikProduk(keyword?: string): Promise<AggregasiProduk> {
const hasil = await client.search({
index: "produk",
size: 0, // kita hanya butuh aggregasi, bukan dokumen
body: {
query: keyword
? { match: { nama: keyword } }
: { match_all: {} },
aggs: {
// terms aggregation — kelompokkan berdasarkan nilai field
per_kategori: {
terms: {
field: "kategori",
size: 20, // maksimal 20 kategori
order: { _count: "desc" },
},
// sub-aggregation — hitung statistik di dalam setiap bucket
aggs: {
rata_rata_harga: { avg: { field: "harga" } },
min_harga: { min: { field: "harga" } },
max_harga: { max: { field: "harga" } },
},
},
// histogram — distribusi berdasarkan rentang nilai
distribusi_harga: {
histogram: {
field: "harga",
interval: 1000000, // setiap 1 juta
min_doc_count: 1,
},
},
// filter aggregation — hitung subset tertentu
produk_aktif: {
filter: { term: { aktif: true } },
},
},
},
});
const aggs = hasil.aggregations as any;
return {
perKategori: (aggs.per_kategori.buckets as any[]).map((bucket) => ({
key: bucket.key,
jumlah: bucket.doc_count,
rataRataHarga: Math.round(bucket.rata_rata_harga.value ?? 0),
minHarga: bucket.min_harga.value ?? 0,
maxHarga: bucket.max_harga.value ?? 0,
})),
distribusiHarga: (aggs.distribusi_harga.buckets as any[]).map((bucket) => ({
key: bucket.key,
jumlah: bucket.doc_count,
})),
totalProdukAktif: aggs.produk_aktif.doc_count,
};
}
Date Histogram — Analitik Berdasarkan Waktu #
async function trendProdukBaruPerBulan(): Promise<
Array<{ bulan: string; jumlah: number }>
> {
const hasil = await client.search({
index: "produk",
size: 0,
body: {
aggs: {
per_bulan: {
date_histogram: {
field: "createdAt",
calendar_interval: "month",
format: "yyyy-MM",
order: { _key: "asc" },
},
},
},
},
});
const aggs = hasil.aggregations as any;
return (aggs.per_bulan.buckets as any[]).map((bucket) => ({
bulan: bucket.key_as_string,
jumlah: bucket.doc_count,
}));
}
Highlight dan Suggest #
Highlight — Tandai Kata yang Cocok #
Highlight mengembalikan potongan teks dengan kata yang cocok ditandai — fitur yang sangat berguna untuk menampilkan hasil pencarian ke pengguna.
async function cariDenganHighlight(keyword: string) {
const hasil = await client.search<Produk>({
index: "produk",
body: {
query: {
multi_match: {
query: keyword,
fields: ["nama", "deskripsi"],
},
},
highlight: {
pre_tags: ["<mark>"], // tag pembuka highlight
post_tags: ["</mark>"], // tag penutup highlight
fields: {
nama: { number_of_fragments: 0 }, // tampilkan seluruh field nama
deskripsi: {
number_of_fragments: 2, // maksimal 2 potongan
fragment_size: 150, // panjang setiap potongan (karakter)
},
},
},
},
});
return hasil.hits.hits.map((hit) => ({
id: hit._id,
produk: hit._source,
highlight: {
nama: hit.highlight?.nama?.[0],
deskripsi: hit.highlight?.deskripsi?.join(" ... "),
},
}));
}
Autocomplete Suggest #
// untuk autocomplete, gunakan completion suggester
// mapping-nya harus didefinisikan terlebih dahulu:
// suggest: { type: "completion" }
async function autocomplete(prefix: string): Promise<string[]> {
const hasil = await client.search({
index: "produk",
body: {
suggest: {
nama_suggest: {
prefix,
completion: {
field: "suggest", // field dengan type "completion" di mapping
size: 5,
skip_duplicates: true,
},
},
},
},
_source: false, // tidak perlu source dokumen, hanya suggestion
});
const suggest = hasil.suggest as any;
return (suggest?.nama_suggest?.[0]?.options ?? []).map(
(opt: any) => opt.text as string
);
}
Pola Integrasi: Elasticsearch sebagai Search Layer #
Elasticsearch jarang berdiri sendiri sebagai database utama. Pola yang paling umum adalah menggunakan database relasional atau MongoDB sebagai source of truth, dan Elasticsearch sebagai search layer yang diisi secara sinkron.
sequenceDiagram
participant Client
participant API
participant DB as Database Utama
participant ES as Elasticsearch
Client->>API: POST /produk (create)
API->>DB: INSERT INTO produk
DB-->>API: id dokumen baru
API->>ES: index dokumen ke ES
ES-->>API: acknowledged
API-->>Client: 201 Created
Client->>API: GET /produk/search?q=laptop
API->>ES: search query
ES-->>API: hasil pencarian + ID
API-->>Client: hasil pencarian// service yang menjaga sinkronisasi antara DB dan Elasticsearch
class ProdukService {
constructor(
private readonly db: any, // koneksi database utama
private readonly es: Client
) {}
async buat(data: Omit<Produk, "createdAt">): Promise<string> {
// 1. simpan ke database utama terlebih dahulu
const id = await this.db.insert("produk", {
...data,
createdAt: new Date().toISOString(),
});
// 2. index ke Elasticsearch
// jika ES gagal, dokumen tetap ada di DB — bisa di-retry
try {
await this.es.index({
index: "produk",
id,
document: { ...data, createdAt: new Date().toISOString() },
refresh: false,
});
} catch (error) {
console.error(`Gagal index produk ${id} ke Elasticsearch:`, error);
// simpan ke antrian retry (Redis, RabbitMQ, dll)
}
return id;
}
async cari(params: { keyword?: string; kategori?: string }) {
// pencarian selalu lewat Elasticsearch
return cariProdukLanjutan(params);
}
async ambilById(id: string): Promise<Produk | null> {
// get by ID bisa langsung dari database utama (lebih konsisten)
return this.db.findById("produk", id);
}
}
Jangan gunakan Elasticsearch sebagai database utama. Elasticsearch tidak menjamin konsistensi data seperti database ACID — dokumen yang baru diindex belum tentu langsung bisa dicari (tergantung refresh interval, default 1 detik). Selalu gunakan Elasticsearch sebagai lapisan pencarian di atas database utama yang lebih konsisten.
Penanganan Error #
import { errors } from "@elastic/elasticsearch";
async function cariAman(keyword: string): Promise<HasilCari<Produk>> {
try {
return await cariProduk(keyword);
} catch (error) {
if (error instanceof errors.ResponseError) {
// error dari Elasticsearch (status code 4xx atau 5xx)
const statusCode = error.meta.statusCode;
if (statusCode === 404) {
// index tidak ditemukan
throw new Error(`Index 'produk' tidak ditemukan. Pastikan sudah dibuat.`);
}
if (statusCode === 400) {
// query tidak valid
const detail = error.meta.body?.error?.reason ?? "Query tidak valid";
throw new Error(`Query error: ${detail}`);
}
if (statusCode === 429) {
// too many requests — Elasticsearch kelebihan beban
throw new Error("Elasticsearch sedang sibuk, coba lagi sebentar");
}
}
if (error instanceof errors.ConnectionError) {
throw new Error("Tidak bisa terhubung ke Elasticsearch");
}
if (error instanceof errors.TimeoutError) {
throw new Error("Request ke Elasticsearch timeout");
}
throw error;
}
}
// re-index — berguna untuk sinkronisasi ulang jika ES tertinggal dari DB
async function reindexSemua(
ambilSemua: () => AsyncGenerator<Array<{ id: string; data: Produk }>>
): Promise<void> {
let totalBerhasil = 0;
let totalGagal = 0;
for await (const batch of ambilSemua()) {
const { berhasil, gagal } = await bulkSimpanProduk(batch);
totalBerhasil += berhasil;
totalGagal += gagal;
console.log(`Progress: ${totalBerhasil} berhasil, ${totalGagal} gagal`);
}
console.log(`Reindex selesai: ${totalBerhasil} dokumen`);
}
Kapan Menggunakan Elasticsearch #
Elasticsearch bukan solusi untuk semua masalah. Memahami batasannya sama pentingnya dengan memahami kemampuannya.
Gunakan Elasticsearch jika:
✓ Aplikasi memerlukan full-text search yang relevan (e-commerce, blog, portal berita)
✓ Perlu pencarian dengan toleransi typo (fuzziness)
✓ Butuh highlight kata yang cocok di hasil pencarian
✓ Perlu analitik real-time dari data yang terus masuk (log, event)
✓ Autocomplete dan suggest pada kolom pencarian
✓ Skala data sangat besar dan perlu horizontal scaling untuk search
Jangan gunakan Elasticsearch sebagai pengganti jika:
✗ Data memerlukan konsistensi ACID yang kuat — gunakan PostgreSQL/MySQL
✗ Query sederhana seperti "cari by ID" atau "filter by status" — overkill
✗ Data memiliki relasi kompleks yang memerlukan join — gunakan database relasional
✗ Butuh transaksi multi-dokumen yang atomik — Elasticsearch tidak mendukung ini
✗ Tim belum punya kapasitas untuk maintain cluster — pertimbangkan PostgreSQL full-text search
Ringkasan #
- Elasticsearch bukan database utama — gunakan sebagai search layer di atas database yang lebih konsisten (PostgreSQL, MongoDB). Database utama adalah source of truth.
- Definisikan mapping secara eksplisit sebelum menyimpan data. Mapping otomatis dari Elasticsearch sering tidak optimal —
textvskeywordharus dipilih secara sadar sesuai kebutuhan query.- Query context vs filter context — gunakan
filteruntuk kondisi binary (aktif/tidak, range, exact match) karena lebih cepat dan bisa di-cache; gunakanmusthanya untuk kondisi yang mempengaruhi relevance score.- Bool query adalah fondasi — hampir semua query production menggunakan kombinasi
must,filter,should, danmust_notdi dalamboolquery.- Bulk API wajib untuk indexing massal — jangan pernah loop
index()satu per satu; kirim dalam batch menggunakanbulk()untuk performa yang jauh lebih baik.- Aggregation menggantikan SQL GROUP BY — manfaatkan
terms,histogram,date_histogram, dan sub-aggregation untuk laporan analitik langsung dari Elasticsearch.- Highlight dan fuzziness adalah fitur yang langsung meningkatkan UX — tampilkan kata yang cocok dengan highlight dan toleransi typo dengan
fuzziness: "AUTO".- Tangkap
errors.ResponseErroruntuk error dari server danerrors.ConnectionErroruntuk masalah jaringan — keduanya perlu penanganan yang berbeda.