Query $geoWithin di Chapter Data Geospatial berjalan tanpa index — tetapi dengan pemindaian collection penuh, dan query “terdekat” bahkan menolak jalan sama sekali. Dua index geospatial MongoDB menuntaskan keduanya: 2d untuk pasangan koordinat legacy di bidang datar, dan 2dsphere untuk data GeoJSON di permukaan bola bumi. Chapter ini membuat keduanya, memverifikasi strukturnya lewat getIndexes(), dan menutup dengan pola compound index untuk query lokasi yang selalu disertai filter lain.

Peta materi Chapter ini:

  • Membuat index 2dsphere dan 2d, lalu membaca strukturnya.
  • Perbedaan keduanya: model perhitungan, bentuk data, dan batas koordinat.
  • Compound index geospatial untuk filter jenis + lokasi.

Membuat Index 2dsphere dan 2d #

Kita bangun ulang data toko lengkap dengan index-nya. Pembuatannya sama seperti index biasa — yang berbeda hanya nilai key: string "2dsphere" atau "2d" menggantikan angka 1/-1:

js
use latihan_geo
db.toko.drop()
db.toko_lama.drop()
db.toko.insertMany([
  { nama: "Toko Menteng", kota: "Jakarta", lokasi: { type: "Point", coordinates: [106.83, -6.2] } },
  { nama: "Toko Dago", kota: "Bandung", lokasi: { type: "Point", coordinates: [107.62, -6.9] } },
  { nama: "Toko Gajah Mada", kota: "Semarang", lokasi: { type: "Point", coordinates: [110.42, -6.97] } },
  { nama: "Toko Malioboro", kota: "Yogyakarta", lokasi: { type: "Point", coordinates: [110.37, -7.8] } },
  { nama: "Toko Tunjungan", kota: "Surabaya", lokasi: { type: "Point", coordinates: [112.73, -7.25] } },
  { nama: "Toko Pusat Medan", kota: "Medan", lokasi: { type: "Point", coordinates: [98.67, 3.59] } },
  { nama: "Toko Losari", kota: "Makassar", lokasi: { type: "Point", coordinates: [119.42, -5.13] } },
  { nama: "Toko Denpasar", kota: "Denpasar", lokasi: { type: "Point", coordinates: [115.22, -8.65] } }
])
db.toko_lama.insertMany([
  { nama: "Kios Pasar Baru", kota: "Jakarta", posisi: [106.83, -6.16] },
  { nama: "Kios Cikini", kota: "Jakarta", posisi: [106.84, -6.18] },
  { nama: "Kios Dago", kota: "Bandung", posisi: [107.62, -6.9] }
])
db.toko.createIndex({ lokasi: "2dsphere" })
db.toko_lama.createIndex({ posisi: "2d" })
db.toko.getIndexes()
db.toko_lama.getIndexes()

Output yang Dihasilkan:

text
switched to db latihan_geo
true
true
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3e22cc65ea689c77a15ba'),
    '1': ObjectId('6ad3e22cc65ea689c77a15bb'),
    '2': ObjectId('6ad3e22cc65ea689c77a15bc'),
    '3': ObjectId('6ad3e22cc65ea689c77a15bd'),
    '4': ObjectId('6ad3e22cc65ea689c77a15be'),
    '5': ObjectId('6ad3e22cc65ea689c77a15bf'),
    '6': ObjectId('6ad3e22cc65ea689c77a15c0'),
    '7': ObjectId('6ad3e22cc65ea689c77a15c1')
  }
}
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3e22cc65ea689c77a15c2'),
    '1': ObjectId('6ad3e22cc65ea689c77a15c3'),
    '2': ObjectId('6ad3e22cc65ea689c77a15c4')
  }
}
lokasi_2dsphere
posisi_2d
[
  {
    v: 2,
    key: { _id: 1 },
    name: '_id_',
    collation: { locale: 'simple' }
  },
  {
    v: 2,
    key: { lokasi: '2dsphere' },
    name: 'lokasi_2dsphere',
    '2dsphereIndexVersion': 4,
    collation: { locale: 'simple' }
  }
]
[
  {
    v: 2,
    key: { _id: 1 },
    name: '_id_',
    collation: { locale: 'simple' }
  },
  {
    v: 2,
    key: { posisi: '2d' },
    name: 'posisi_2d',
    collation: { locale: 'simple' }
  }
]

Dua keluaran getIndexes() di atas — yang pertama milik toko, yang kedua milik toko_lama — masing-masing memuat index _id_ bawaan plus index geospatial kita. Tiga field pantas dicermati:

  1. key: nilai '2dsphere' dan '2d' menggantikan arah 1/-1 index biasa — inilah penanda index geospatial.
  2. '2dsphereIndexVersion': 4: versi internal index 2dsphere; letaknya hanya di entri milik toko.
  3. name otomatis: lokasi_2dsphere dan posisi_2d, mengikuti pola <field>_<tipe> seperti index lain.

Dengan index berdiri, query $near yang tadinya mustahil kini bisa jalan, dan $geoWithin tidak lagi memindai seluruh collection.

Dua Index, Dua Dunia Perhitungan #

Perbedaan keduanya bukan sekadar nama — dokumentasi resminya ringkas: index 2d melakukan perhitungan pada bidang datar Euclidean, sedangkan 2dsphere menghitung di permukaan bola untuk data GeoJSON. Konsekuensi praktisnya:

Aspek2d2dsphere
Bentuk datapasangan [lng, lat]GeoJSON dan pasangan legacy
Model hitungbidang datar (Euclidean)permukaan bola bumi
$near, jaraksatuan koordinatmeter (GeoJSON)
Rentang bawaan-180 sampai 180validasi GeoJSON penuh
Kecocokandata warisan, peta datarlokasi dunia nyata

Batas rentang 2d bisa kita buktikan langsung — index ini menolak koordinat di luar -180..180 saat insert:

js
try {
  db.toko_lama.insertOne({ nama: "Kios Aneh", posisi: [999, 10] })
} catch (e) {
  print("Galat: " + e.code + " :: " + e.message)
}

Output yang Dihasilkan:

text
Galat: 13027 :: point not in interval of [ -180, 180 ] :: caused by :: { _id: ObjectId('6ad3e22cc65ea689c77a15c5'), nama: "Kios Aneh", posisi: [ 999, 10 ] }

Error 13027 menyebut interval yang diperiksa secara eksplisit: point not in interval of [ -180, 180 ], lengkap dengan document yang menyebabkannya. Perhatikan bahwa batas ini berlaku seragam untuk kedua sumbu — tidak seperti GeoJSON yang membedakan rentang longitude (-180..180) dan latitude (-90..90). Rentang itu memang bawaan 2d; opsi min/max pada createIndex bisa mempersempitnya untuk data yang berada di peta kecil, misalnya denah satu gedung.

Compound Index Geospatial #

Query lokasi di aplikasi nyata hampir selalu disertai filter biasa: “toko (bukan kios) dalam radius 130 km”. Field equality bisa digabungkan langsung ke satu index bersama field geospatial — urutan penulisan mengikuti pola compound index biasa:

js
db.mitra.drop()
db.mitra.insertMany([
  { nama: "Toko Menteng", jenis: "toko", lokasi: { type: "Point", coordinates: [106.83, -6.2] } },
  { nama: "Toko Dago", jenis: "toko", lokasi: { type: "Point", coordinates: [107.62, -6.9] } },
  { nama: "Kios Cikini", jenis: "kios", lokasi: { type: "Point", coordinates: [106.84, -6.18] } },
  { nama: "Kios Dago Atas", jenis: "kios", lokasi: { type: "Point", coordinates: [107.61, -6.88] } }
])
db.mitra.createIndex({ jenis: 1, lokasi: "2dsphere" })
db.mitra.find({ jenis: "toko", lokasi: { $geoWithin: { $centerSphere: [[106.83, -6.2], 130 / 6371] } } }, { _id: 0, nama: 1, jenis: 1 })
db.mitra.find({ jenis: "toko", lokasi: { $near: { $geometry: { type: "Point", coordinates: [106.83, -6.2] }, $maxDistance: 200000 } } }, { _id: 0, nama: 1, jenis: 1 })

Output yang Dihasilkan:

text
true
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3e22cc65ea689c77a15c6'),
    '1': ObjectId('6ad3e22cc65ea689c77a15c7'),
    '2': ObjectId('6ad3e22cc65ea689c77a15c8'),
    '3': ObjectId('6ad3e22cc65ea689c77a15c9')
  }
}
jenis_1_lokasi_2dsphere
[
  { nama: 'Toko Dago', jenis: 'toko' },
  { nama: 'Toko Menteng', jenis: 'toko' }
]
[
  { nama: 'Toko Menteng', jenis: 'toko' },
  { nama: 'Toko Dago', jenis: 'toko' }
]
  1. { jenis: 1, lokasi: "2dsphere" }: field equality di depan, field geospatial di belakang — satu index melayani kombinasi filter jenis + query lokasi.
  2. $geoWithin + jenis: Toko Menteng (jarak 0) dan Toko Dago (±117 km) lolos radius 130 km; dua kios yang berada di dekat keduanya tersaring oleh jenis. Urutan hasilnya mengikuti pemindaian index — $geoWithin tidak mengurutkan.
  3. $near + jenis: dua toko yang sama, tetapi kali ini terurut dari terdekat — Toko Menteng (0 km) mendahului Toko Dago (±117 km). Kontras urutan dua query inilah bukti bahwa hanya $near yang mengurutkan hasil berdasarkan jarak.

Pola ini menjadi penting saat satu collection memuat banyak jenis entitas lokasi. Tanpa compound index, server harus memilih: memakai index geo lalu menyaring jenis satu per satu, atau memakai index jenis lalu menghitung jarak tanpa index. Dengan compound index, keduanya terjadi sekaligus dalam satu tahap.

Rangkuman #

  • Buat index geospatial dengan createIndex({ field: "2dsphere" }) atau createIndex({ field: "2d" }); verifikasi lewat getIndexes() pada field key.
  • 2d = pasangan legacy, bidang datar, rentang bawaan -180..180 kedua sumbu; error 13027 menolak koordinat di luar rentang.
  • 2dsphere = GeoJSON (dan pasangan legacy), perhitungan bola bumi, jarak dalam meter untuk GeoJSON.
  • Compound index { equality: 1, lokasi: "2dsphere" } melayani filter jenis + query lokasi dalam satu index.

Index sudah berdiri. Chapter berikutnya menggunakannya untuk pertanyaan paling sering diajukan aplikasi lokasi: “mana toko terdekat?”