# RAG Bahasa Indonesia: Full-Text Search Postgres Sering Meleset

> Postgres 16 punya text search config indonesian yang menyimpan semua stop word dan salah men-stem kata berawalan me-. Hasil ukur di 16.11, plus SQL perbaikannya.

URL: https://www.matthewswong.com/id/blog/rag-bahasa-indonesia-search-retrieval/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2026-09-18
- Updated: 2026-09-18
- Category: AI

> **Key takeaway:** PostgreSQL 16 menyediakan text search configuration indonesian, tetapi tidak mendaftarkan stop word list apa pun dan Snowball stemmer-nya tidak mengembalikan asimilasi nasal bahasa Indonesia. Mencari terima tidak menemukan dokumen berisi menerima. Pasang stop word list Indonesia dari Snowball, uji setiap keluarga imbuhan yang dipakai domain Anda, dan padukan lexical search dengan multilingual embedding model.

Sebuah keyword query yang seharusnya cocok malah tidak mengembalikan apa pun. Frasanya pembayaran yang belum diterima, sedangkan dokumen tersimpan berbunyi pembayaran belum diterima dari pelanggan. Kata-katanya sama, hanya satu kata sambung yang hilang. Postgres full-text search, configuration indonesian, tidak ada hasil. Pada 18 September 2026 saya membuka psql di PostgreSQL 16.11 dan menghabiskan satu sore membongkar configuration itu.

Semua di bawah ini saya ukur sendiri di instance tersebut dan setiap barisnya bisa Anda ulang dengan psql. Temuannya bukan bahwa Postgres tidak mendukung bahasa Indonesia, justru sebaliknya, dan di situlah masalahnya. Configuration-nya ada, terlihat benar di catalogue, tetapi diam-diam meng-index semua stop word dan salah men-stem banyak kata kerja yang justru paling sering dicari.

## Postgres 16 memang menyediakan configuration Indonesia

Di titik inilah kebanyakan tulisan berhenti, dan itu titik yang salah. PostgreSQL 16.11 membawa 29 text search configuration dan indonesian salah satunya, bersama arabic, hindi, nepali dan tamil. to_tsvector dengan configuration indonesian jalan, GIN index di atasnya terbentuk, EXPLAIN menunjukkan index itu terpakai, dan tidak ada satu pun peringatan. Jadi Anda mencentang dukungan bahasa Indonesia lalu lanjut, persis seperti yang saya lakukan.

Perbedaannya dengan English bersembunyi di satu kolom pg_ts_dict yang tidak pernah dibaca siapa pun. english_stem dibuat dengan parameter language dan parameter stopwords. indonesian_stem dibuat dengan parameter language saja. Ketiadaan itu berpasangan dengan ketiadaan di disk: tidak ada file indonesian.stop di direktori tsearch_data, jadi parameter yang hilang itu pun tidak punya sasaran seandainya Anda menambahkannya.

## Kata yang, dan, di ikut ter-index karena stop list-nya tidak ada

Kalimat bahasa Indonesia sepanjang tiga belas kata menghasilkan tiga belas lexeme. Kalimat pembanding berbahasa Inggris sepanjang sembilan belas kata menghasilkan delapan. Tidak ada satu pun yang dibuang dari sisi Indonesia, tidak yang, tidak dan, tidak di, tidak dari, tidak belum. Jalankan keduanya berdampingan dan ketimpangannya langsung terlihat, tetapi hanya kalau Anda terpikir untuk menjalankannya, dan tidak ada apa pun di tooling yang menyarankan itu.

Ini soal file yang tidak ada, bukan soal preferensi tuning. Postgres memasang lima belas stop word list di tsearch_data: danish, dutch, english, finnish, french, german, hungarian, italian, nepali, norwegian, portuguese, russian, spanish, swedish dan turkish. english.stop berisi 127 kata. Bahasa Indonesia tidak kebagian, sehingga kata yang, yang muncul di sebagian besar kalimat, berakhir dengan posting list sebesar tabel Anda, dan setiap ranking function harus menimbangnya seperti term biasa.

## Kerusakan sebenarnya ada di query, bukan di index

Menyimpan stop word di index memakan storage. Menyimpannya di query memakan recall, dan recall itulah yang sampai ke Anda sebagai laporan bug. plainto_tsquery menyambung term yang diambilnya dengan AND, jadi setiap kata fungsi yang kebetulan diketik pengguna berubah menjadi term yang wajib ada di dokumen.

