README
¶

Bacot
Filter kata kasar untuk Bahasa Indonesia.
b.Text("4njiiing").WithLeetSpeak().Scan().IsProfane() // true
b.Text("babi dan anjing").Collect(true).Scan().Censor() // "**** dan ******"
Bahasa Indonesia itu aglutinatif. babi bisa jadi mebabi, pebabi, dibabi, kebabi, memperbabikan. Coba lo handle pake strings.Contains sendirian — good luck, you'll need it.
Bacot lahir karena capek liat developer nulis regex panjang 20 baris cuma buat deteksi "4njiiing". Kami sudah melalui masa gelap itu (bohong). Kami tidak akan kembali (bohong lagi).
Tidak klise pake regex aja
| Masalah | Contoh | Regex | Bacot |
|---|---|---|---|
| Imbuhan | mebabi, penganjing |
lo harus bikin pattern buat tiap imbuhan | otomatis |
| Leet speak | 4njing, 8abi, k0nt0l |
[4a][nN]... — capek? |
built-in |
| Unstack | anjiiiiing |
anj+ing — gampang sih, tapi itu baru 1 kata |
otomatis |
| False positive | babiru bukan babi |
lo bikin pengecualian? ya nulis lagi | udah di-handle |
| 10Kb censor | 4000 kata kasar | 5-20ms, rerata CPU merintih | 172µs, CPU nggak sadar |
Instalasi
import bacot "github.com/yumiodd/bacot/src"
Nggak ada go get lagi ini itu. ga ada requirements.txt instalasi misterius. Zero dep (untuk sekarang)
kami bukan python
Quick Start
b := bacot.New()
// Cek — biar tau
b.Text("4njiiing").WithLeetSpeak().Scan().IsProfane() // true
// Sensor — biar aman
b.Text("babi dan anjing").Collect(true).Scan().Censor() // "**** dan ******"
// Hitung — biar puas
res := b.Text("babi dan anjing asu kontol").Collect(true).Scan()
res.Count() // 4
res.Extract() // ["babi", "anjing", "asu", "kontol"]
res.First() // "babi"
res.Last() // "kontol"
New()itu bikin dictionary + generate ribuan varian imbuhan. Jangan dipanggil tiap scan — reuse instance. Ini bukannew StringBuilder()tiap loop.
Fitur
Affix-aware
"mebabi" itu "babi" pake imbuhan me-. penganjing itu peng- + anjing. Bacot tau ini karena dia pake sliding window: stripping prefix Indonesia satu per satu sampe ketemu kata dasarnya.
b.Text("mebabi").Scan().IsProfane() // true
b.Text("mebabi").Affix(false).Scan()... // false — kalo mau exact match aja
Yang didukung: me-, pe-, di-, te-, be-, ber-, ter-, per-, meng-, peng-, men-, pen-, meny-, peny-, mem-, pem-, ng-, ny-.
Bonus: nasal fusion. memukul itu mem- + pukul (p-nya luluh). Di kamus Bacot cuma ada pukul, tapi memukul tetep kedeteksi. Seperti mantan — apapun kamu kedoknya, tetap ketahuan.
Leet Speak
4 → a, 8 → b, 3 → e, @ → a, $ → s, ! → i, 0 → o, 7 → t, 5 → s, # → h, dan masih banyak lagi.
b.Text("4njing").WithLeetSpeak().Scan().IsProfane() // true
// 4 → a | njing → anjing | match ✅
Leet speak nggak aktif default karena butuh extra pass. Tapi kalo lo tau user base lo banyak pake leet, aktifkan aja.
Unstack Char
b.Text("anjiiiiing").Scan().IsProfane() // true
// anji{i}ng → anjing | match ✅
User kira pake huruf berulang lolos? Apakah ini pertama kalinya lo berhadapan dengan internet Indonesia?
Recursive Scan
b.Text("xbabi").RecursiveScan().IsProfane() // true
Nyari substring match di dalam token. Nge-scan dari tiap posisi karakter di kata. Jadi kalau user lo kreatif nambahin prefiks acak — tetap kena.
Konsekuensi: O(token_length × dict_variants). Buat kata normal (<20 char) — nggak kerasa. Buat teks 10Kb tanpa spasi — ya agak mikir dikit. Worst case complexity: istilah keren buat "bakal lebih lambat dikit".
Custom Dictionary
b.AddWord("pukul") // otomatis detect memukul, dipukul, pemukul, mukul
b.AddWord("setan") // + nyetan, kesetanan, dll
b.Dict.AddWords("custom")
b.Dict.DelWords("anjing")
Istilah baru muncul tiap hari. Lo tinggal AddWord(), kami urus varian imbuhannya.
Config — biar nggak repot chaining manual tiap scan
b.Config(&bacot.ModalScanConfig{
Affix: true,
Collect: true,
Order: []bacot.SanitizeOrder{
bacot.WithLeetSpeak,
bacot.UnstackChar,
},
})
b.Text("4njiiing").Scan().IsProfane() // true — leet + unstack otomatis
Cara Kerja
Pipeline dari input sampe hasil:
Input → ToLower → [Sanitize] → Scan → ScanResult ─→ Censor()
├→ IsProfane()
├→ Extract()
└→ Count()
Arsitektur:
-
Precomputed WordIndex.
Scan()itu bagian paling berat — dia nge-tokenize, stripping affix, ngecek dictionary. Tapi hasilnya adalah[]WordIndexyang nyimpen posisi start/end tiap kata kotor.Censor()tinggal replace*di posisi itu — O(n) per kata, O(1) per replace. Nggak perlu scan ulang. Bayangin kayak index di database — alokasi sekali, query berkali-kali. -
Length histogram. Sebelum map lookup, Bacot ngecek: "apa ada kata di dictionary dengan panjang segini?". Kalau nggak ada, token langsung di-skip. Ini pre-filter O(log n) yang ngurangin map lookup nggak perlu. Micro-optimization yang hasilnya keliatan di 10Kb.
-
Affix variant generation (
craftMan). DiNewDictionary(), setiap kata dasar di-generate semua varian imbuhan yang mungkin. Inilah kenapaNew()agak berat (beberapa ratus µs) tapi scan jadi cepat — cukup exact match di map, nggak perlu stemming di runtime. Tradeoff klasik: bayar sekali di init, gratis selamanya. -
False positive filter. Kalau setelah stripping prefix ditemukan match, tapi sisa stringnya cuma 1 suku kata — kemungkinan itu bukan kata imbuhan beneran. Contoh:
"babiru"→ stripme-gagal → stripdi-gagal → ambil stem"babi"(ditemukan!) → sisa"ru"= 1 suku kata → skip. Heuristic sederhana yang manjur buat ngejaga akurasi.
Benchmark
Intel i5-13400, Linux, Go 1.26.4.
| Level | Operasi | Waktu |
|---|---|---|
| Ringan | Contains lookup aja |
5.7 ns |
| Ringan | Scan kalimat pendek |
531 ns |
| Ringan | Censor kata tunggal |
0.89 ns |
| Sedang | Scan paragraf 5 kalimat |
10.6 µs |
| Berat | Scan 10Kb (1000 kata) |
344 µs |
| Berat | Censor 10Kb (4000 match) |
172 µs |
| Ekstrim | Scan 100Kb lorem |
20 ms |
"2 juta kalimat per detik." —
📊 Raw benchmark — buat yang demen angka
goos: linux | goarch: amd64 | cpu: 13th Gen Intel(R) Core(TM) i5-13400
--- RINGAN ---
Contains-16 217,744,636 5.711 ns/op 0 B/op 0 allocs/op
AddWord-16 1,960,904 607.800 ns/op 280 B/op 7 allocs/op
TextPreprocessing-16 3,364,468 348.700 ns/op 160 B/op 7 allocs/op
ScanSingleWord-16 5,298,542 221.700 ns/op 168 B/op 6 allocs/op
Scan-16 2,273,468 531.400 ns/op 192 B/op 7 allocs/op
RecursiveScan-16 2,765,752 433.400 ns/op 192 B/op 7 allocs/op
Censor-16 1,000,000,000 0.8916 ns/op 0 B/op 0 allocs/op
CensorSentence-16 6,700,214 181.500 ns/op 32 B/op 1 allocs/op
--- SEDANG ---
ScanParagraph-16 114,300 10,681 ns/op 2,744 B/op 23 allocs/op
RecursiveScanParagraph-16 129,304 9,309 ns/op 2,744 B/op 23 allocs/op
CensorParagraph-16 224,820 5,680 ns/op 3,704 B/op 10 allocs/op
ScanCleanLong-16 165,366 6,979 ns/op 2,144 B/op 16 allocs/op
ScanManyStopWords-16 171,465 6,520 ns/op 2,144 B/op 16 allocs/op
ScanAffixLong-16 642,274 1,829 ns/op 1,160 B/op 16 allocs/op
ScanAllLeet-16 193,651 5,823 ns/op 1,120 B/op 14 allocs/op
ScanHeavyUnstack-16 1,269,277 942.6 ns/op 600 B/op 17 allocs/op
ScanMixedAffixLeet-16 879,368 1,312 ns/op 352 B/op 10 allocs/op
RecursiveScanNoMatch-16 478,560 2,376 ns/op 352 B/op 10 allocs/op
CensorLeet-16 1,000,000,000 0.8819 ns/op 0 B/op 0 allocs/op
--- BERAT ---
Scan10Kb-16 3,390 344,589 ns/op 200,666 B/op 1,448 allocs/op
RecursiveScan10Kb-16 3,636 346,540 ns/op 203,865 B/op 1,548 allocs/op
Censor10Kb-16 6,565 207,774 ns/op 144,889 B/op 19 allocs/op
Scan10KbAllLeet-16 3,864 347,040 ns/op 126,049 B/op 36 allocs/op
TextPreprocessing10Kb-16 10,000 109,373 ns/op 125,985 B/op 35 allocs/op
Scan10KbContinuous-16 10,084 116,301 ns/op 139,865 B/op 50 allocs/op
RecursiveScan10KbContinuous-16 8,287 131,111 ns/op 139,905 B/op 52 allocs/op
Scan100Kb-16 57 20,040,838 ns/op 8,255,097 B/op 66 allocs/op
RecursiveScan100Kb-16 62 18,785,788 ns/op 8,255,106 B/op 66 allocs/op
ScanAllProfanity-16 2,328 457,570 ns/op 460,508 B/op 4,055 allocs/op
RecursiveScanAllProfanity-16 2,036 573,975 ns/op 460,507 B/op 4,055 allocs/op
CensorAllProfanity-16 7,722 147,106 ns/op 122,880 B/op 2 allocs/op
RecursiveScanLongSingleWord-16 1,544 744,142 ns/op 93,321 B/op 36 allocs/op
ScanLongSingleWord-16 14,616 81,681 ns/op 93,280 B/op 34 allocs/op
FAQ
Dictionary cuma 200 kata?
200+ kata dasar. Tiap kata dasar generate varian imbuhan — jadinya ribuan. Kurang? AddWord(). Atau buka PR. Makin kotor makin kita senang.
Gunakan AddFalsePositive() untuk menambal kesalahan hehe.
Thread safe?
Enggak. Bacot pake pointer sharing. Lo mau pake di banyak goroutine? Buat instance baru tiap goroutine atau bungkus sync.Mutex. nekad, resiko tanggung sendiri
Cocok buat production?
Well coba aja dulu.