Insights

Spotlight: Menemukan Lebih Penting daripada Meranking

Pencarian yang meranking menjawab "mana yang paling cocok". Pencarian yang menemukan menjawab "apakah ini ada". Tentang membangun Spotlight yang menemukan setiap kata dari 74.000 kata konten, dengan server yang menyiapkan bahan dan browser yang mencari.

· 13 min

Spotlight: Menemukan Lebih Penting daripada Meranking


Catatan tentang Kata yang Kamu Tahu Ada di Sana

Saya mengetik az-zumar di pencarian situs saya sendiri.

Kosong.

Padahal saya yang menulis kata itu. Ada di dalam blok kutipan Al-Quran pada salah satu catatan.

Pencarian di window Notes menemukannya tanpa kesulitan. Spotlight tidak.

Ada rasa tertentu ketika sebuah sistem gagal menemukan sesuatu yang kamu tahu persis ada di dalamnya.

Itu bukan rasa "hasilnya kurang relevan". Itu rasa tidak bisa dipercaya.

Spotlight di macOS tidak pernah memberi rasa itu. Kata di dalam PDF yang kamu lupa pernah menyimpannya, nama berkas yang kamu buat tiga tahun lalu, catatan yang tak pernah kamu buka lagi. Semua ketemu.

Standarnya bukan "tampilkan yang paling cocok". Standarnya lebih sederhana dan lebih keras: kalau itu ada di dalam mesinmu, ia ketemu.

Standar itu yang saya kejar.

Ternyata yang dituntutnya bukan cuma arsitektur yang berbeda dari dugaan saya, tapi juga satu kebiasaan berpikir yang harus saya buang.


1. Dua Produk yang Sering Disangka Satu

"Pencarian yang bagus" sebenarnya menggambarkan dua hal berbeda.

Yang pertama menjawab: dari sekian banyak yang cocok, mana yang paling layak muncul duluan?

Itu pekerjaan mesin pencari. Ada jutaan halaman tentang satu kata kunci, dan nilainya ada pada apa yang tidak ditampilkan.

Yang kedua menjawab: apakah benda ini ada di sini?

Itu pekerjaan Spotlight. Yang dicari cuma satu, kamu tahu ia ada, dan kegagalannya bukan "urutannya kurang pas", tapi "tidak ketemu sama sekali".

Situs ini butuh yang kedua. Isinya bukan jutaan halaman, tapi ratusan. Dan pemiliknya, saya sendiri, sering mencari sesuatu yang sudah pasti ada.

Konsekuensinya panjang.

Setiap teks yang pernah disuguhkan ke pembaca harus bisa ditemukan: nama app di Dock, item di desktop, judul dan isi catatan, dokumen, track audio, album foto, produk toko beserta detailnya.

Salah ketik harus dimaafkan. Potongan di tengah kata harus ketemu. Satu huruf pun sudah harus menjawab.

Bukan daftar fitur. Itu satu janji yang dipecah jadi banyak syarat.


2. Angka yang Menentukan Segalanya

Sebelum memilih mesin pencarian, ada satu hal yang harus diukur lebih dulu: seberapa besar teks yang mau dicari.

Angka itu yang menentukan apakah indeks boleh tinggal di browser sama sekali.

Semua keputusan sesudahnya menggantung padanya. Menebaknya berarti menebak seluruh arsitektur.

Hasil pengukuran isi situs ini:

Entri 172: 128 catatan, 3 dokumen, 41 produk
Teks 445.859 karakter, sekitar 74.000 kata
JSON mentah 474 KB
Yang benar-benar diunduh 164 KB (brotli, dari CDN)

Angka terakhir itu yang penting, dan cuma itu yang layak dipakai untuk memutuskan.

Mengukur JSON mentah lalu menyimpulkan "474 KB terlalu berat" akan menghasilkan keputusan yang salah tiga kali lipat.

164 KB itu murah. Kira-kira seukuran satu foto produk yang belum dioptimalkan. Situs ini sudah mengirim lebih dari itu untuk hal-hal yang jauh kurang berguna.

Kalau hasilnya 5 MB, seluruh tulisan ini akan berbeda. Indeksnya harus tinggal di server, dan kecepatannya harus dikorbankan.

Yang membuat arsitektur di bawah ini masuk akal bukan kepintaran rancangannya, tapi satu angka yang kebetulan kecil.


3. Algolia, Orama, atau Tulis Sendiri

Saya berangkat dengan asumsi harus memasang mesin pencarian sungguhan.