> Semakin panjang dan semakin wajar query bahasa Indonesia-nya, semakin rendah recall-nya. Itu kebalikan persis dari yang diajarkan test suite berbahasa Inggris, dan itu sebabnya keluhannya selalu berbentuk sama: pencarian jalan kalau saya ketik dua kata, dan berhenti jalan kalau saya ketik satu kalimat.

Empat term wajib, dua di antaranya tanpa makna. Pembanding bahasa Inggrisnya menghasilkan dua. Dan dokumen yang menulis kalimat sama tanpa kata yang, yang dalam bahasa Indonesia soal ragam bahasa bukan soal makna, sama sekali tidak bisa cocok. Pasangan itu saya ukur langsung: versi dengan yang cocok, versi tanpa yang tidak, dan tidak ada bagian sistem yang menganggap itu aneh.

## Stemmer membuang me- tanpa mengembalikan konsonan yang ditelannya

Bahasa Indonesia membentuk kata kerja dengan awalan me-, dan awalan itu berasimilasi dengan bunyi pertama kata dasarnya lalu menyerapnya. me- plus tulis menjadi menulis, huruf t ditelan oleh n. me- plus kirim menjadi mengirim, huruf k lebur ke ng. me- plus masak menjadi memasak. Bagi pembaca konsonan dasarnya tidak hilang, ia tersimpan di bunyi nasal, dan setiap penutur membongkarnya tanpa perlu berpikir.

Algoritma Indonesia milik Snowball, yang dipakai Postgres, adalah implementasi stemmer bergaya Porter untuk bahasa Indonesia yang dijelaskan dalam tesis Fadillah Z Tala tahun 2003. Ia membuang awalan, termasuk varian men, meng, meny dan mem, tetapi tidak mengembalikan konsonan yang tertelan. Jadi menerima kehilangan men lalu menjadi erima, sementara terima sendiri salah dibaca sebagai awalan ter- plus kata dasar ima. Satu keluarga kata pecah menjadi tiga lexeme terpisah: terima menghasilkan ima, diterima menghasilkan terima, dan menerima serta penerimaan sama-sama menghasilkan erima.

Tidak

Tidak

Tidak

Tidak

Ya

Ya

Ya

Perhatikan lagi baris memasak. Stem-nya bukan sekadar salah, melainkan pasak, sebuah kata Indonesia sungguhan yang berarti patok kayu. Saya cek dua arah: query pasak cocok dengan dokumen tentang memasak nasi, dan query memasak cocok dengan dokumen tentang memasang pasak kayu. Itu bukan meleset tipis, itu tabrakan diam-diam antara dua topik yang tidak berhubungan, dan tidak ada log yang akan menyebutnya.

## Bukan rusak, tetapi benar secara tidak merata, dan itu lebih buruk

Tiga dari tujuh kata kerja yang saya uji ter-stem dengan benar. menyimpan, memproses dan mencatat semuanya kembali ke kata dasarnya. Keluarga bayar bahkan sempurna: bayar, membayar, pembayaran, dibayar, membayarkan dan pembayar semuanya menghasilkan satu lexeme bayar, persis seperti yang Anda harapkan dari sebuah stemmer.

Itulah yang membuat masalah ini mahal. Uji sekilas dengan bayar, simpulkan stemmer-nya benar, lalu rilis. Secara end to end, mencari kata dasar gagal menemukan dokumen berimbuhan dua kali dari lima percobaan. kirim cocok dengan dokumen berisi dikirim dan gagal pada dokumen berisi mengirim, kata dasar sama dengan dua awalan, hanya satu yang selamat. Tidak ada error, tidak ada baris log, tidak ada metric yang bergerak. Satu-satunya sinyal adalah pengguna yang bilang pencariannya jelek, berbulan-bulan kemudian.

## Perbaiki stop word lebih dulu, karena Snowball sudah menerbitkan daftarnya

Halaman Indonesia milik Snowball menerbitkan stop word list. Postgres saja yang tidak pernah memasangnya. File terbitannya 91 baris dan setelah diratakan menjadi 93 kata unik, dan memasukkannya ke tsearch_data bersama sebuah dictionary yang merujuknya memakan waktu di bawah dua puluh menit. Kalimat uji saya turun dari tiga belas lexeme menjadi delapan, query empat term menyusut jadi dua term, dan dokumen yang tadinya hilang mulai muncul.

> Masukkan juga noise dari corpus Anda sendiri ke file yang sama. Kalau sebuah kata muncul di hampir setiap record yang Anda index, misalnya judul kolom seperti nomor atau keterangan yang ikut tercetak di setiap dokumen hasil export, kata itu memakan ruang index dan mengencerkan ranking persis seperti kata yang. Hitung dulu document frequency-nya, jangan menebak isi daftarnya.

