Pencarian “buku tentang kebiasaan” tidak bisa dilayani index biasa: find({ judul: "kebiasaan" }) hanya cocok untuk judul yang persis sama, padahal pembaca mengetik kata kunci bebas. Untuk kebutuhan ini MongoDB menyediakan text index — index khusus yang memecah teks menjadi token kata, lengkap dengan skor relevansi. Chapter ini membahas pembuatannya, query $text, pengurutan berdasarkan skor, logika OR/frasa/negasi, dan satu hal yang wajib diverifikasi: daftar bahasa yang didukung — termasuk nasib teks bahasa Indonesia.
Peta materi Chapter ini:
- Membuat text index multi-field dan membaca strukturnya lewat
getIndexes(). - Query
$textdengan$search, skor{$meta: "textScore"}, frasa, dan negasi. - Perilaku bahasa: stemming, case-insensitive, dan status bahasa Indonesia.
Membuat Text Index #
Text index dibuat dengan nilai "text" alih-alih arah 1/-1. Satu index boleh mencakup beberapa field sekaligus — berbeda dari index B-tree biasa:
use latihan_index
db.buku.drop()
db.buku.insertMany([
{ judul: "Laut Bercerita", penulis: "Leila S. Chudori", sinopsis: "novel sejarah tentang pelaut muda yang mencari keadilan" },
{ judul: "Bumi", penulis: "Tere Liye", sinopsis: "kisah remaja dengan kekuatan luar biasa untuk melindungi bumi" },
{ judul: "Filosofi Teras", penulis: "Henry Manampiring", sinopsis: "filsafat praktis untuk hidup lebih tenang" },
{ judul: "Atomic Habits", penulis: "James Clear", sinopsis: "kebiasaan kecil dengan hasil luar biasa" },
{ judul: "Rumah Kaca", penulis: "Tere Liye", sinopsis: "thriller politik di negara yang diguyur hujan" },
{ judul: "Bumi Manusia", penulis: "Pramoedya A. Toer", sinopsis: "roman sejarah pada masa kolonial di pulau Jawa" },
{ judul: "Hujan Bulan Juni", penulis: "Sapardi Djoko Damono", sinopsis: "kumpulan puisi cinta yang tenang" },
{ judul: "Ensiklopedia Sains Populer", penulis: "Tim Sains", sinopsis: "referensi sains lengkap untuk keluarga" }
])
db.buku.createIndex({ judul: "text", sinopsis: "text" })
db.buku.getIndexes()Output yang Dihasilkan:
switched to db latihan_index
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3b4d5b71636c78bdff841'),
'1': ObjectId('6ad3b4d5b71636c78bdff842'),
'2': ObjectId('6ad3b4d5b71636c78bdff843'),
'3': ObjectId('6ad3b4d5b71636c78bdff844'),
'4': ObjectId('6ad3b4d5b71636c78bdff845'),
'5': ObjectId('6ad3b4d5b71636c78bdff846'),
'6': ObjectId('6ad3b4d5b71636c78bdff847'),
'7': ObjectId('6ad3b4d5b71636c78bdff848')
}
}
judul_text_sinopsis_text
[
{
v: 2,
key: { _id: 1 },
name: '_id_',
collation: { locale: 'simple' }
},
{
v: 2,
key: { _fts: 'text', _ftsx: 1 },
name: 'judul_text_sinopsis_text',
weights: { judul: 1, sinopsis: 1 },
default_language: 'english',
language_override: 'language',
textIndexVersion: 3,
collation: { locale: 'simple' }
}
]Entry kedua adalah text index kita, dan strukturnya berbeda dari index biasa:
key: { _fts: 'text', _ftsx: 1 }: text index tidak menyimpan field asli sebagai kunci — ia menyimpan token kata hasil pemecahan teks beserta posisinya.weights: bobot tiap field untuk perhitungan skor — sama (1) karena kita tidak mengaturnya; judul bisa diberi bobot lebih tinggi agar kecocokan di judul dianggap lebih relevan.default_language: 'english': bahasa default untuk stemming dan stop words.language_override: 'language': jika sebuah document memiliki fieldlanguage, nilainya dipakai sebagai bahasa document tersebut.
Query $text dan Skor Relevansi #
Query text memakai operator $text dengan $search sebagai kata kuncinya. Beberapa kata dipisahkan spasi dan di-OR-kan — document yang memuat salah satu kata saja tetap lolos:
db.buku.find({ $text: { $search: "luar biasa" } }, { _id: 0, judul: 1, sinopsis: 1 })Output yang Dihasilkan:
[
{
judul: 'Atomic Habits',
sinopsis: 'kebiasaan kecil dengan hasil luar biasa'
},
{
judul: 'Bumi',
sinopsis: 'kisah remaja dengan kekuatan luar biasa untuk melindungi bumi'
}
]Kedua buku memuat frasa “luar biasa” di sinopsisnya. Untuk mengambil skor relevansinya, tambahkan field proyeksi bernilai {$meta: "textScore"} — dan nilai meta yang sama bisa dipakai sebagai kunci sort():
db.buku.find({ $text: { $search: "luar biasa" } }, { _id: 0, judul: 1, skor: { $meta: "textScore" } }).sort({ skor: { $meta: "textScore" } })Output yang Dihasilkan:
[
{ judul: 'Atomic Habits', skor: 1.1666666666666667 },
{ judul: 'Bumi', skor: 1.1111111111111112 }
]Skor dihitung dari kepadatan kecocokan: Atomic Habits mendapat nilai lebih tinggi karena sinopsisnya pendek (kata yang dicari menyumbang porsi lebih besar), sementara sinopsis Bumi lebih panjang. Angka persisnya tidak perlu dihafal — yang penting adalah pola: semakin dominan kata kunci dalam sebuah document, semakin tinggi skornya.
Frasa dan Negasi #
String $search mendukung tiga bentuk sekaligus:
| Bentuk | Arti |
|---|---|
luar biasa | OR — cocok document yang memuat luar atau biasa |
"luar biasa" dalam kutip ganda | Frasa — kedua kata harus berurutan |
-remaja | Negasi — document yang memuat kata ini dikeluarkan dari hasil |
Uji frasa dan negasi pada katalog kita:
db.buku.find({ $text: { $search: "\"luar biasa\"" } }, { _id: 0, judul: 1 })
db.buku.find({ $text: { $search: "luar -remaja" } }, { _id: 0, judul: 1 })Output yang Dihasilkan:
[ { judul: 'Atomic Habits' }, { judul: 'Bumi' } ]
[ { judul: 'Atomic Habits' } ]Query pertama (frasa) tetap mengembalikan dua buku karena keduanya memuat frasa utuh “luar biasa”. Query kedua mencari document ber-kata luar tetapi tanpa kata remaja — Bumi tersingkir karena sinopsisnya memuat “remaja”, sehingga hanya Atomic Habits yang tersisa.
Perilaku Bahasa: Stemming, Kapital, dan Bahasa Indonesia #
Text index bawaan memproses teks dengan aturan bahasa Inggris: huruf besar-kecil diabaikan, dan kata dipangkas ke bentuk dasarnya (stemming) — Habits menjadi habit — baik saat teks diindeks maupun saat kata kunci dicari:
db.buku.find({ $text: { $search: "habits" } }, { _id: 0, judul: 1 })
db.buku.find({ $text: { $search: "ATOMIC" } }, { _id: 0, judul: 1 })
db.buku.find({ $text: { $search: "habits", $language: "none" } }, { _id: 0, judul: 1 })Output yang Dihasilkan:
[ { judul: 'Atomic Habits' } ]
[ { judul: 'Atomic Habits' } ]
[]Tiga hasil di atas memperlihatkan cara kerja mesinnya:
habitsmenemukanAtomic Habits— kata kunci dipangkas stemmer menjadihabit, sama dengan token yang tersimpan di index saat judul diindeks.ATOMICtetap menemukan — pencarian tidak membedakan huruf besar dan kecil.habitsdengan$language: "none"gagal — modenonemematikan stemming di sisi query, sehingga kata kunci dicocokkan mentah: token tersimpanhabittidak sama denganhabits.
Bagaimana dengan bahasa Indonesia? Daftar bahasa yang didukung text index hanya memuat belasan bahasa — antara lain german, french, spanish, italian, dutch, portuguese, danish, turkish, russian, dan none. Bahasa Indonesia tidak ada di dalamnya. Mencobanya langsung ditolak:
db.uji_bahasa.insertOne({ teks: "tes bahasa" })
try {
db.uji_bahasa.createIndex({ teks: "text" }, { default_language: "indonesian" })
} catch (e) {
print("Galat: " + e.message.split(":: caused by ::").pop().trim())
}Output yang Dihasilkan:
{
acknowledged: true,
insertedId: ObjectId('6ad3b5253845db5d3d8e5706')
}
Galat: default_language is not validKabar baiknya: kata bahasa Indonesia tetap terindeks dan bisa dicari pada index biasa maupun mode none, karena tokenisasi memecah kalimat berdasarkan spasi dan tanda baca, bukan kamus:
db.buku.find({ $text: { $search: "kebiasaan" } }, { _id: 0, judul: 1 })Output yang Dihasilkan:
[ { judul: 'Atomic Habits' } ]Rangkuman #
- Text index dibuat dengan
createIndex({ field: "text" }); satu collection hanya boleh satu text index yang mencakup semua field teks yang dibutuhkan. $text+$searchmencari dengan logika OR; frasa memakai kutip ganda, negasi memakai awalan-.- Skor relevansi diambil lewat
{$meta: "textScore"}dan bisa langsung dipakai untuksort(). - Default bahasa Inggris: case-insensitive dan stemming; daftar bahasa yang didukung tidak memuat Indonesia — gunakan
language: "none". - Untuk pencarian teks produksi yang serius, pertimbangkan Atlas Search yang memiliki pemrosesan bahasa yang lebih lengkap.