Algolia adalah pilihan yang jelas kalau butuh layanan terkelola. Free tier-nya nyata dan tidak pelit untuk situs seukuran ini.

Tapi itu layanan jaringan. Setiap ketikan jadi permintaan ke server orang lain, dan syarat "satu huruf pun langsung muncul" berubah jadi soal latensi yang bukan milik saya.

Orama menyelesaikan masalah itu dengan cara lain. Pustaka, bukan layanan.

Indeksnya berupa radix tree yang dibangun di dalam proses yang memakainya, skornya BM25, API-nya sinkron di v3, dan tidak ada dependensi yang ikut terseret. Di dalam build situs ini mesinnya menempati satu chunk sendiri berukuran 18,6 KB terkompresi.

Yang tidak saya duga: kode pencarian yang sudah ada di situs ini ternyata sudah mengerjakan sebagian besar pekerjaan dengan baik.

Mencocokkan metadata dari state yang memang sudah ada di memori, tanpa satu pun permintaan jaringan. Dan paham hal-hal yang tak diketahui pustaka mana pun: bahwa nama app lebih penting daripada nama berkas, bahwa app yang dinonaktifkan tetap harus muncul tapi paling bawah.

Jadi pertanyaannya bukan "Algolia, Orama, atau tulis sendiri".

Pertanyaannya: bagian mana yang saya kerjakan sendiri, dan bagian mana yang saya serahkan?

Jawabannya jadi pembelahan yang cukup rapi.

Metadata sudah ada di browser, jumlahnya ratusan, dan aturan prioritasnya sangat spesifik untuk situs ini. Itu bagian saya, tanpa pustaka.

Badan tulisan berjumlah 74.000 kata, butuh BM25 dan struktur indeks yang benar, dan tak ada nilainya menulis ulang radix tree. Itu bagian Orama.


4. Pembagian yang Mengikuti Sifat Kerjanya

Pembelahan berikutnya bukan soal pustaka, tapi soal tempat.

Menyusun teks itu mahal. Artinya membaca 128 catatan, 3 dokumen, dan 41 produk dari Firestore, mengambil teks polosnya, membuang yang kosong.

Tapi hasilnya sama persis untuk setiap pengunjung.

Mencocokkan teks itu murah. Satu kueri di korpus ini selesai dalam waktu di bawah satu milidetik.

Tapi harus terjadi di detik yang sama dengan ketikan.

Dua sifat itu menunjuk ke dua tempat yang berbeda.

Yang mahal-tapi-sama milik server: dikerjakan sekali, dipakai bersama. Yang murah-tapi-harus-instan milik browser: dikerjakan di tempat jarinya mengetik.

Sisi server jadi satu berkas:

// src/lib/search/fullTextArtifact.ts
import "server-only";

export async function buildFullTextArtifact(): Promise<PublicFullTextArtifact> {
  const [notes, documents, products] = await Promise.all([
    loadNotes(), loadDocuments(), loadStoreProducts(),
  ]);

  return {
    notes: withText(notes.map((note) => [note.id, note.plainText ?? ""] as const)),
    documents: withText(documentBodies),
    products: withText(products.map((product) =>
      [product.id, Object.values(product.sections).join(" ")] as const)),
  };
}

Disajikan lewat route handler yang tidak pernah jalan saat orang mencari:

// src/app/api/public/search-index/route.ts
export const dynamic = "force-static";
export const revalidate = false;

export async function GET() {
  return publicContentJson(await buildFullTextArtifact());
}

force-static dengan revalidate: false berarti berkas ini dirender sekali saat build, lalu jadi objek statis di CDN.

Header respons dari situs live mengonfirmasinya: X-Vercel-Cache: HIT, Content-Encoding: br.

Tidak ada function invocation. Tidak ada tagihan per pencarian.

Selisihnya di sini bukan soal elegansi.

Satu kueri lokal terukur antara 0,04 dan 1,6 milidetik. Yang paling lambat justru kueri satu huruf, karena mencocok hampir seluruh korpus.

Satu perjalanan bolak-balik ke server, seoptimal apa pun, jarang di bawah lima puluh milidetik.

Perbedaan puluhan kali lipat itulah yang memisahkan "hasil muncul sambil kamu mengetik" dari "hasil muncul setelah kamu berhenti mengetik".

Dan begitu hasilnya baru muncul setelah orang berhenti mengetik, kamu butuh debounce. Janji "satu huruf pun langsung menjawab" batal dengan sendirinya.