Dua jebakan sebelum Anda menyalin file itu. File-nya beranotasi, tiap baris berisi kata, tab, tanda pipa dan terjemahan Inggrisnya, dan beberapa baris memuat dua kata. Postgres ingin satu kata polos per baris dan tidak melakukan pemrosesan lain, jadi buang dulu komentarnya dan pecah pasangannya. Kedua, daftar itu belum lengkap untuk teks bisnis: ada telah tetapi tidak ada sudah, padahal sudah muncul terus-menerus di tulisan operasional berbahasa Indonesia. Tambahkan kata Anda sendiri dan simpan file-nya di version control.

## Yang tidak diperbaiki stop list, dan tiga jalan keluarnya

Stemmer-nya sama sekali tidak tersentuh. menerima tetap menghasilkan erima setelah perbaikan itu, dan akan selalu begitu, karena stop list dan stemmer adalah bagian dictionary yang terpisah. Tersisa tiga pilihan dan tidak ada yang gratis.

Turun ke configuration simple lalu normalisasi teksnya sendiri: tanpa stemming sama sekali, plus aturan awalan buatan Anda. Tahan godaan memakai regex. Membuang awalan me, mem, men, meng atau meny dengan sebuah pola mengubah meja menjadi ja, dan tetap mengubah mengirim menjadi irim. Sebuah pola tidak bisa membedakan kata kerja dari kata benda, sedangkan stemmer bawaan setidaknya benar untuk meja.

Tambahkan pg_trgm sebagai jaring fuzzy. Hasil pengukuran, similarity antara terima dan menerima adalah 0.3333, melewati ambang default 0.3 yang dilaporkan show_limit, jadi operator persen menemukannya. Antara terima dan penerimaan nilainya 0.2 dan tidak lolos. Trigram memulihkan sebagian morfologi bahasa Indonesia secara kebetulan, bukan karena dirancang begitu, dan lebih cocok untuk field pendek daripada isi dokumen.

Terima saja bahwa sisi lexical memang lemah secara struktural untuk bahasa Indonesia, lalu beri bobot lebih besar ke sisi dense saat Anda menggabungkan dua result set. Ini jawaban yang jujur, bukan yang pintar, dan ke sanalah sisa tulisan ini menuju.

Di sistem production saya akan menjalankan ketiganya, dengan urutan usaha seperti itu: stop list di hari pertama, trigram index pada field pendek yang benar-benar diketik orang, dan bobot fusion yang tidak berpura-pura bahwa ts_rank atas teks Indonesia sama bisa dipercayanya dengan ts_rank atas teks Inggris. Yang ketiga memang penilaian subjektif, dan menuliskannya sebagai angka di config lebih baik daripada membiarkannya tersirat.

## Hybrid retrieval, dan apa yang sebenarnya dikatakan angka embedding

SEA-BED adalah benchmark embedding untuk Asia Tenggara, 169 dataset di 10 bahasa dan 9 jenis task. Di Tabel 2 paper SEA-Embedding, kolom Indonesia berbunyi: SEA-Embedding-E5-Large-600M 0.808, Cohere-embed-multilingual-v3.0 0.797, multilingual-e5-large-instruct 0.795, bge-m3 dense 0.781, Qwen3-Embedding-0.6B 0.756. Itu skor rata-rata per bahasa di seluruh sembilan jenis task, bukan skor retrieval saja, dan mengutipnya sebagai angka retrieval jelas keliru.

Baca sebaran angkanya, bukan peringkatnya. Untuk bahasa Indonesia jarak antara model terkuat dan terlemah di daftar itu sekitar lima poin. Kolom Lao di tabel yang sama terentang dari 0.841 sampai 0.298. Bahasa Indonesia termasuk yang paling terlayani di setiap multilingual encoder serius, dan itu memberi tahu ke mana recall Anda sebenarnya pergi: bukan ke pilihan embedding, melainkan ke sisi lexical yang Anda konfigurasi dalam sepuluh detik. MMTEB, yang mencakup lebih dari 500 task di lebih dari 250 bahasa, menunjuk arah sama dengan menyebut multilingual-e5-large-instruct berukuran 560 juta parameter sebagai model publik terbaiknya, di atas model berukuran miliaran parameter.

Satu hal yang tidak saya benchmark dan tidak akan saya klaim: chunk size. Imbuhan bahasa Indonesia membuat kata lebih panjang dalam jumlah karakter dibanding padanan Inggrisnya, sehingga chunker yang memotong berdasarkan jatah karakter hasil tuning bahasa Inggris akan mendarat di tempat berbeda relatif terhadap batas kalimat. Ukur panjang chunk Anda sendiri dengan tokenizer Anda sendiri sebelum memakai ulang default bahasa Inggris, dan anggap angka chunk size apa pun yang disodorkan tanpa menyebut tokenizer-nya sebagai hiasan belaka.

