Bagian 2 menutup dengan dua keluhan. Pertama, peringkat: Buku Tulis 58 Lembar — yang cocok lewat satu kata di deskripsi — duduk di atas Teknik Sketsa dengan Pensil untuk Pemula yang cocok di judulnya sendiri; bagi pembeli, kecocokan nama jauh lebih berarti. Kedua, performa: query filter kategori + harga murni belum punya index tujuan. Bagian ini membetulkan keduanya sekaligus: membangun ulang text index dengan opsi weights, lalu menambah compound index untuk filter terstruktur.
Batasan Satu Text Index Per Collection #
Sebelum mengubah apa pun, kita buktikan dulu aturan yang mengikatnya: satu collection hanya boleh memiliki satu text index. Skrip kita mencoba membuat text index kedua yang hanya menaungi deskripsi:
db.getSiblingDB("proyek_pencarian").katalog.createIndex({ deskripsi: "text" })Server menolaknya dengan kode 85 — index bertipe text kedua tidak diperbolehkan, berapa pun bedanya daftar field. Konsekuensinya praktis: mengubah komposisi text index — menambah field, mengganti bahasa, menimbang ulang bobot — berarti membangun ulang index dari nol. Pada collection kecil seperti katalog kita prosesnya sekejap; pada collection berjuta document, ini operasi yang direncanakan di jam sepi. Detail batasan lainnya sudah kita bedah di chapter Keterbatasan Text Index.
Weights: Menimbang Nama Lebih Berat dari Deskripsi #
Opsi weights memberi bobot per field dalam perhitungan skor. Kita memberi nama bobot 10 dan deskripsi bobot 1:
db.katalog.createIndex(
{ nama: "text", deskripsi: "text" },
{ default_language: "none", weights: { nama: 10, deskripsi: 1 } }
)Mekanismenya lurus: setiap kontribusi skor dikalikan bobot field asalnya. Kecocokan di nama kini menyumbang sepuluh kali lipat kecocokan di deskripsi yang setara. Karena index yang lama tidak boleh ditimpa — createIndex dengan key sama tetapi opsi berbeda juga ditolak — urutannya dropIndex() lalu createIndex().
Angka 10 sendiri bukan konstanta istimewa. Bobot adalah rasio: { nama: 10, deskripsi: 1 } menghasilkan peringkat yang sama dengan { nama: 100, deskripsi: 10 } — yang berpengaruh adalah perbandingannya, bukan besarnya. Nilai yang wajar ditetapkan dari perilaku query nyata: kalau hasil teratas terasa sering meleset karena kecocokan deskripsi acak, naikkan bobot nama; kalau judul-judul terlalu pendek membuat deskripsi tak berpengaruh, rapatkan rasionya. Katalog kita memakai 10 berbanding 1 karena satu kata di judul produk layak dianggap sepuluh kali lebih bermakna daripada satu kata di kalimat deskripsi.
Index Compound untuk Filter #
Query filter murni — tanpa $text — kini mendapat index sendiri: { kategori: 1, harga: 1 }. Susunannya mengikuti aturan equality-dulu yang kita pelajari di chapter Compound Index: kategori selalu difilter tepat sehingga berdiri di depan, harga dipakai rentang sehingga menyusul — dan index yang sama sekaligus melayani sort({ harga: 1 }) tanpa tahap pengurutan terpisah.
Skrip Pembuatan Index #
// buat-index.js — bangun ulang text index dengan weights + index compound filter
// Jalankan setelah pencarian-filter.js:
// mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/buat-index.js
var dbp = db.getSiblingDB("proyek_pencarian")
function cekBatasanTextIndex() {
print("-- coba membuat text index kedua")
try {
dbp.katalog.createIndex({ deskripsi: "text" })
} catch (galat) {
print("ditolak, kode:", galat.code)
print(galat.message)
}
}
function peringkat(namaFungsi) {
print("--", namaFungsi, ": peringkat 'pensil'")
dbp.katalog
.find(
{ $text: { $search: "pensil" } },
{ _id: 0, nama: 1, skor: { $meta: "textScore" } }
)
.sort({ skor: { $meta: "textScore" } })
.limit(6)
.forEach(function (p) { print(" ", p.nama, "| skor", p.skor.toFixed(4)) })
}
function pasangBobotNama() {
dbp.katalog.dropIndex("nama_text_deskripsi_text")
dbp.katalog.createIndex(
{ nama: "text", deskripsi: "text" },
{ default_language: "none", weights: { nama: 10, deskripsi: 1 } }
)
print("-- text index dibangun ulang dengan weights { nama: 10, deskripsi: 1 }")
}
function buatCompoundFilter() {
dbp.katalog.createIndex({ kategori: 1, harga: 1 })
print("-- index compound filter dibuat:", dbp.katalog.getIndexes()
.map(function (i) { return i.name })
.join(", "))
}
function cekRencanaQueryFilter() {
const rencana = dbp.katalog
.find({ kategori: "alat tulis", harga: { $gte: 5000, $lte: 20000 } })
.sort({ harga: 1 })
.explain()
print("-- rencana query filter kategori + harga:")
printjson(rencana.queryPlanner.winningPlan)
}
cekBatasanTextIndex()
peringkat("sebelum weights")
pasangBobotNama()
peringkat("sesudah weights")
buatCompoundFilter()
cekRencanaQueryFilter()Jalankan setelah skrip filter:
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/buat-index.jsOutput yang Dihasilkan:
-- coba membuat text index kedua
ditolak, kode: 85
An equivalent index already exists with a different name and options. Requested index: { v: 2, key: { _fts: "text", _ftsx: 1 }, name: "deskripsi_text", weights: { deskripsi: 1 }, default_language: "english", language_override: "language", textIndexVersion: 3 }, existing index: { v: 2, key: { _fts: "text", _ftsx: 1 }, name: "nama_text_deskripsi_text", default_language: "none", weights: { deskripsi: 1, nama: 1 }, language_override: "language", textIndexVersion: 3 }
-- sebelum weights : peringkat 'pensil'
Pensil Warna 24 Warna | skor 1.1875
Rautan Pensil Dua Lubang | skor 1.1806
Pensil Mekanik 0.5 mm | skor 1.1556
Pensil Kayu HB | skor 0.6667
Buku Tulis 58 Lembar | skor 0.6000
Teknik Sketsa dengan Pensil untuk Pemula | skor 0.5833
-- text index dibangun ulang dengan weights { nama: 10, deskripsi: 1 }
-- sesudah weights : peringkat 'pensil'
Pensil Warna 24 Warna | skor 6.8125
Rautan Pensil Dua Lubang | skor 6.8056
Pensil Kayu HB | skor 6.6667
Pensil Mekanik 0.5 mm | skor 6.5556
Teknik Sketsa dengan Pensil untuk Pemula | skor 5.8333
Buku Tulis 58 Lembar | skor 0.6000
-- index compound filter dibuat: _id_, nama_text_deskripsi_text, kategori_1_harga_1
-- rencana query filter kategori + harga:
{
isCached: false,
stage: 'FETCH',
nss: 'proyek_pencarian.katalog',
inputStage: {
stage: 'IXSCAN',
nss: 'proyek_pencarian.katalog',
keyPattern: {
kategori: 1,
harga: 1
},
indexName: 'kategori_1_harga_1',
isMultiKey: false,
multiKeyPaths: {
kategori: [],
harga: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
kategori: [
'["alat tulis", "alat tulis"]'
],
harga: [
'[5000, 20000]'
]
}
}
}Membaca Bukti dari Output #
- Penolakan kode 85 memperlihatkan kedua spesifikasi sekaligus: index yang diminta (
deskripsi_text, bahasaenglishbawaan) dan index yang sudah ada (nama_text_deskripsi_text, bahasanone). Pesan galat ini adalah wajah resmi dari aturan satu-text-index. - Peringkat sebelum weights mengulang masalah Bagian 2: enam besar diakhiri
Buku Tulis 58 Lembar(0.6) di atasTeknik Sketsa dengan Pensil untuk Pemula(0.5833) — kepadatan kata di deskripsi pendek mengalahkan kecocokan judul. - Peringkat sesudah weights membaliknya: skor produk yang cocok di nama melonjak kira-kira sepuluh kali lipat (0.6667 → 6.6667), sementara
Buku Tulis 58 Lembaryang hanya cocok di deskripsi tertahan di 0.6 dan turun satu posisi. Inilahweightsbekerja: bukan menambah hasil, melainkan mengatur siapa yang naik. - Rencana query filter menutup skrip dengan bukti pemakaian compound index:
FETCH→IXSCANmemakaikategori_1_harga_1, denganindexBoundsyang sempit —kategoriterkunci"alat tulis",hargadibatasi[5000, 20000]. - Baris ringkasan index di tengah output mencatat kondisi akhir katalog:
_id_,nama_text_deskripsi_textversi berbobot, dankategori_1_harga_1— tiga index, tiga tugas berbeda, tidak saling menimpa.dropIndex()memakai nama index, bukan definisinya, karena itulah pengenal yang dilaporkangetIndexes().
Apa Berikutnya #
Index lengkap: satu text index berbobot dan satu compound index. Bagian 4 menutup proyek dengan pagination dua jalur — sort skor untuk teks, keyset harga + _id untuk filter — lalu mengukur docsExamined untuk membuktikan jarak antara regex, text index, dan COLLSCAN.