5. Batas yang Tidak Boleh Dilewati

Ada satu kalimat yang mengubah cara saya melihat berkas ini: artefak itu berkas publik.

Disajikan dari /api/public/search-index, di-cache di CDN, dan bisa dibuka siapa saja yang mengetik URL-nya. Tidak ada sesi, tidak ada header, tidak ada yang menjaganya.

Maka pertanyaan "apa saja yang boleh masuk ke sini" bukan pertanyaan desain.

Itu pertanyaan keamanan, dan jawabannya cuma satu: hanya yang sudah publik lewat halaman atau endpoint lain.

Window User tidak boleh masuk: pesanan, alamat, nomor resi. Window Settings juga tidak: katalog pemilik, HPP, catatan utang.

Selamanya, bukan sampai ada cara mengamankannya.

Baris pertama berkasnya menegakkan separuh aturan itu:

import "server-only";

Separuh sisanya tidak bisa ditegakkan oleh tipe.

Bahaya sebenarnya bukan seseorang sengaja menambahkan data pribadi. Bahayanya adalah ...note yang tampak polos, yang menyeret seluruh isi record ke dalam berkas publik tanpa siapa pun menyadarinya.

Jadi ditegakkan oleh test:

it("carries bodies only, nothing else from the source objects", async () => {
  loaders.loadNotes.mockResolvedValue([{
    id: "note",
    plainText: "the writing itself",
    title: "A Title",
    company: "A private employer note",
  }]);

  const serialised = JSON.stringify(await buildFullTextArtifact());

  expect(serialised).toContain("the writing itself");
  expect(serialised).not.toContain("A Title");
  expect(serialised).not.toContain("A private employer note");
});

Test ini tidak memeriksa fungsinya bekerja benar. Test ini memeriksa fungsinya tidak bocor.

Merahnya akan datang pada hari seseorang menulis spread yang kelihatan tidak berbahaya. Kemungkinan besar saya, setahun dari sekarang, sedang buru-buru.

Batas keamanan yang ditulis sebagai komentar akan dilanggar. Batas yang ditulis sebagai test akan berteriak.


6. Menemukan Lebih Penting daripada Meranking

Di sini saya nyaris mengambil jalan yang salah, dan menolaknya mengubah sisa sistem.

Setelah pencarian di dalam kata jalan, hasilnya jadi ramai.

Mengetik cap menemukan "Cap" di judul produk, tapi juga menemukan kata "pencapaian" di tengah sebuah esai. Secara teknis benar. Sebagai hasil pencarian, terasa seperti sampah.

Jalan keluarnya kelihatan gampang: naikkan ambang minimum pencarian substring, supaya potongan pendek berhenti mencocok di tengah kata. Hasilnya langsung rapi.

Saya tidak mengambilnya.

Kalau teksnya memang ada di situ, menyembunyikannya membatalkan seluruh tujuan mengindeksnya.

Sistem yang berjanji menemukan segalanya lalu diam-diam berhenti menemukan sebagian bukan sistem yang lebih rapi. Cuma sistem yang tidak memberitahu apa yang disembunyikannya.

Perbaikan yang benar bukan menyaring. Perbaikan yang benar adalah memperbaiki urutan:

/**
 * A fragment that begins a word is much closer to what someone meant than one
 * buried inside another: "cap" starting "Cap 1" versus "cap" inside
 * "pencapaian". Both stay findable, every character in the corpus is
 * reachable, which is the point. They simply do not deserve the same rank.
 */
const WORD_START_SCORE = 0.5;
const MID_WORD_SCORE = 0.2;

Potongan yang mengawali kata bernilai 0,5. Yang terkubur di tengah bernilai 0,2.

Keduanya tetap muncul. Yang berubah cuma siapa duluan.

Perbedaan antara dua pendekatan ini kecil di kode dan besar di kepercayaan.

Menyaring berarti sistem memutuskan apa yang pantas kamu lihat. Mengurutkan berarti sistem memberi pendapat, tapi tetap menunjukkan semuanya.

Sekali kamu memilih yang pertama, kamu tidak akan pernah tahu apa lagi yang disembunyikannya darimu.


7. Salah Ketik yang Benar-Benar Dilakukan Manusia

Jarak edit standar, Levenshtein, menghitung tiga jenis kesalahan: huruf salah, huruf hilang, huruf berlebih.

Huruf tertukar tidak dihitung.

Bagi Levenshtein, mengubah report jadi reprot butuh dua operasi, sama mahalnya dengan dua kesalahan yang sama sekali tak berhubungan.

