Text index bekerja baik untuk katalog kecil, tetapi semakin banyak kebutuhan yang muncul — pencarian multi-bahasa, logika AND antar kata, integrasi dengan aggregation — semakin sering kita menabrak dinding batasannya. Bab ini membedah batasan-batasan itu satu per satu dengan eksperimen langsung, supaya keputusan “tetap memakai text index atau naik kelas” diambil berdasarkan fakta, bukan tebakan. Kita masih memakai database latihan_geo dan collection buku.
Peta batasan yang akan kita uji:
- Satu collection hanya boleh memiliki satu text index.
- Logika antar kata terbatas: OR, frasa, dan negasi — tanpa AND bebas.
$texthanya boleh di$matchstage pertama pada aggregation pipeline.- Stop words dan stemming mengikuti bahasa Inggris.
Satu Text Index Per Collection #
MongoDB menolak text index kedua pada collection yang sama. Semua field teks yang ingin dicari harus digabungkan ke dalam satu index sejak awal — termasuk bobotnya, seperti weights yang kita pakai di Chapter Text Search:
use latihan_geo
db.buku.drop()
db.buku.insertMany([
{ judul: "Laut Bercerita", sinopsis: "novel sejarah tentang pelaut muda yang mencari keadilan" },
{ judul: "Sirkus Pohon", sinopsis: "dongeng tentang samudra dan hutan dalam ingatan" },
{ judul: "Warung Tepi Jalan", sinopsis: "kisah warung gorengan dan kopi yang tak pernah sepi" },
{ judul: "The Great Gatsby", sinopsis: "a story of wealth, love and the american dream" }
])
db.buku.createIndex({ judul: "text", sinopsis: "text" })
try {
db.buku.createIndex({ judul: "text" })
} catch (e) {
print("Galat: " + e.code)
print(e.message)
}Output yang Dihasilkan:
switched to db latihan_geo
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3e08ee505a46c80c29b4f'),
'1': ObjectId('6ad3e08ee505a46c80c29b50'),
'2': ObjectId('6ad3e08ee505a46c80c29b51'),
'3': ObjectId('6ad3e08ee505a46c80c29b52')
}
}
judul_text_sinopsis_text
Galat: 85
An equivalent index already exists with a different name and options. Requested index: { v: 2, key: { _fts: "text", _ftsx: 1 }, name: "judul_text", weights: { judul: 1 }, default_language: "english", language_override: "language", textIndexVersion: 3 }, existing index: { 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 }Error 85 menampilkan kedua spesifikasi sekaligus: index yang diminta (judul_text) dan index yang sudah ada (judul_text_sinopsis_text). Keduanya sama-sama bertipe text, dan itulah yang ditolak. Konsekuensinya praktis: menambah field baru ke pencarian teks berarti membangun ulang index dengan daftar field lengkap — operasi yang mahal pada collection besar.
Logika AND yang Tidak Ada #
Di Chapter Text Search kita melihat kata biasa di-OR-kan dan hanya frasa yang di-AND-kan. Batas ini terasa nyata ketika pengguna mengetik dua kata yang keduanya wajib ada, tetapi tidak berurutan:
db.buku.find({ $text: { $search: "samudra keadilan" } }, { _id: 0, judul: 1 })Output yang Dihasilkan:
[ { judul: 'Laut Bercerita' }, { judul: 'Sirkus Pohon' } ]Tidak ada satu pun buku yang memuat samudra dan keadilan sekaligus — Sirkus Pohon hanya memuat samudra, Laut Bercerita hanya memuat keadilan. Keduanya tetap lolos karena logikanya OR. Tiga jalan keluar yang tersedia:
- Frasa
"samudra keadilan"— hanya jika kedua kata memang selalu berurutan. - Menyaring hasil di aplikasi — ambil hasil OR, lalu buang document yang tidak memuat kedua kata.
- Naik ke Atlas Search — search engine yang mendukung operator AND antar kata secara native (Chapter berikutnya).
$text Hanya di Stage Pertama #
Pada aggregation pipeline, $match yang memuat $text wajib menjadi stage pertama. Menaruhnya setelah stage apa pun — termasuk setelah $lookup — langsung ditolak server:
try {
db.buku.aggregate([ { $limit: 5 }, { $match: { $text: { $search: "samudra" } } } ]).toArray()
} catch (e) {
print("Galat: " + e.code + " :: " + e.message)
}
try {
db.buku.aggregate([
{ $lookup: { from: "buku", localField: "judul", foreignField: "judul", as: "salinan" } },
{ $match: { $text: { $search: "samudra" } } }
]).toArray()
} catch (e) {
print("Galat: " + e.code + " :: " + e.message)
}Output yang Dihasilkan:
Galat: 17313 :: $match with $text is only allowed as the first pipeline stage
Galat: 17313 :: $match with $text is only allowed as the first pipeline stageKedua usaha menemui error 17313 yang sama — baik setelah $limit maupun setelah $lookup. Penyebabnya mekanis: $text hanya bisa dilayani lewat text index milik collection asal, sedangkan document yang mengalir setelah $lookup berasal dari collection lain dan tidak lagi terikat pada index itu. Konsekuensinya: pencarian teks harus selalu di depan, dan penggabungan data dari collection lain dilakukan setelahnya.
Stop Words dan Stemming Bahasa Indonesia #
Dua perilaku bahasa Inggris ini sering mengejutkan pengguna bahasa Indonesia. Pertama, stop word Inggris dibuang saat pengindeksan sehingga tidak bisa dicari. Kedua, stemmer hanya mengenal imbuhan Inggris — kata gorengan dan goreng dianggap dua kata berbeda:
db.buku.find({ $text: { $search: "the and a of" } }, { _id: 0, judul: 1 }).toArray()
db.buku.find({ $text: { $search: "goreng" } }, { _id: 0, judul: 1 }).toArray()
db.buku.find({ $text: { $search: "gorengan" } }, { _id: 0, judul: 1 })Output yang Dihasilkan:
[]
[]
[ { judul: 'Warung Tepi Jalan' } ]the and a of→ kosong: keempatnya stop word Inggris yang tidak pernah tersimpan di index.goreng→ kosong,gorengan→ ketemu: stemmer bahasa Inggris tidak mengenal akhiran-anmilik bahasa Indonesia, sehinggagorengandibiarkan utuh sebagai token. Pencarianmenanamvstanam,penulisvstulisbernasib sama.- Bahasa index tunggal:
default_languageditetapkan sekali untuk seluruh index. Chapter Text Index sudah membuktikanindonesianditolak karena tidak ada di daftar bahasa yang didukung; opsi praktisnya adalah"none"atau fieldlanguageper document (lihatlanguage_overridedi dokumentasi).
Alternatif: Regex dan Kapan Naik Kelas #
Untuk kebutuhan sederhana, regex menutup beberapa celah text index — terutama pencarian bagian kata yang tidak terdeteksi stemmer:
db.buku.find({ sinopsis: /goreng/i }, { _id: 0, judul: 1 })
db.buku.find({ judul: /^warung/i }, { _id: 0, judul: 1 })Output yang Dihasilkan:
[ { judul: 'Warung Tepi Jalan' } ]
[ { judul: 'Warung Tepi Jalan' } ]Regex /goreng/i menemukan gorengan karena mencocokkan substring, bukan token. Regex berprefiks ^warung bahkan bisa memanfaatkan index biasa pada field itu. Namun regex tidak punya peringkat relevansi, tidak memahami frasa, dan regex tanpa prefiks memindai seluruh collection. Tabel berikut merangkum keputusannya:
| Kebutuhan | Solusi tepat |
|---|---|
| Cari kata/frasa sederhana, satu bahasa | Text index $text |
Cari bagian kata (goreng dalam gorengan) | Regex /goreng/i |
| Awalan kata pada satu field | Regex prefiks ^... + index biasa |
| AND antar kata, fuzzy, autocomplete, facet | Atlas Search |
Rangkuman #
- Satu collection satu text index; menambah field berarti membangun ulang index.
- Kata biasa hanya bisa di-OR-kan — AND bebas tidak tersedia, hanya frasa berurutan.
$matchdengan$textwajib stage pertama; setelah$lookuplangsung error 17313.- Stop words dan imbuhan mengikuti bahasa Inggris;
gorengan≠goreng, dan katathetidak pernah terindeks. - Regex menutup kebutuhan bagian kata dan prefiks, tetapi tanpa relevansi.
Batasan-batasan ini bukan cacat perancangan — text index memang dirancang ringan untuk kebutuhan dasar. Ketika aplikasi menuntut lebih, saatnya berkenalan dengan Atlas Search.