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 $polygon dan 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:

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.find({ lokasi: { $geoWithin: { $centerSphere: [[106.83, -6.2], 130 / 6371] } } }, { _id: 0, nama: 1, kota: 1 })

Output yang Dihasilkan:

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

  1. [[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.
  2. Hasil keanggotaan: Toko Dago (±117 km) dan Toko Menteng (0 km) berada di dalam; Toko Gajah Mada (±406 km) dan seluruh toko luar Jawa jauh di luar jangkauan.
  3. Urutan hasil: Toko Dago muncul sebelum Toko Menteng padahal jaraknya lebih jauh — $geoWithin tidak 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:

js
db.toko_lama.find({ posisi: { $geoWithin: { $center: [[106.83, -6.16], 0.05] } } }, { _id: 0, nama: 1, kota: 1 })

Output yang Dihasilkan:

text
[
  { nama: 'Kios Pasar Baru', kota: 'Jakarta' },
  { nama: 'Kios Cikini', kota: 'Jakarta' }
]

Mari kita baca angka-angkanya:

  1. Radius 0.05 derajat: Kios Cikini berjarak datar sqrt(0,01² + 0,02²) = 0,0224 derajat (±2,5 km) dari Pasar Baru — lolos. Kios Dago berjarak ±1,08 derajat (hampir 120 km) — tertinggal.
  2. 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, $centerSphere jauh 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:

js
db.toko.find({ lokasi: { $geoWithin: { $box: [[105, -8], [114.5, -6]] } } }, { _id: 0, nama: 1, kota: 1 })

Output yang Dihasilkan:

text
[
  { 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:

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

text
[
  { 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:

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

text
[
  { 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:

  1. coordinates bercabang 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.
  2. 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.
  3. 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:

js
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.indexName

Output yang Dihasilkan:

text
COLLSCAN
lokasi_2dsphere
posisi_2d

Tiga keluaran singkat itu merangkum seluruh mekanisme percepatan:

  1. $box pada toko → COLLSCAN: meski hasil tetap keluar di data GeoJSON, index 2dsphere tidak dipakai — server memindai seluruh collection. Dokumentasi hanya menjamin percepatan bentuk legacy lewat index 2d.
  2. $centerSphere pada toko → lokasi_2dsphere: query bola memakai index 2dsphere lewat tahap IXSCAN di bawah FETCH.
  3. $box pada toko_lama → posisi_2d: pada data legacy, bentuk legacy memang dipercepat index 2d.

Rekap lengkapnya:

BentukOperatorRadius/syaratDipercepat oleh
Lingkaran bola$centerSphereradian (km / 6371)2dsphere dan 2d
Lingkaran datar$centersatuan koordinat (derajat)hanya 2d
Kotak$boxdua sudut SW + NEhanya 2d
Polygon legacy$polygontitik otomatis tertutuphanya 2d
Polygon GeoJSON$geometryring wajib tertutup2dsphere

Rangkuman #

  • $geoWithin menguji keanggotaan area: lingkaran ($centerSphere, $center), kotak ($box), dan polygon ($polygon, $geometry).
  • $centerSphere memakai radian (jarak_km / 6371); $center memakai derajat pada bidang datar.
  • $box menerima sudut kiri-bawah dan kanan-atas; $polygon menutup ring otomatis, Polygon GeoJSON menuntut penutupan eksplisit.
  • Tanpa index pun jalan dan hasil tidak diurutkan; bentuk GeoJSON dipercepat 2dsphere, bentuk legacy hanya 2d — terbukti lewat explain().
  • 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.