Padahal itulah salah ketik yang paling manusiawi. Lahir dari dua jari yang mendarat tidak berurutan.

Dan sudah tercatat sejak F. J. Damerau – A Technique for Computer Detection and Correction of Spelling Errors, CACM 1964 sebagai salah satu dari empat jenis kesalahan yang mencakup mayoritas salah eja satu-huruf.

Ini ditemukan oleh sebuah test yang gagal, bukan oleh kejelian.

Saya menulis kasus uji dengan contoh reprot untuk report, yakin akan hijau. Merah.

Perbaikannya menambah satu baris ke dalam matriks jarak:

const swapped = row > 1
  && column > 1
  && left[row - 1] === right[column - 2]
  && left[row - 2] === right[column - 1];
if (swapped) cell = Math.min(cell, (beforePrevious[column - 2] ?? 0) + 1);

Sejak itu pencarian metadata memaafkan huruf tertukar.

Dan di situlah cerita ini sebenarnya baru mulai.

Saat menyiapkan tulisan ini saya mengukur ulang, lalu menemukan sesuatu yang tidak saya duga.

Orama punya tolerance sendiri untuk salah ketik. Sudah saya pasang, sudah ada test-nya, dan test itu hijau.

Yang tak pernah saya uji: apakah toleransi Orama menangani jenis kesalahan yang sama.

monochrome     benar                      → KETEMU
monochorme     huruf tertukar (r <-> o)   → tidak ketemu
monochrone     satu huruf salah           → KETEMU
monochrom      satu huruf hilang          → KETEMU
monochromee    satu huruf lebih           → KETEMU

Fuzzy search milik Orama memakai Levenshtein biasa. Memaafkan tiga jenis kesalahan, menolak yang keempat, yang paling sering terjadi.

Sistemnya jadi timpang tanpa ada yang menyadari.

Kata yang ada di judul produk memaafkan monochorme, karena itu jalur kode sendiri. Kata yang sama, kalau hanya ada di badan tulisan, tidak dimaafkan, karena di situ Orama yang bekerja.

Dua perilaku berbeda untuk satu salah ketik. Dan yang menentukan bukan apa yang kamu ketik, tapi di bagian mana kata itu kebetulan tertulis.

Test-nya hijau selama berminggu-minggu, karena test hanya menguji apa yang terpikir untuk diuji.

Perbaikannya tidak mengganti mesinnya, cuma mengembalikan hurufnya ke tempat semula sebelum bertanya lagi:

/** Every way the query reads if one adjacent pair were typed the other way round. */
function swappedPairs(query: string): string[] {
  if (query.length < MIN_TYPO_TOLERANCE_LENGTH || query.length > MAX_TRANSPOSITION_LENGTH) return [];

  const variants: string[] = [];
  for (let at = 0; at + 1 < query.length; at += 1) {
    const left = query[at] ?? "";
    const right = query[at + 1] ?? "";
    // Swapping across a space rewrites two words instead of repairing one, and
    // swapping a letter with its twin gives the query back unchanged.
    if (left === right || /\s/.test(left) || /\s/.test(right)) continue;
    variants.push(query.slice(0, at) + right + left + query.slice(at + 2));
  }
  return variants;
}

Setiap varian berarti satu pencarian tambahan, jadi ini hanya menyala ketika kueri aslinya tidak menemukan apa pun. Satu-satunya keadaan yang memang jadi alasannya ada.

Kueri yang benar tetap satu pencarian, persis seperti sebelumnya.

Batas atasnya juga ditutup. Di atas dua puluh empat karakter, kegagalan bukan lagi selip dua jari, tapi kalimat yang di-paste. Memperbaikinya cuma membuang pencarian.

Terukur di korpus asli:

Kueri Hasil Waktu Pencarian
spotlight (benar) 7 dokumen 0,79 ms 1
monochorme 17 dokumen (dari 0) 2,18 ms 8
az-zumra 2 dokumen (dari 0) 1,08 ms 8
24 huruf, tanpa hasil 0 dokumen 1,87 ms 24

Kasus terburuknya justru cepat, karena pencarian yang gagal tidak punya apa pun untuk diskor.

Yang saya bawa dari sini bukan soal algoritma jarak edit.

Ketika kamu menyerahkan satu bagian ke pustaka orang lain, kamu bukan cuma mewarisi kemampuannya. Kamu juga mewarisi definisinya tentang apa yang dianggap "cukup dekat".

