Sembilan produk membuat semua query terasa instan — tetapi nomor itu menipu. Tanpa index, setiap find() memindai seluruh collection dari awal (COLLSCAN), dan biaya scanning tumbuh linear mengikuti jumlah produk. Pada bagian ini kita memasang tiga index yang menyasar tiga query terpenting gudang: pencarian produk per sku, filter kategori dengan rentang harga, dan pencarian bebas berdasarkan nama.

Tiga Index untuk Tiga Query #

Query aplikasiIndexJenis
Detail produk per sku{ sku: 1 }Unique — sekaligus penjaga integritas data
Katalog per kategori, disaring rentang harga{ kategori: 1, harga: 1 }Compound — equality di depan, range di belakang
Pencarian bebas nama produk{ nama: "text" }Text — dengan skor relevansi

Urutan field pada index compound mengikuti aturan equality-sort-range dari chapter Compound Index: kategori difilter tepat (equality) sehingga berada di posisi pertama, harga dipakai rentang (range) sehingga menyusul di belakang. Index yang sama sekaligus melayani sort({ harga: 1 }) di dalam satu kategori tanpa tahap pengurutan terpisah.

Skrip Index dan Pencarian #

js
// inventaris-index.js — index dan pencarian produk
// Jalankan setelah inventaris-crud.js:
//   mongosh "mongodb://127.0.0.1:27017/?directConnection=true" --file src/inventaris-index.js

var inv = db.getSiblingDB("inventaris")

function buatIndex() {
  inv.produk.createIndex({ sku: 1 }, { unique: true })
  inv.produk.createIndex({ kategori: 1, harga: 1 })
  inv.produk.createIndex({ nama: "text" })
  print("index siap:", inv.produk.getIndexes()
    .map(function (i) { return i.name })
    .join(", "))
}

function ujiIndexUnik() {
  let kode = null
  try {
    inv.produk.insertOne({ sku: "KB-001", nama: "duplikat" })
  } catch (galat) {
    kode = galat.code
  }
  print("insert duplikat sku ditolak, kode:", kode)
}

function cariPerSku(sku) {
  print("-- cari per sku:", sku)
  printjson(inv.produk.findOne(
    { sku: sku },
    { _id: 0, sku: 1, nama: 1, harga: 1, stok: 1 }
  ))
}

function cariKategoriRentang(kategori, hargaMin, hargaMax) {
  print("--", kategori, "| harga", hargaMin, "sampai", hargaMax)
  inv.produk
    .find({ kategori: kategori, harga: { $gte: hargaMin, $lte: hargaMax } })
    .sort({ harga: 1 })
    .projection({ _id: 0, sku: 1, nama: 1, harga: 1 })
    .forEach(function (p) { print(" ", p.sku, "|", p.nama, "|", p.harga) })
}

function cariTeks(frase) {
  print("-- pencarian teks:", frase)
  inv.produk
    .find(
      { $text: { $search: frase } },
      { skor: { $meta: "textScore" }, _id: 0, sku: 1, nama: 1 }
    )
    .sort({ skor: { $meta: "textScore" } })
    .forEach(function (p) { print(" ", p.sku, "|", p.nama, "| skor", p.skor.toFixed(4)) })
}

function rencanaQuery() {
  const rencana = inv.produk
    .find({ kategori: "keyboard", harga: { $gte: 100000, $lte: 1000000 } })
    .sort({ harga: 1 })
    .explain()
  printjson(rencana.queryPlanner.winningPlan)
}

buatIndex()
ujiIndexUnik()
cariPerSku("MN-002")
cariKategoriRentang("keyboard", 100000, 1000000)
cariTeks("monitor ips")
rencanaQuery()

Jalankan setelah skrip CRUD:

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

Output yang Dihasilkan:

text
index siap: _id_, sku_1, kategori_1_harga_1, nama_text
insert duplikat sku ditolak, kode: 11000
-- cari per sku: MN-002
{
  sku: 'MN-002',
  nama: 'Monitor VA 27 inci',
  harga: 2400000,
  stok: 15
}
-- keyboard | harga 100000 sampai 1000000
  KB-001 | Keyboard Mekanik 60% | 750000
  KB-004 | Keyboard Tenkeyless Hot-swap | 950000
-- pencarian teks: monitor ips
  MN-001 | Monitor IPS 24 inci | skor 1.2500
  MN-002 | Monitor VA 27 inci | skor 0.6250
{
  isCached: false,
  stage: 'FETCH',
  nss: 'inventaris.produk',
  inputStage: {
    stage: 'IXSCAN',
    nss: 'inventaris.produk',
    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: [
        '["keyboard", "keyboard"]'
      ],
      harga: [
        '[100000, 1000000]'
      ]
    }
  }
}

Membaca Bukti dari Output #

  1. ujiIndexUnik() mencoba menyisipkan produk dengan sku yang sudah ada. Index unik menolak operasi tersebut dengan kode 11000 — duplikat tertangani di sisi server, melengkapi penjaga findOne() pada Bagian 2. Kata kunci try/catch diperlukan karena tanpa penangkapan, skrip akan berhenti di titik tersebut.
  2. cariKategoriRentang() menampilkan dua keyboard di rentang Rp100.000–Rp1.000.000 dalam keadaan terurut. Keterurutan tersebut datang gratis dari index — tidak ada tahap SORT terpisah.
  3. cariTeks("monitor ips") memasangkan dua operator $meta: "textScore": satu di projection agar skor ikut terbaca, satu di sort agar hasil diurutkan dari paling relevan. Monitor IPS mencetak skor 1.2500 karena kedua kata pencarian cocok; Monitor VA hanya cocok pada kata monitor sehingga skornya setengah.
  4. rencanaQuery() membuka kartu rencana query lewat explain(). Tahap IXSCAN dengan keyPattern { kategori: 1, harga: 1 } membuktikan index compound terpakai, dan indexBounds memperlihatkan batas pencarian yang sempit — kategori terkunci pada satu nilai, harga dibatasi rentang. Tahap FETCH di atasnya hanya mengambil document lengkap untuk dua kandidat hasil.

Apa Berikutnya #

Query utama kini berjalan lewat index dan integritas sku terjaga otomatis. Bagian 4 menutup proyek dengan tiga laporan aggregation — nilai persediaan, stok menipis, dan rekap pergerakan bulanan — lalu memverifikasi seluruh perilaku proyek melalui suite tes.