kekavigi.xyz

Membuat Versi Lain KBBI

Seperti umumnya kamus, kita dapat mencari definisi suatu kata di KBBI. Namun bagaimana sebaliknya? Dapatkah kita mencari kata yang sesuai dengan deskripsi yang kita tulis? Tidak bisa? Mari kita buat.

Ditulis tanggal oleh A. Keyka Vigiliant. Revisi terakhir pada tanggal . Konten diterbitkan dibawah lisensi CC BY-SA 4.0.


Saya sangat ingin memulai tulisan ini dengan mencurahkan satu paragraf panjang, menjabarkan pengamatan saya tentang kondisi penutur bahasa Indonesia (yang selanjutnya saya singkat sebagai Bahasa). Intinya, ada golongan penutur yang mengeluh tentang kualitas Bahasa; bahwa pembendaraannya kecil, struktur bahasanya terlalu sederhana, dan lain sebagainya. Namun… untuk sebagian besar hal yang saya amati, penyebab mereka memilih mengeluh panjang-lebar tentang Bahasa, adalah karena mereka tidak biasa hidup di lingkungan berbahasa Indonesia, dengan segala ragamnya: baik/benar, lisan/tulisan, baku/lokal/awam, dll. Saya belum ingin terjun langsung ke diskusi panas tersebut, tetapi hanya diam menonton juga membuat hati dan pikiran panas.

Ada baiknya saya berusaha membantu di satu aspek mungil dari pergulatan berbahasa Indonesia: saya ingin orang bisa mencari kata yang pas untuk konsep yang ingin disampaikan. Bayangkan suasana canggung saat banyak orang menunggu dia memikirkan kata yang tepat, putus asa sambil berpikir “apa sih sebutannya”, menjentikkan jari atau semacamnya, dan ayolah… saya yakin Anda juga pernah merasakan hal itu. Mari kita buat program yang dapat menyelesaikan masalah itu.

Jadi… pada intinya kita ingin membuat program (entah bagaimana bentuknya) yang dapat menghasilkan kata-kata yang cocok, dengan deskripsi yang ditulis pengguna. Kita dapat berpikir jauh dengan menerapkan pemrosesan bahasa natural (NLP), membuat vector embedding dari setiap definisi kata yang ada di KBBI, menggabungkannya dengan informasi kemiripan kata (dengan kata lain: sinonimnya), dan melakukan operasi vektor guna mendapatkan hasil-hasil terbaik – atau dalam bahasa yang membuat investor birahi: sebuah AI (kecerdasan buatan) untuk menjawab masalah ini. Namun, hei… ada baiknya kita memulai dengan cara klasik, agar setidaknya kita dapat memastikan ide ini bisa dilakukan. Selain itu, cara klasik ini dapat menjadi dasar (baseline) penilaian untuk setiap perbaikan kita nantinya.

Akhirnya, saya yang membuat program dan Anda hanya membaca. Saya memutuskan membuat program dengan bahasa Python, SQLite untuk basis data, dan Flask untuk tampilan. Alasannya memilih mereka… karena saya tidak tertarik menggunakan sistem/jasa yang muluk-muluk (sebenarnya sebagai alat berlatih agar fasih menggunakan SQL dan membuat back-end, tetapi secara gamblang menyertakan ini terlihat tidak profesional ¯\_(ツ)_/¯ ).

Mari kita mulai membuat program.

Membuat basis data

Sebenarnya, ada banyak data terkait kata yang ditampilkan laman situs KBBI, selain definisi kata tersebut. Terkadang, akan ada atribut tambahan seperti cara pelafalan seperti /meng.gu.na.kan/, induk kata kata (notes adalah induk dari notes tempel), dan pengalihan ke bentuk baku (adzan ke azan). Untuk sebagian besar kasus, kata juga dikelompokkan berdasarkan jenisnya (kata benda, sifat, dll.). Nah untuk lengkapnya, ada baiknya Anda membaca berkas PDF Juknis Pemakaian KBBI Daring dan Petunjuk Teknis mereka. Namun saya tidak ingin membuat salinan KBBI, saya tidak butuh semua informasi itu, sehingga saya tidak perlu membuat basis data yang meniru KBBI. Oleh karena itu, dengan sadar ini mungkin menghasilkan makna yang sedikit berbeda dengan yang diinginkan Pusat Bahasa, saya memutuskan menyusun basis data sederhana seperti berikut:

erDiagram
word ||--o{ defn : punya_definisi
word |o--o{ asota_root : induk_kata
word |o--o| asota_redir : alih_kata
defn }|--o{ type : dengan_jenis

Oke… sedikit contoh terkait konten KBBI. Hampir semua kata (contoh pukul) akan memiliki setidaknya satu definisi. Kata tak-baku umumnya tidak punya definisi (isi entrinya cuma pengalihan ke bentuk kata yang baku). Nah, lalu saya bertindak liberal dengan menganggap semua teks penjelasan yang ada di entri kamus sebagai definisi. Setiap definisi selanjutnya mungkin tidak/banyak memiliki jenis kata (seperti verba dan cakapan). Oiya… kemunculan kata tanpa jenis (seperti sekali pukul) cukup besar, dan itu membuat bingung. Akhirnya saya menganggap semua contoh kalimat atau ungkapan (misalnya pukul dulu, bayar belakang) sebagai suatu kata tersendiri… asalkan dia memiliki setidaknya satu definisi (dalam kasus ini, barang diambil dulu, soal membayar urusan belakang). Lebih lanjut, suatu kata (sekali pukul) dapat berupa anak dari beberapa kata lain (kali dan sekali)… atau suatu kata lain (pukul) juga menampilkan definisi untuk kata tadi. Dalam upaya tidak menjadi gila, saya menganggap kata seperti sekali pukul adalah anak dari induk (root) dari tiga kata: kali, sekali, dan pukul. Lalu, kata mungkin merupakan bentuk tak baku (cinteng) dan dialihkan ke suatu kata lain (centeng). Saya tidak mengurutkan definisi karena informasi itu tidak dibutuhkan.

Untuk definisi kolom-kolom setiap tabel sebenarnya cukup mudah; saya menetapkan ukuran kolom yang besar karena beberapa kali kaget panjang teks yang bisa didapatkan dari proses scraping:

CREATE TABLE word (
    id          INTEGER PRIMARY KEY,
    term        TEXT NOT NULL UNIQUE    CHECK(len(term) <= 500),
    spell       TEXT                    CHECK(len(spell) <= 200),
    principal   BOOLEAN NOT NULL        CHECK(principal IN (0, 1)),
    cached      INTEGER NOT NULL        DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE defn (
    id          INTEGER PRIMARY KEY,
    desc        TEXT NOT NULL           CHECK(len(desc) <= 1000),
    desc_stemmed TEXT NOT NULL          CHECK(len(desc_stemmed) <= 1000)    
);
CREATE TABLE type (
    id          INTEGER PRIMARY KEY,
    code        TEXT NOT NULL UNIQUE    CHECK(len(code) <= 100),
    long        TEXT                    CHECK(len(long) <= 500),
    desc        TEXT                    CHECK(len(desc) <= 1000)
);

SQLAlchemy mempermudah pendefinisian tabel-tabel tersebut, contohnya agar sekalian membuat indeks untuk word.term, membuat agar nilai word.cached tetap diperbarui ketika terjadi perubahan, dan membuat tabel-tabel asosiasi untuk mengurus hubungan many-to-many antar data. Kolom defn.desc_stemmed sebenarnya bisa dihilangkan, karena isi kolom itu hanya berisi teks definisi hasil stemming (penghilangan imbuhan); contohnya mereka bekerja-sama membangun perumahan akan diubah menjadi mereka kerja sama bangun rumah. Kolom defn.desc tidak diindeks, karena saya membuat tabel FTS5 (Full-Text Search) eksternal yang sekaligus akan menjadi metode pencarian-kata kita nantinya:

CREATE VIRTUAL TABLE fts_desc USING
    fts5(id UNINDEXED, desc, desc_stemmed,
         content='defn', tokenize='ascii');

Oh.. ya, pendefinisian tabel FTS harus dilakukan manual (saya membuat class tersendiri). Selain itu, Anda perlu membuat beberapa TRIGGER agar isi tabel tersebut tetap konsisten dengan tabel defn. Dalam keadaan yang menyedihkan, isi tabel menjadi tidak konsisten, ada perintah rebuild yang dapat memperbaiki data dengan, sepengamatan saya, cukup cepat.

Scraping data

Setelah basis data dibuat, mari kita mengisinya. Saya awalnya menganggap ini adalah tahap yang mudah: dengan menggunakan selectolax, sembari mengingat PTSD mengerjakan pratikum matkul Algoritma dan Struktur Data, dan menerapkan aturan-aturan scraping yang sopan (yang intinya, jangan ngebuat situs tujuan menjadi marah atau malah down). Namun keadaan menjadi lebih rumit karena situs KBBI menetapkan batas sekitar 100 pencarian setiap harinya. Bahkan walau saya menjarak waktu antar scraping menjadi 10 menit, saya akan kena ban sebelum hari berganti.

Bukan cara yang elegan dan implementasinya tidak sebagus harapan – anggaplah sebagai skill issue dari pihak saya, tetapi masalah ini dapat diselesaikan dengan menggunakan aiohttp dan [DISENSOR]. Paradigma scraping yang sopan menjadi fokus utama saya, karena… dari pengalaman saya berurusan dengan situs-situs pemerintah, mungkin ada alasan yang memilukan mengapa mereka membatasi total pencarian harian, terlebih dengan batas serendah itu.

Membuat tampilan

Jika ada satu hal yang dapat saya bagikan disini, adalah tulisan oleh Edward Krueger tentang cara menggunakan SQLAlchemy di Flask, tanpa perlu menggunakan abstraksi Flask-SQLAlchemy. Itu adalah bacaan yang bagus, dan kedepannya kalau saya memutuskan program ini langsung menggunakan SQLite tanpa ORM, [seharusnya] saya dapat dengan mudah mengganti beberapa baris kode saja. Namun, saya juga menyesuaikan saran beliau dengan jawaban SE ini, menjadi:

from threading import get_ident

# Ini ada di tempat lain
# engine = create_engine(DATABASE_URL,
#     connect_args={"check_same_thread": False})
# Session = sessionmaker(bind=engine, autocommit=False)

app = Flask(__name__)
app.dbs = scoped_session(Session, scopefunc=get_ident)

@app.before_request
def create_tables():
    app.before_request_funcs[None].remove(create_tables)
    Base.metadata.create_all(bind=engine)

@app.teardown_appcontext
def remove_session(*args, **kwargs):
    app.dbs.remove()

Oke, setelah akses basis data dibuat, tampilan dengan mudah dapat disusun – apalagi bagi saya yang suka desain minimalis. Hal terpenting disini tentunya adalah pencarian menggunakan FTS, dan untungnya ini cukup mudah. Pertama kita dapatkan deskripsi text dari pengguna, lalu men-sanitasi-nya. Dari definisi tabel fts_desc di awal tadi, teks kueri SQL berikut akan menghasilkan semua baris di tabel defn, yang gabungan teks kolom desc dan desc_stemmed-nya, mengandung semua kata di text:

SELECT * FROM fts_desc WHERE fts_desc MATCH :text ORDER BY rank LIMIT 25

Tapi tentu, kita sebenarnya cukup SELECT id ketimbang SELECT *, dan sebenarnya ingin mencari kata-kata di tabel word (misal menggunakan subkueri). Itu mudah. Saat ini saya menulis masih menggunakan ORM-nya SQLAlchemy, tetapi saya mulai menduga teks SQL mentah bisa jauh efisien. Malang saya tidak mengetes dugaan itu… karena mulai merasa lelah berurusan dengan proyek ini.

Hasil

Berikut dua tangkapan layar tampilan program saya

Hasil pencarian dengan deskripsi `lantai yang tinggi` adalah kata `jrambah`, `panggung`, `anjung`, `pentas`, `persada`, `pelantar`, dan lain sebagainya.
"Hasil pencarian dengan deskripsi `lantai yang tinggi` adalah kata `jrambah`, `panggung`, `anjung`, `pentas`, `persada`, `pelantar`, dan lain sebagainya.."
Hasil pencarian dengan deskripsi `omong kosong` adalah kata `rapik`, `kecap`, `nol`, `bual`, `cerita`, `bombas`, `cola-cala`, dan `bombastis`
"Hasil pencarian dengan deskripsi `omong kosong` adalah kata `rapik`, `kecap`, `nol`, `bual`, `cerita`, `bombas`, `cola-cala`, dan `bombastis`."

Scope creep

Scope creep: Dalam manajemen proyek, istilah ini merujuk pada perkembangan cakupan proyek yang terus-menerus (atau lebih mengerikan, yang tidak kendali) dari tujuan proyek mulanya.

Selagi mengembangkan ini, saya berpikir untuk menerapkan spell-checking agar dapat memperbaiki teks pengguna yang saltik. Ini tidak jadi dilakukan, karena saya tidak tahu cara melakukan pencarian spell-checking yang baik. Saran dari SE adalah mengubah text dari pengguna menjadi versi spell-checked, lalu melakukan pencarian FTS dengan versi itu. Namun bagaimana kalau pengguna menulis kata "maw"? apakah sebaiknya menghasilkan pencarian untuk kata "mau" atau "mas" atau keduanya? bagaimana kalau pengguna tersebut memang ingin mencari kata "maw"? lalu bagaimana cara mengurutkan hasil yang didapat? Ugh… oke, alternatif solusi adalah membuat tabel FTS dengan tokenisasi trigram, yang akan mencari substring. Dengan menggunakan contoh, pencarian akan teks "empat" selain menghasilkan kata "empat", juga akan menghasilkan "tempat", "menempatkan", dst. Namun melakukan uji coba kualitatif dengan sampel populasi pengguna sebanyak satu orang, cara ini terbukti signifikan tidak membantu.

Saya juga terpikir untuk menggunakan vector embedding untuk mencari hasil. Namun itu tidak jadi saya terapkan. Ide yang kedua terasa seperti scope creep, karena untuk dapat menghasilkan vektor-vektor embedding yang baik, hanya menggunakan data KBBI saja sepertinya tidak cukup. Saya berpaling ke model Doc2Vec-nya gensim maupun model BERT indo-sentence-bert-base oleh Firqa Aqila. Mungkin karena saya masih terlalu awam dengan kedua model ini, atau data definisi kamus yang spesifik, kedua cara ini membuat hasil pencarian menjadi jauh lebih buruk daripada sekadar SQL MATCH (yang cukup mematahkan semangat). Lagipula, andai itupun bisa dilakukan, saya tidak yakin dengan tingkat performa proses pencarian-di-basis-data Semoga saya salah untuk kedua temuan/anggapan tersebut.

Berbicara tentang scope creep: tahu bahwa Pusat Bahasa juga membuat glosarium padanan istilah asing? Nama glosarium itu adalah PASTI. Saya menemukan suatu cara menggabungkannya dengan KBBI, agar para pengguna langsung dapat mencari istilah asing seperti franchise di KBBI, dan langsung tahu itu adalah padanan dari kata “waralaba”; dan sebaliknya. Kalau definisi dari flora/fauna saja dapat mengandung nama ilmiah dari mereka, kenapa istilah umum tidak dapat mengandung daftar padanan istilah asing mereka? Lagipula, saya juga punya dataset Kaggle yang menyimpan versi lawas glosarium tersebut. Untungnya, tidak seperti dua paragraf sebelumnya, saya langsung tersadar bahwa fitur ini bukanlah tujuan awal proyek.

Karena tema subjudul ini adalah scope creep, saya sebenarnya juga ingin membuat permainan seperti permainan Wikipedia-itu. Itu loh… yang seorang pengguna perlu mencapai suatu halaman dari halaman lain, contohnya mencapai halaman “Keong mas” dari halaman “Saturnus”, dengan mengeklik pranala-pranala yang ada di artikel, contohnya: Saturnus → Planet → Bumi → Binatang → … → Keong mas. Dalam permainan itu, semakin sedikit artikel yang perlu ditempuh untuk mencapai halaman tujuan, Anda dianggap semakin hebat. Oke, sekarang KBBI punya kata, dan [hampir] setiap kata mengandung definisi yang terdiri dari kumpulan kata. Anda lihat hubungannya? Saya juga terpikir untuk membuat permainan tebak kata: diberikan satu definisi dan beberapa pilihan kata, cari kata yang memiliki definisi tersebut. Ini permainan yang pernah saya buat sekitar satu semester yang lalu, tetapi tidak dilanjutkan karena sama sekali tidak ada data. Apakah saya perlu membuatnya? Atau sekalian memadukannya dengan program ini? Ugh, scope creep.

Akhir kata

Apakah Anda merasa semua yang di atas tadi adalah skill issue saya, dan menganggap itu dapat diterapkan? atau setiap scope-creep tadi sebenarnya bisa dikembangkan menjadi proyeknya masing-masing? Silahkan berdiskusi dengan saya jika Anda mau data yang telah saya kumpulkan (ketimbang perlu scraping ulang).

Ketika memulai proyek ini, saya sudah punya firasat banyaknya unit-testing yang perlu dibuat – terlebih karena saya tidak ingin ada kesalahan dan harus men-scrape KBBI dua kali. Dugaan sedikit meleset karena pada kenyataannya, ada lebih banyak tes yang perlu dibuat. Saya melihat ini kesempatan yang bagus untuk belajar cara mengetes Flask maupun fungsi-fungsi async. Namun ini belum saya lakukan karena masih belum yakin dengan kode program saat ini – tidak banyak gunanya membuat banyak tes untuk kode yang kemungkinan besar bakal di-refaktor, atau malah dihapus, esok hari.

Ini ide lawas sejak tahun 2018-2019, dan baru terwujud sekarang, setengah dekade setelahnya. Tidak akan ada perkembangan jika hanya berharap Pusat Bahasa akan sukarela membagikan data kamus mereka. Jadilah perubahan yang kamu inginkan, setidaknya itu kata seorang di 4chan. Saya ingin membuat nyata ide ini, dan ide ini akhirnya menjadi nyata. Walau tidak punya definisi kata sebanyak yang KBBI punya, program ini dapat saya gunakan untuk mencari kata-kata yang pas, khususnya ketika kata itu “sudah sampai di mulut tapi ngga keluar” atau “argh.. apa sih bahasanya aku lupa”. Kualitasnya tidak sebaik harapan awal (yang terlalu tinggi, sejujurnya), tetapi juga tidak buruk.

Setidaknya saya bisa senang dan lega… saat ini.