Chapter Text Index di section Indexing sudah memperkenalkan pembuatan text index dan query $text dasar. Chapter ini memperdalam sisi query-nya untuk katalog toko buku yang sesungguhnya: kecocokan di judul harus lebih berbobot daripada kecocokan di sinopsis, pencarian harus bisa digabung dengan filter kategori, dan kita perlu memahami logika antar kata yang sesungguhnya dijalankan server. Semua contoh di bawah memakai database latihan_geo — database yang sama akan kita pakai terus sampai akhir section ini untuk data lokasi toko.

Peta materi Chapter ini:

  • Bobot field (weights) dan pengaruhnya terhadap skor relevansi.
  • Logika antar kata: OR, frasa eksak, multi-frasa, dan negasi.
  • Perilaku bahasa di sisi query: stemming, huruf besar-kecil, dan $language.
  • Kombinasi $text dengan filter field biasa.

Bobot Field, Skor, dan Pengurutan #

Skor relevansi text index dihitung dari kepadatan kecocokan, tetapi tidak semua field sederajat. Sebuah buku yang judulnya memuat kata kunci jauh lebih relevan daripada buku yang hanya menyebut kata itu sekilas di sinopsis. Opsi weights pada createIndex mengatur kontribusi tiap field terhadap skor:

js
use latihan_geo
db.buku.drop()
db.buku.insertMany([
  { judul: "Laut Bercerita", penulis: "Leila S. Chudori", kategori: "novel", sinopsis: "novel sejarah tentang pelaut muda yang mencari keadilan" },
  { judul: "Sirkus Pohon", penulis: "Andrea Hirata", kategori: "novel", sinopsis: "dongeng tentang samudra, laut, dan hutan yang tersimpan dalam ingatan" },
  { judul: "Masakan Rumahan Nusantara", penulis: "Siti Rahayu", kategori: "masak", sinopsis: "kumpulan resep praktis: pisang goreng, ubi rebus, dan teh hangat" },
  { judul: "Berkebun di Pekarangan", penulis: "Dewi Lestari", kategori: "berkebun", sinopsis: "menanam pisang dan singkong di kebun kecil belakang rumah" },
  { judul: "Jajanan Pasar Klasik", penulis: "Rudy Hartono", kategori: "masak", sinopsis: "aneka goreng dan kue basah untuk teman minum teh" },
  { judul: "The Great Gatsby", penulis: "F. Scott Fitzgerald", kategori: "novel", sinopsis: "a story of wealth, love and the american dream" }
])
db.buku.createIndex({ judul: "text", sinopsis: "text" }, { weights: { judul: 10, sinopsis: 1 } })

Output yang Dihasilkan:

text
switched to db latihan_geo
true
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3e01bacd1615c30e707a1'),
    '1': ObjectId('6ad3e01bacd1615c30e707a2'),
    '2': ObjectId('6ad3e01bacd1615c30e707a3'),
    '3': ObjectId('6ad3e01bacd1615c30e707a4'),
    '4': ObjectId('6ad3e01bacd1615c30e707a5'),
    '5': ObjectId('6ad3e01bacd1615c30e707a6')
  }
}
judul_text_sinopsis_text

Koleksi ini sengaja memuat satu buku berbahasa Inggris — toko buku lazim menjual buku impor. Sekarang kita cari kata laut, yang muncul di judul satu buku dan di sinopsis buku lain:

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

Output yang Dihasilkan:

text
[ { judul: 'Laut Bercerita' }, { judul: 'Sirkus Pohon' } ]
[
  { judul: 'Laut Bercerita', skor: 7.5 },
  { judul: 'Sirkus Pohon', skor: 0.55 }
]

Perhatikan urutan kedua hasil tersebut. Query $text tidak mengurutkan hasil menurut relevansi — query pertama mengembalikan Sirkus Pohon tepat setelah Laut Bercerita karena urutannya mengikuti pemindaian index, bukan skor. Urutan relevansi hanya muncul bila kita memproyeksikan {$meta: "textScore"} sekaligus memakai meta yang sama sebagai kunci sort(), seperti pola di Chapter Text Index — dan kali ini selisih skornya terlihat jelas:

  1. weights: { judul: 10, sinopsis: 1 }: kecocokan di judul diberi bobot sepuluh kali lipat kecocokan di sinopsis.
  2. Skor 7.5 vs 0.55: Laut Bercerita mencetak skor tinggi karena kata laut ditemukan di field berbobot tinggi; Sirkus Pohon hanya menyebut laut di sinopsis.
  3. Angka absolut tidak bermakna tetap: skor hanya berguna untuk membandingkan document dalam satu query yang sama, bukan antar query.

Frasa Eksak dan Logika Antar Kata #

Aturan dasar $search di Chapter Text Index menyebut OR untuk kata dan frasa untuk kutip ganda. Sekarang kita bedah kasus yang membedakannya dengan tegas — dan satu aturan tambahan yang jarang disadari: beberapa frasa dihubungkan dengan AND:

js
db.buku.find({ $text: { $search: "pisang goreng" } }, { _id: 0, judul: 1 })
db.buku.find({ $text: { $search: "\"pisang goreng\"" } }, { _id: 0, judul: 1 })
db.buku.find({ $text: { $search: "\"pisang goreng\" \"teman minum\"" } }, { _id: 0, judul: 1 }).toArray()
db.buku.find({ $text: { $search: "goreng -ubi" } }, { _id: 0, judul: 1 })

Output yang Dihasilkan:

