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:

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

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

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

bash
mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/buat-index.js

Output yang Dihasilkan:

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

  1. Penolakan kode 85 memperlihatkan kedua spesifikasi sekaligus: index yang diminta (deskripsi_text, bahasa english bawaan) dan index yang sudah ada (nama_text_deskripsi_text, bahasa none). Pesan galat ini adalah wajah resmi dari aturan satu-text-index.
  2. Peringkat sebelum weights mengulang masalah Bagian 2: enam besar diakhiri Buku Tulis 58 Lembar (0.6) di atas Teknik Sketsa dengan Pensil untuk Pemula (0.5833) — kepadatan kata di deskripsi pendek mengalahkan kecocokan judul.
  3. Peringkat sesudah weights membaliknya: skor produk yang cocok di nama melonjak kira-kira sepuluh kali lipat (0.6667 → 6.6667), sementara Buku Tulis 58 Lembar yang hanya cocok di deskripsi tertahan di 0.6 dan turun satu posisi. Inilah weights bekerja: bukan menambah hasil, melainkan mengatur siapa yang naik.
  4. Rencana query filter menutup skrip dengan bukti pemakaian compound index: FETCH → IXSCAN memakai kategori_1_harga_1, dengan indexBounds yang sempit — kategori terkunci "alat tulis", harga dibatasi [5000, 20000].
  5. Baris ringkasan index di tengah output mencatat kondisi akhir katalog: _id_, nama_text_deskripsi_text versi berbobot, dan kategori_1_harga_1 — tiga index, tiga tugas berbeda, tidak saling menimpa. dropIndex() memakai nama index, bukan definisinya, karena itulah pengenal yang dilaporkan getIndexes().

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.