Dan definisi itu tidak wajib sama dengan definisimu.


8. 110 ms yang Dicicil

Membangun indeks Orama untuk 172 entri memakan waktu median 110 milidetik dalam satu panggilan.

Komentar di kode masih menyebut 116 ms, angka dari pengukuran yang lebih dulu, sebelum korpusnya bertambah dan berkurang beberapa kali. Selisih enam milidetik itu tidak mengubah apa pun; yang penting ordenya, dan ordenya seratusan.

Dan insertMultiple di Orama v3 sepenuhnya sinkron. Argumen batchSize-nya tidak lagi melepaskan event loop, apa pun yang masih tertulis di dokumentasi.

Seratus sepuluh milidetik sinkron berarti seratus sepuluh milidetik antarmuka membeku.

Di desktop terasa seperti tersandung. Di ponsel menengah, angka itu bisa berlipat.

Jadi indeksnya tidak dibangun sekaligus:

/**
 * Small enough that one chunk stays well inside a frame on a mid-range phone.
 * The whole corpus measured ~116 ms in one blocking call; split this way it
 * never holds the main thread long enough to be felt.
 */
const CHUNK_SIZE = 12;

Dua belas entri per requestIdleCallback, dengan timeout: 200 supaya browser yang sedang sibuk tetap dipaksa maju.

Kursornya disimpan di ref, bukan di state. Menutup Spotlight di tengah pembangunan menyimpan apa yang sudah masuk, dan pembukaan berikutnya melanjutkan, bukan mengulang.

Selama pencicilan berjalan, hasil metadata sudah menjawab dari memori sejak ketikan pertama. Hasil dari badan tulisan menyusul begitu potongannya mendarat.

Tidak ada yang menunggu.

Satu hal harus saya sebut apa adanya: 110 ms itu terukur di mesin saya, bukan di ponsel.

Pencicilan ini dirancang supaya tidak pernah terasa di perangkat lambat, dan alasannya masuk akal.

Tapi rancangan yang masuk akal bukan bukti. Sampai saya mengukurnya di perangkat sungguhan, bagian ini tetap asumsi yang kebetulan belum terbantah.


9. Teksnya Dibagi, Mesinnya Tidak

Window Notes dan window Store juga punya kotak pencarian sendiri, dan keduanya butuh teks yang sama.

Tapi keduanya tidak butuh peringkat.

Notes punya urutan tanggal; Store punya kontrol sortir A–Z dan harga. Skor relevansi di situ bukan bantuan, tapi lawan dari kontrol yang sudah dipilih pembaca.

Yang mereka perlukan cuma menyaring: tampilkan yang cocok, biarkan urutannya seperti yang diminta.

Jadi artefaknya dipakai bersama, mesinnya tidak:

                 artefak 164 KB (satu unduhan)
                    ├── Spotlight  → Orama (18,6 KB, await import) → peringkat + cuplikan
                    ├── Notes      → matchesAllWords               → saring saja
                    └── Store      → matchesAllWords               → saring saja

Awalnya tidak begitu.

Saya mengimpor Orama secara statis, lalu menulis di catatan sendiri bahwa mesinnya dimuat malas. Dua hal yang tidak bisa keduanya benar.

Yang membongkarnya bukan ingatan saya, tapi memeriksa isi chunk hasil build: mesin pencarinya ada di dalam bundle desktop, ikut terunduh oleh setiap pengunjung, termasuk yang tak pernah sekali pun membuka Spotlight.

Perbaikannya satu kata:

void import("@/lib/search/fullTextIndex").then((engine) => { ... });

Pelajarannya bukan soal await import.

Pelajarannya: saya sempat mempercayai catatan saya sendiri tentang apa yang sudah dikerjakan, padahal artefak build ada di depan mata dan bisa langsung dibuka.


10. Dua Bug yang Mengajari Sesuatu

Yang pertama adalah kata yang membuka tulisan ini.

az-zumar tidak ketemu, dan setelah ditelusuri, penyebabnya ternyata dua. Berdiri sendiri-sendiri, di fungsi yang sama.

Fungsi stripMdx mengubah MDX jadi teks polos sebelum disimpan. Baris pertamanya membuang seluruh blok yang diawali :::, karena penanda itu memang bukan prosa.

Tapi yang terbuang bukan penandanya. Yang terbuang seluruh isinya.

Setiap terjemahan ayat, setiap nama perawi, setiap sumber riwayat lenyap dari teks polos. Jadi tidak pernah ada di indeks mana pun.