text
[
  { judul: 'Jajanan Pasar Klasik' },
  { judul: 'Masakan Rumahan Nusantara' },
  { judul: 'Berkebun di Pekarangan' }
]
[ { judul: 'Masakan Rumahan Nusantara' } ]
[]
[ { judul: 'Jajanan Pasar Klasik' } ]

Empat query di atas memperlihatkan seluruh logika $search:

  1. pisang goreng (tanpa kutip): OR — tiga buku lolos karena memuat pisang atau goreng.
  2. "pisang goreng" (dalam kutip): frasa eksak — hanya Masakan Rumahan Nusantara yang memuat kedua kata berurutan. Tanda kutip ganda di dalam string mongosh harus ditulis \" agar tidak menutup string itu sendiri.
  3. Dua frasa sekaligus: AND — tidak ada buku yang memuat pisang goreng dan teman minum sekaligus, sehingga hasilnya kosong. Inilah satu-satunya cara membuat logika “kedua kata wajib ada” di $text: menjadikannya frasa.
  4. goreng -ubi: OR lalu buang — Jajanan Pasar Klasik lolos, Masakan Rumahan Nusantara tersingkir karena sinopsisnya memuat ubi.

Perilaku Bahasa di Sisi Query #

Sama seperti yang kita lihat dengan habits di Chapter Text Index, pencarian tetap menemukan bentuk berimbuhan dan kapitalisasi apa pun. Dua fakta tambahan penting muncul saat opsi $language dijadikan "none" hanya di sisi query:

js
db.buku.find({ $text: { $search: "stories" } }, { _id: 0, judul: 1 })
db.buku.find({ $text: { $search: "STORY" } }, { _id: 0, judul: 1 })
db.buku.find({ $text: { $search: "stories", $language: "none" } }, { _id: 0, judul: 1 }).toArray()
db.buku.find({ $text: { $search: "the", $language: "none" } }, { _id: 0, judul: 1 }).toArray()

Output yang Dihasilkan:

text
[ { judul: 'The Great Gatsby' } ]
[ { judul: 'The Great Gatsby' } ]
[]
[]
  1. stories menemukan The Great Gatsby — stemmer bahasa Inggris memangkas kata kunci stories ke bentuk dasar yang sama dengan token tersimpan dari kata story di sinopsis.
  2. STORY juga menemukan — pencarian tidak membedakan huruf besar dan kecil.
  3. stories + $language: "none" gagal — mode ini mematikan pemrosesan hanya untuk kata kunci, sementara token di index sudah dipangkas saat data diindeks. Kata kunci mentah stories tidak sama dengan token tersimpan stori.
  4. the + $language: "none" tetap kosong — stop word bahasa Inggris sudah dibuang saat pengindeksan, sehingga token the memang tidak pernah tersimpan. Mengubah bahasa di sisi query tidak bisa mengembalikannya.

Kesimpulannya: bahasa index ditetapkan sekali di createIndex (lewat default_language), dan $language di query hanya mengubah cara kata kunci diproses. Kedua sisi harus konsisten agar kecocokan terjadi.

Menggabungkan $text dengan Filter Lain #

Query toko buku jarang berdiri sendiri — pembaca mengetik kata kunci sambil memilih kategori. Field biasa cukup ditambahkan ke filter yang sama dengan $text:

js
db.buku.find({ kategori: "masak", $text: { $search: "teh" } }, { _id: 0, judul: 1 })
db.buku.find({ $text: { $search: "teh" }, kategori: "masak" }, { _id: 0, judul: 1 })
try {
  db.buku.find({ $and: [ { $text: { $search: "teh" } }, { $text: { $search: "laut" } } ] }).toArray()
} catch (e) {
  print("Galat: " + e.code + " :: " + e.message)
}

Output yang Dihasilkan:

text
[
  { judul: 'Jajanan Pasar Klasik' },
  { judul: 'Masakan Rumahan Nusantara' }
]
[
  { judul: 'Jajanan Pasar Klasik' },
  { judul: 'Masakan Rumahan Nusantara' }
]
Galat: 2 :: Too many text expressions
  1. kategori + $text dalam satu filter: server memakai text index untuk $text, lalu menyaring kecocokan kategori pada kandidat hasil — dua buku kategori masak yang memuat teh lolos.
  2. Urutan tidak penting: kedua query pertama menghasilkan document yang sama. MongoDB 9.0 tidak mensyaratkan $text ditulis lebih dulu; posisi field dalam object filter tidak mengubah rencana eksekusi.
  3. $and dengan dua $text ditolak: error Too many text expressions — satu query hanya boleh memuat satu predicate $text. Menyatukan dua pencarian teks harus dilakukan di aplikasi.

Rangkuman #

  • weights pada createIndex membuat kecocokan di field penting (judul) mengungguli kecocokan di field pendukung (sinopsis); skor hanya dibandingkan dalam satu query yang sama.
  • Tanpa sort() bertanda {$meta: "textScore"}, hasil $text tidak terurut menurut relevansi.
  • Kata biasa di-OR-kan; frasa eksak pakai \"...\"; beberapa frasa di-AND-kan; negasi -kata selalu mengeluarkan.
  • $language: "none" hanya mematikan pemrosesan di sisi kata kunci — token yang sudah dipangkas atau dibuang sebagai stop word tidak bisa ditemukan kembali.
  • Filter field biasa boleh digabung bebas dengan $text di posisi mana pun, tetapi hanya satu $text per query.

Dengan jangkauan ini, batasan-batasan text index mulai terasa — satu index per collection, logika AND yang terbatas, dan bahasa tunggal. Chapter berikutnya membedahnya satu per satu.