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 $text dengan $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:

js
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:

text
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:

  1. key: { _fts: 'text', _ftsx: 1 }: text index tidak menyimpan field asli sebagai kunci — ia menyimpan token kata hasil pemecahan teks beserta posisinya.
  2. 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.
  3. default_language: 'english': bahasa default untuk stemming dan stop words.
  4. language_override: 'language': jika sebuah document memiliki field language, 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:

js
db.buku.find({ $text: { $search: "luar biasa" } }, { _id: 0, judul: 1, sinopsis: 1 })

Output yang Dihasilkan:

text
[
  {
    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():

js
db.buku.find({ $text: { $search: "luar biasa" } }, { _id: 0, judul: 1, skor: { $meta: "textScore" } }).sort({ skor: { $meta: "textScore" } })

Output yang Dihasilkan:

text
[
  { 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:

BentukArti
luar biasaOR — cocok document yang memuat luar atau biasa
"luar biasa" dalam kutip gandaFrasa — kedua kata harus berurutan
-remajaNegasi — document yang memuat kata ini dikeluarkan dari hasil

Uji frasa dan negasi pada katalog kita:

js
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:

text
[ { 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:

js
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:

text
[ { judul: 'Atomic Habits' } ]
[ { judul: 'Atomic Habits' } ]
[]

Tiga hasil di atas memperlihatkan cara kerja mesinnya:

  1. habits menemukan Atomic Habits — kata kunci dipangkas stemmer menjadi habit, sama dengan token yang tersimpan di index saat judul diindeks.
  2. ATOMIC tetap menemukan — pencarian tidak membedakan huruf besar dan kecil.
  3. habits dengan $language: "none" gagal — mode none mematikan stemming di sisi query, sehingga kata kunci dicocokkan mentah: token tersimpan habit tidak sama dengan habits.

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:

js
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:

text
{
  acknowledged: true,
  insertedId: ObjectId('6ad3b5253845db5d3d8e5706')
}
Galat: default_language is not valid

Kabar 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:

js
db.buku.find({ $text: { $search: "kebiasaan" } }, { _id: 0, judul: 1 })

Output yang Dihasilkan:

text
[ { 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 + $search mencari dengan logika OR; frasa memakai kutip ganda, negasi memakai awalan -.
  • Skor relevansi diambil lewat {$meta: "textScore"} dan bisa langsung dipakai untuk sort().
  • 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.