Penyebab kedua lebih halus dan lebih menjengkelkan:

.replace(/[#>*_`-]/g, " ")

Tanda hubung ikut di dalam kelas karakter itu, jadi tanda hubung dibuang dari mana saja. Az-Zumar tersimpan sebagai Az Zumar, Al-Albani jadi Al Albani.

Kata-katanya sebenarnya ada, hanya bisa ditemukan kalau kamu kebetulan mengetiknya tanpa tanda hubung. Dan tidak ada yang mengetiknya begitu, karena bukan begitu cara menulisnya.

Yang menarik: memperbaiki satu penyebab tidak menyembuhkan apa pun.

Selama keduanya masih hidup, gejalanya identik. Mudah sekali menyimpulkan perbaikan pertama tidak berhasil, lalu membatalkannya.

Yang kedua tidak menyerang kodenya, tapi menyerang cara saya memeriksa.

Setelah memperbaiki sebuah loop permintaan yang berulang tanpa henti, saya jalankan test end-to-end untuk membuktikannya. Hasilnya: 33 permintaan.

Perbaikannya seolah tidak berpengaruh sama sekali, dan saya nyaris membongkarnya.

Penyebabnya: Playwright di sini berjalan melawan next start, artinya yang diuji adalah hasil build, bukan berkas yang sedang terbuka di editor.

Saya belum build ulang. Selama satu jam saya memburu bug yang sudah tidak ada lagi, di dalam artefak lama yang masih mengandungnya.

Sejak itu urutannya tidak pernah saya lewati: build dulu, baru e2e.

Tanpa itu, yang kamu uji adalah masa lalu.


11. Menjaga Artefak Tetap Jujur

Berkas statis punya satu kelemahan yang jelas: benar hanya sampai isinya berubah.

Artefak ini dibangun ulang lewat revalidatePath ketika konten berubah. Tapi "berubah" perlu didefinisikan dengan hati-hati, karena membangun ulang berarti membatalkan cache untuk semua orang.

Memindahkan produk ke kategori lain adalah perubahan. Menu berubah, URL berubah, sitemap berubah.

Yang tidak berubah: satu huruf pun dari teks yang bisa dicari.

Mula-mula saya memakai pengecekan yang sudah ada untuk hal-hal seperti itu, dan artefaknya dibangun ulang setiap kali produk dipindah. Kerja penuh untuk hasil yang identik byte per byte.

Perbedaannya akhirnya jadi fungsinya sendiri:

function fullTextChanged(event: ContentChangeEvent, bodyFields: string[]) {
  if (event.operation === "noop" || event.operation === "move") return false;
  if (event.operation !== "update") return true;
  return fieldsAffect(event, bodyFields);
}

Empat baris, dan test dari dua arah: mengubah teks harus memicu pembangunan ulang, memindahkan produk tidak boleh.

Arah kedua yang lebih penting, karena kegagalannya tak terlihat. Tak ada yang rusak, hanya cache yang dibuang percuma sepanjang tahun.


Penutup

Saya membuka tulisan ini dengan sebuah kata yang tidak ketemu.

Sekarang ketemu, bersama 74.000 kata lain: dari kutipan ayat sampai deskripsi bahan kaus, dalam waktu di bawah dua milidetik, tanpa satu pun permintaan jaringan setelah unduhan pertama.

Tapi yang paling berubah dari proyek ini bukan angkanya.

Pencarian yang meranking menjawab pertanyaan mana yang paling cocok. Ia berhak menyembunyikan, karena menyembunyikan memang pekerjaannya.

Pencarian yang menemukan menjawab pertanyaan yang lain: apakah ini ada di sini.

Dan pertanyaan itu tidak punya jawaban yang sopan. Ada, atau tidak ada.

Begitu sebuah kotak pencarian pernah bilang "tidak ada" untuk sesuatu yang kamu tahu ada, kamu berhenti mempercayainya.

Bukan cuma untuk kata itu. Untuk semuanya.

Sesudah itu, setiap hasil kosong jadi ambigu: benar tidak ada, atau sistemnya sedang tidak mau memberitahu?

Itu yang sebenarnya dijaga oleh 0,5 lawan 0,2, oleh sembilan varian huruf tertukar, oleh tanda hubung yang dikeluarkan dari kelas karakter.

Bukan hasil yang lebih relevan.

Kotak pencarian yang masih bisa dipercaya ketika ia mengatakan tidak ada.


Sandi Maulana Juhana