Pagi Itu, Tool Riset Saya Menelan Obatnya Sendiri
Baris pruned di build pagi mengungkap dua hal: index vektor tanpa pruning menjawab pakai dokumen yang sudah tidak ada, dan format bebas itu kontrak implisit.
Ringkasan
Gara-gara rename file dossier lupa update index vektor, query malah ngasih dokumen hantu yang udah nggak ada. Fix-nya cuma 64 baris tapi ada tiga lapis: baca field ringkasan/summary yang toleran, pruning otomatis file hilang, sama selftest yang sengaja dibikin gagal. Intinya index jadi jujur lagi dan nggak percaya sama dokumen yang cuma ada di JSON doang.
Baris yang bikin saya berhenti ngopi pagi ini cuma satu, dari output build pengindeks riset: pruned (file hilang/renamed). Satu entri di index vektor dibuang karena filenya ternyata sudah nggak ada. Saya rename file dossier risetnya semalam, dan lupa satu hal: file JSON yang nyimpen embedding-nya nggak ikutan tahu. Tool yang saya tulis sendiri buat kebutuhan saya sendiri baru saja menangkap bumerang kerja saya kemarin.
Dugaan pertama saya remeh banget. Alias field frontmatter? Fix kecil. Pruning entri index? Sekadar housekeeping biar file JSON nggak gendut. Dugaan itu bertahan sampai saya coba bayangin kondisi tanpa pruning: query pencarian riset tetap mengembalikan dossier yang sudah di-rename, skor cosine-nya masih tinggi, dan knowledge check di cron artikel percaya bahwa dossier itu masih hidup. Index vektor tanpa pruning nggak cuma boros. Dia menjawab pakai dokumen yang secara fisik udah nggak ada. Jadi ini soal kebenaran hasil query, bukan kerapian file.
Commit perbaikannya cuma +64/-4 baris di tools/research-knowledge.py, tapi isinya tiga lapisan pertahanan yang setelah kejadian ini saya anggap wajib buat tool kecil apa pun: pembacaan toleran di batas baca, kebersihan index, dan tes yang bisa membuktikan dirinya bohong.
Pembacaan Toleran di Satu Batas Baca
Biang keroknya biasa aja. Dossier riset lama saya tulis lewat agen dengan konvensi Indonesia, kunci ringkasan:. Yang baru bahasa Inggris, kunci summary:. Dua penulis, dua konvensi, padahal orangnya sama. Ini kejadian umum, dan [2] Hyrum's Law merangkum kenapa ini nggak bisa dihindari: dengan cukup banyak pengguna, semua perilaku yang bisa diobservasi pasti diandalkan seseorang, seberapa pun janji di kontraknya. Di skala saya, "seseorang" itu ya diri saya sendiri tiga bulan lagi, bingung kenapa ringkasannya kosong.
Solusinya satu fungsi summary_field() jadi satu-satunya pintu baca field ringkasan, dengan fallback berlapis:
# satu batas baca: semua pemanggil lewat sini, nggak ada yang baca frontmatter mentah
FIELD_ALIASES = {"ringkasan": ("ringkasan", "summary")}
def summary_field(path):
for name in FIELD_ALIASES["ringkasan"]:
value = frontmatter_field(path, name)
if value:
return value
return "" # kosong pun non-fatal: fallback L0 ambil kalimat inti pertama
Kebalikannya adalah parser ketat yang nyandung sama skema. Fowler nulis pola gagalnya di Tolerant Reader: binding berbasis skema pecah justru saat penyedia data nambah field yang seharusnya nggak breaking, dan [1] rekomendasinya jadilah setoleran mungkin saat membaca data, ambil yang dibutuhkan, abaikan sisanya. Postel's Law versi data lokal. Bonusnya, alias yang dikonsolidasi di satu fungsi bikin perubahan berikutnya cukup nyentuh satu tempat, bukan digerek di seluruh kode.
Dokumen Hantu dan Beban yang Dipikul Sendiri
Lapisan kedua soal entri mati. Pakai vector database sungguhan, masalah ini ditangani enginenya. [3] Qdrant implement penghapusan sebagai soft delete pakai bitmask, index nggak perlu di-rebuild tiap ada delete, dan poin yang dihapus langsung nggak bisa diakses lewat API. [4] Pinecone milih jalur beda: upsert dengan ID yang sudah ada menimpa seluruh record. Setup saya jauh lebih merakyat: satu file JSON berisi daftar vektor, cosine similarity dihitung pas query, tanpa database sama sekali. Konsekuensinya jelas, nggak ada engine yang bisa disalahkan, jadi lapisan build yang harus pikul.
Jadinya cmd_build sekarang ngecek tiap entri sebelum dipakai: filenya masih ada nggak? Nggak ada, buang, dan print barisnya ke output. Baris pagi tadi itu. Enam baris inti, dan justru enam baris itu yang bikin index saya jujur lagi.
Selftest yang Sengaja Dibikin Salah
Lapisan ketiga menurut saya yang paling sering dilewatin orang: test suite buat tool yang cuma dipakai sendiri. Commit ini nambahin subperintah selftest dengan fixture kecil. Alias harus ketangkap. Field kosong harus jatuh ke fallback, bukan crash. Seksi Findings harus kebaca buat L0 fallback. Bagian yang paling saya suka justru assertion sengaja salahnya: baris yang membandingkan hasil dengan string yang jelas beda, dan tesnya harus bisa gagal kalau implementasinya rusak. Ini versi mini prinsip mutation testing. Alat ukur yang nggak mungkin salah itu alat ukur yang rusak, dan suite yang selalu hijau bukan bukti kode benar, cuma bukti tesnya nggak ngecek apa-apa.
Opini saya sekarang tegas: data lokal berformat bebas itu janji palsu. Format yang "cukup jelas buat saya" berubah jadi kontrak implisit begitu ada dua penulis, dua bulan, dua konvensi. Saya milih bayar 64 baris sekarang daripada debugging knowledge check yang percaya sama dokumen hantu enam bulan lagi. Besok pagi kalau ada rename lagi, baris pruned itu bakal muncul lagi, dan itu bukan error. Itu tool-nya ngaku nggak sempurna, persis seperti kodenya yang bisa saya percaya.