Mengaktifkan text search configuration indonesian tidak sama dengan mendukung pencarian bahasa Indonesia, dan selisihnya tidak terlihat dari catalogue. Pasang stop word list sebelum Anda meng-index apa pun. Uji setiap keluarga imbuhan yang benar-benar dipakai domain Anda, bukan satu yang kebetulan berhasil. Perlakukan Postgres full-text search sebagai sisi yang lebih lemah dari hybrid retriever bahasa Indonesia, bukan sisi yang Anda tuning. Kalau hanya satu yang sempat dikerjakan, kerjakan stop list-nya: dua puluh menit, dan recall-nya naik di hari yang sama.

## Sumber

- Dokumentasi PostgreSQL 16, Text Search Dictionaries dan stop word
- Snowball, algoritma stemming bahasa Indonesia dan stop word list terbitannya
- Dokumentasi PostgreSQL 16, pencocokan trigram pg_trgm dan similarity
- SEA-Embedding: Open and Reproducible Text Embeddings for Southeast Asia, arXiv 2606.03027
- MMTEB: Massive Multilingual Text Embedding Benchmark, arXiv 2502.13595

## Source links

- https://www.postgresql.org/docs/16/textsearch-dictionaries.html
- https://snowballstem.org/algorithms/indonesian/stemmer.html
- https://www.postgresql.org/docs/16/pgtrgm.html
- https://arxiv.org/abs/2606.03027
- https://arxiv.org/abs/2502.13595

## FAQ

### Apakah PostgreSQL mendukung full-text search bahasa Indonesia?

Ya. PostgreSQL 16 membawa 29 text search configuration dan indonesian salah satunya, memakai Snowball stemmer bahasa Indonesia. Dukungannya nyata tetapi belum lengkap: configuration itu tidak mendaftarkan stop word list, sehingga setiap yang, dan, di dan dari ikut ter-index lalu menjadi term wajib di query Anda.

### Kenapa mencari terima tidak menemukan dokumen berisi menerima?

Awalan me- dalam bahasa Indonesia berasimilasi dan menyerap konsonan pertama kata dasarnya, sehingga menerima menyembunyikan huruf t dari terima di dalam bunyi n. Snowball stemmer membuang awalan tanpa mengembalikan konsonan itu sehingga menghasilkan erima, sedangkan terima sendiri salah dibaca sebagai ter- plus ima. Kedua bentuk berakhir sebagai lexeme berbeda dan tidak pernah cocok.

### Bagaimana cara menambahkan stop word bahasa Indonesia di PostgreSQL?

Snowball menerbitkan stop word list bahasa Indonesia yang tidak dipasang Postgres. Ratakan menjadi satu kata polos per baris, simpan sebagai indonesian.stop di direktori tsearch_data yang ditunjukkan pg_config --sharedir, lalu buat dictionary snowball dengan StopWords bernilai indonesian dan petakan configuration salinan ke dictionary itu. Tambahkan juga kata noise dari corpus Anda sendiri.

### Embedding model apa yang paling baik untuk retrieval bahasa Indonesia?

Di SEA-BED, kolom Indonesia pada Tabel 2 paper SEA-Embedding mencatat SEA-Embedding-E5-Large-600M 0.808, Cohere-embed-multilingual-v3.0 0.797, multilingual-e5-large-instruct 0.795 dan bge-m3 dense 0.781. Itu rata-rata per bahasa di sembilan jenis task, bukan skor retrieval saja. Sebarannya untuk bahasa Indonesia sempit, jadi sisi lexical retriever Anda lebih menentukan daripada pilihan model.

### Apakah Postgres full-text search cukup untuk pipeline RAG bahasa Indonesia?

Tidak kalau berdiri sendiri. Bahkan setelah stop word list dipasang, stemmer-nya masih salah menangani beberapa kata kerja berawalan me- yang umum, dan dalam pengujian saya query kata dasar gagal menemukan dokumen berimbuhan dua kali dari lima percobaan. Pakai sebagai sisi yang lebih lemah dari hybrid retriever, dengan multilingual embedding model memikul bobot lebih besar.

---

- HTML version: https://www.matthewswong.com/id/blog/rag-bahasa-indonesia-search-retrieval/
- English: https://www.matthewswong.com/en/blog/rag-bahasa-indonesia-search-retrieval/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
