Operator $near menjawab “yang terdekat mana?”; pertanyaan lawannya sama pentingnya: “siapa saja yang berada di dalam area ini?”. Dashboard peta menampilkan semua toko dalam kotak pandangan (viewport) layar, dan tim kurir membagi kota menjadi zona pengantaran. Operator $geoWithin menjawab keduanya — dengan lingkaran, kotak, atau polygon, tanpa mewajibkan index, dan tanpa mengurutkan hasil. Chapter ini menguji seluruh bentuk areanya pada data toko kita.
Peta materi Chapter ini:
- Lingkaran:
$centerSphere(radian, bola bumi) dan$center(derajat, bidang datar). - Kotak pembatas dengan
$box. - Polygon legacy
$polygondan Polygon GeoJSON via$geometry. - Perilaku index, hasil tak terurut, dan urutan
[lng, lat].
Lingkaran di Permukaan Bola dengan $centerSphere #
Kita sudah memakai $centerSphere sekilas di Chapter Data Geospatial; sekarang angka konversinya kita bedah. Radius operator ini dinyatakan dalam radian, dan rumus konversinya selalu jarak_km / 6371 — jari-jari bumi dalam kilometer. Untuk radius 130 km dari Toko Menteng: 130 / 6371 = 0,0204 radian, atau sudut busur ±1,17° jika diukur dari pusat bumi. Kita bangun ulang data lengkap dengan index-nya, lalu query:
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.find({ lokasi: { $geoWithin: { $centerSphere: [[106.83, -6.2], 130 / 6371] } } }, { _id: 0, nama: 1, kota: 1 })Output yang Dihasilkan:
switched to db latihan_geo
true
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3e9c33fa5e14f72cb79b4'),
'1': ObjectId('6ad3e9c33fa5e14f72cb79b5'),
'2': ObjectId('6ad3e9c33fa5e14f72cb79b6'),
'3': ObjectId('6ad3e9c33fa5e14f72cb79b7'),
'4': ObjectId('6ad3e9c33fa5e14f72cb79b8'),
'5': ObjectId('6ad3e9c33fa5e14f72cb79b9'),
'6': ObjectId('6ad3e9c33fa5e14f72cb79ba'),
'7': ObjectId('6ad3e9c33fa5e14f72cb79bb')
}
}
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3e9c33fa5e14f72cb79bc'),
'1': ObjectId('6ad3e9c33fa5e14f72cb79bd'),
'2': ObjectId('6ad3e9c33fa5e14f72cb79be')
}
}
lokasi_2dsphere
posisi_2d
[
{ nama: 'Toko Dago', kota: 'Bandung' },
{ nama: 'Toko Menteng', kota: 'Jakarta' }
]Dua toko lolos radius 130 km dari Menteng:
[[106.83, -6.2], 130 / 6371]: pusat lingkaran ditulis sebagai pasangan[lng, lat]telanjang — tidak dibungkus GeoJSON — dan radiusnya langsung berupa ekspresi pembagian; mongosh menghitungnya menjadi 0,0204 radian.- Hasil keanggotaan:
Toko Dago(±117 km) danToko Menteng(0 km) berada di dalam;Toko Gajah Mada(±406 km) dan seluruh toko luar Jawa jauh di luar jangkauan. - Urutan hasil:
Toko Dagomuncul sebelumToko Mentengpadahal jaraknya lebih jauh —$geoWithintidak mengurutkan hasil berdasarkan jarak; urutannya mengikuti pemindaian index.
Lingkaran Datar dengan $center #
Bentuk legacy-nya, $center, menghitung pada bidang datar (Euclidean) dan radiusnya memakai satuan yang sama dengan koordinat — derajat untuk data lng/lat. Dengan satu derajat ≈ 111 km, radius 0.05 berarti lingkaran selebar ±5,5 km. Kita uji pada collection legacy toko_lama dengan pusat di Kios Pasar Baru:
db.toko_lama.find({ posisi: { $geoWithin: { $center: [[106.83, -6.16], 0.05] } } }, { _id: 0, nama: 1, kota: 1 })Output yang Dihasilkan:
[
{ nama: 'Kios Pasar Baru', kota: 'Jakarta' },
{ nama: 'Kios Cikini', kota: 'Jakarta' }
]Mari kita baca angka-angkanya:
- Radius
0.05derajat:Kios Cikiniberjarak datar sqrt(0,01² + 0,02²) = 0,0224 derajat (±2,5 km) dari Pasar Baru — lolos.Kios Dagoberjarak ±1,08 derajat (hampir 120 km) — tertinggal. - Jenis hitungan: Pythagoras pada bidang datar, sama seperti index
2d— bukan kelengkungan bumi seperti$centerSphere. Untuk radius sekecil kota bedanya nyaris tak terasa; untuk antarpulau,$centerSpherejauh lebih akurat.
Kotak Pembatas dengan $box #
Bentuk paling sering dipakai aplikasi peta: bounding box. $box menerima dua sudut — kiri-bawah (barat-daya) dan kanan-atas (timur-laut) — persis batas viewport peta. Kotak berikut meliputi Pulau Jawa pada longitude 105 sampai 114,5 dan latitude -8 sampai -6:
db.toko.find({ lokasi: { $geoWithin: { $box: [[105, -8], [114.5, -6]] } } }, { _id: 0, nama: 1, kota: 1 })Output yang Dihasilkan:
[
{ nama: 'Toko Menteng', kota: 'Jakarta' },
{ nama: 'Toko Dago', kota: 'Bandung' },
{ nama: 'Toko Gajah Mada', kota: 'Semarang' },
{ nama: 'Toko Malioboro', kota: 'Yogyakarta' },
{ nama: 'Toko Tunjungan', kota: 'Surabaya' }
]Lima toko Jawa masuk. Toko Denpasar ([115.22, -8.65]) berada di luar pada kedua sumbu; Medan dan Makassar jauh di luar kotak. Perhatikan bahwa query ini berjalan pada data GeoJSON toko — manual mendokumentasikan $box untuk pasangan legacy, dan pada uji kita di 9.0.2 operator ini tetap mencocokkan titik GeoJSON (konsekuensi index-nya kita ukur di bagian akhir chapter).
Polygon dengan $polygon dan $geometry #
Kotak hanya punya empat sudut; zona nyata sering berbentuk bebas. Versi legacy-nya adalah $polygon: daftar titik yang otomatis tertutup — titik terakhir tersambung ke titik pertama tanpa perlu ditulis ulang:
db.toko_lama.find({ posisi: { $geoWithin: { $polygon: [[106.80, -6.20], [106.88, -6.20], [106.88, -6.14], [106.80, -6.14]] } } }, { _id: 0, nama: 1, kota: 1 })Output yang Dihasilkan:
[
{ nama: 'Kios Pasar Baru', kota: 'Jakarta' },
{ nama: 'Kios Cikini', kota: 'Jakarta' }
]Empat titik di atas membentuk persegi Jakarta tanpa pengulangan titik penutup — dua kios berada di dalamnya, Kios Dago tidak. Jalur GeoJSON-nya berbeda: bentuk area dinyatakan sebagai Polygon di dalam $geometry, dan ring-nya wajib ditutup eksplisit — titik pertama diulang di akhir, aturan yang error 510-nya sudah kita buktikan di Chapter GeoJSON. Kita gambar bentuk Jawa yang tidak beraturan dengan tujuh titik:
db.toko.find({ lokasi: { $geoWithin: { $geometry: { type: "Polygon", coordinates: [ [ [105.0, -6.2], [106.5, -5.9], [110.0, -5.9], [114.5, -6.3], [114.5, -7.9], [108.0, -7.9], [105.0, -6.8], [105.0, -6.2] ] ] } } } }, { _id: 0, nama: 1, kota: 1 })Output yang Dihasilkan:
[
{ nama: 'Toko Tunjungan', kota: 'Surabaya' },
{ nama: 'Toko Dago', kota: 'Bandung' },
{ nama: 'Toko Menteng', kota: 'Jakarta' },
{ nama: 'Toko Gajah Mada', kota: 'Semarang' },
{ nama: 'Toko Malioboro', kota: 'Yogyakarta' }
]Tiga hal patut dicermati dari bentuk ini:
coordinatesbercabang dua: array terluar memuat daftar ring; array kedua adalah ring itu sendiri berisi titik-titik[lng, lat]. Ring tunggal tanpa lubang tampak sebagai array dua lapis seperti ini.- Titik penutup:
[105.0, -6.2]muncul dua kali — di awal dan di akhir. Hapus salah satu, dan query gagal dengan error validasi, bukan sekadar hasil kosong. - Hasil sama, presisi beda: lima toko Jawa yang sama seperti hasil
$box, tetapi polygon bisa mengikuti garis pantai, memotong laut, atau menjorok — kotak tidak.
Perilaku Index dan Urutan Koordinat #
Manual 9.0 menegaskan: $geoWithin tidak mewajibkan index geospatial, tetapi index mempercepat — dan keduanya sudah kita lihat sejak Chapter Data Geospatial. Pertanyaan yang tersisa: bentuk-bentuk tadi memakai index yang mana? Tiga baris explain() menjawabnya langsung dari server kita:
db.toko.find({ lokasi: { $geoWithin: { $box: [[105, -8], [114.5, -6]] } } }).explain().queryPlanner.winningPlan.stage
db.toko.find({ lokasi: { $geoWithin: { $centerSphere: [[106.83, -6.2], 130 / 6371] } } }).explain().queryPlanner.winningPlan.inputStage.indexName
db.toko_lama.find({ posisi: { $geoWithin: { $box: [[106.8, -7.0], [106.9, -6.15]] } } }).explain().queryPlanner.winningPlan.inputStage.indexNameOutput yang Dihasilkan:
COLLSCAN
lokasi_2dsphere
posisi_2dTiga keluaran singkat itu merangkum seluruh mekanisme percepatan:
$boxpadatoko→COLLSCAN: meski hasil tetap keluar di data GeoJSON, index2dspheretidak dipakai — server memindai seluruh collection. Dokumentasi hanya menjamin percepatan bentuk legacy lewat index2d.$centerSpherepadatoko→lokasi_2dsphere: query bola memakai index2dspherelewat tahapIXSCANdi bawahFETCH.$boxpadatoko_lama→posisi_2d: pada data legacy, bentuk legacy memang dipercepat index2d.
Rekap lengkapnya:
| Bentuk | Operator | Radius/syarat | Dipercepat oleh |
|---|---|---|---|
| Lingkaran bola | $centerSphere | radian (km / 6371) | 2dsphere dan 2d |
| Lingkaran datar | $center | satuan koordinat (derajat) | hanya 2d |
| Kotak | $box | dua sudut SW + NE | hanya 2d |
| Polygon legacy | $polygon | titik otomatis tertutup | hanya 2d |
| Polygon GeoJSON | $geometry | ring wajib tertutup | 2dsphere |
Rangkuman #
$geoWithinmenguji keanggotaan area: lingkaran ($centerSphere,$center), kotak ($box), dan polygon ($polygon,$geometry).$centerSpherememakai radian (jarak_km / 6371);$centermemakai derajat pada bidang datar.$boxmenerima sudut kiri-bawah dan kanan-atas;$polygonmenutup ring otomatis, Polygon GeoJSON menuntut penutupan eksplisit.- Tanpa index pun jalan dan hasil tidak diurutkan; bentuk GeoJSON dipercepat
2dsphere, bentuk legacy hanya2d— terbukti lewatexplain(). - Urutan
[lng, lat]berlaku di semua bentuk; koordinat tertukar menghasilkan area yang keliru tanpa galat.
Query lokasi kita kini lengkap: terdekat dan keanggotaan area. Chapter penutup section ini mengangkat keputusan yang datang lebih dulu dari semua query — cara memodelkan cabang toko, area delivery, dan lokasi kurir di dalam skema.