“Toko buku terdekat dari posisi saya di mana?” — itulah query geospatial yang paling sering dieksekusi aplikasi lokasi, dan MongoDB punya dua alat khusus untuk menjawabnya: operator $near untuk find() dan stage $geoNear untuk aggregation pipeline. Keduanya mengurutkan hasil dari terdekat, keduanya menyertakan angka jarak, dan — berbeda dari $geoWithin — keduanya mensyaratkan index geospatial. Chapter ini membuktikan syarat itu, lalu mengukur jarak nyata antar kota Indonesia kita.
Peta materi Chapter ini:
$neardengan$maxDistancedan$minDistancedalam satuan meter.- Stage
$geoNear: jarak sebagai field,distanceMultiplier, filter gabungan. $nearSphereuntuk pasangan koordinat legacy dengan satuan radian.
$near Menggunakan Index Geospatial #
Kita mulai dengan membuktikan syaratnya. Collection toko diisi ulang tanpa index lebih dulu, lalu query $near dijalankan — dan ditolak:
use latihan_geo
db.toko.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] } }
])
try {
db.toko.find({ lokasi: { $near: { $geometry: { type: "Point", coordinates: [106.83, -6.2] }, $maxDistance: 200000 } } }, { _id: 0, nama: 1, kota: 1 }).toArray()
} catch (e) {
print("Galat: " + e.code + " :: " + e.message)
}
db.toko.createIndex({ lokasi: "2dsphere" })
db.toko.find({ lokasi: { $near: { $geometry: { type: "Point", coordinates: [106.83, -6.2] }, $maxDistance: 200000 } } }, { _id: 0, nama: 1, kota: 1 })
db.toko.find({ lokasi: { $near: { $geometry: { type: "Point", coordinates: [106.83, -6.2] }, $minDistance: 400000 } } }, { _id: 0, nama: 1, kota: 1 })Output yang Dihasilkan:
switched to db latihan_geo
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3e2c3451e26835b66b2b2'),
'1': ObjectId('6ad3e2c3451e26835b66b2b3'),
'2': ObjectId('6ad3e2c3451e26835b66b2b4'),
'3': ObjectId('6ad3e2c3451e26835b66b2b5'),
'4': ObjectId('6ad3e2c3451e26835b66b2b6'),
'5': ObjectId('6ad3e2c3451e26835b66b2b7')
}
}
Galat: 291 :: error enumerating plans for query: ns=latihan_geo.tokoTree: GEONEAR field=lokasi maxdist=200000 isNearSphere=0
Sort: {}
Proj: { _id: 0, nama: 1, kota: 1 }
planner returned error :: caused by :: unable to find index for $geoNear query
lokasi_2dsphere
[
{ 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' },
{ nama: 'Toko Pusat Medan', kota: 'Medan' }
]Mari kita bedah urutan kejadiannya:
- Error 291 sebelum index: pesan panjang itu intinya satu —
unable to find index for $geoNear query. Nama internal$geoNeardipakai server untuk seluruh keluarga query “terdekat”.$geoWithinboleh tanpa index,$neartidak. $maxDistance: 200000: 200.000 meter — untuk titik GeoJSON, jarak selalu dalam meter, bukan radian. Hasilnya terurut dari terdekat:Toko Menteng(0 km) mendahuluiToko Dago(±117 km).$minDistance: 400000: batas bawah — semua toko lebih dari 400 km dari Jakarta, tetap diurutkan dari yang terdekat: Semarang, Yogyakarta, Surabaya, lalu Medan.
$geoNear: Stage dengan Jarak sebagai Field #
Operator $near mengembalikan document, tetapi angka jaraknya tersembunyi di dalam mesin. Stage $geoNear menuliskannya sebagai field biasa lewat distanceField — dan karena berupa stage, hasilnya bisa langsung disambung $project, $limit, atau $match:
db.toko.aggregate([
{ $geoNear: { near: { type: "Point", coordinates: [106.83, -6.2] }, distanceField: "jarakMeter", spherical: true, query: { kota: { $ne: "Jakarta" } } } },
{ $project: { _id: 0, nama: 1, kota: 1, jarakMeter: 1 } },
{ $limit: 3 }
])
db.toko.aggregate([
{ $geoNear: { near: { type: "Point", coordinates: [106.83, -6.2] }, distanceField: "jarakKm", distanceMultiplier: 0.001, spherical: true, maxDistance: 500000 } },
{ $project: { _id: 0, nama: 1, jarakKm: 1 } }
])
try {
db.toko.aggregate([ { $match: { kota: "Bandung" } }, { $geoNear: { near: { type: "Point", coordinates: [106.83, -6.2] }, distanceField: "jarak", spherical: true } } ]).toArray()
} catch (e) {
print("Galat: " + e.code + " :: " + e.message)
}Output yang Dihasilkan:
[
{ nama: 'Toko Dago', kota: 'Bandung', jarakMeter: 117068.64258078934 },
{
nama: 'Toko Gajah Mada',
kota: 'Semarang',
jarakMeter: 406142.3005593865
},
{
nama: 'Toko Malioboro',
kota: 'Yogyakarta',
jarakMeter: 429762.5737023311
}
]
[
{ nama: 'Toko Menteng', jarakKm: 0 },
{ nama: 'Toko Dago', jarakKm: 117.06864258078934 },
{ nama: 'Toko Gajah Mada', jarakKm: 406.1423005593865 },
{ nama: 'Toko Malioboro', jarakKm: 429.7625737023311 }
]
Galat: 40603 :: $geoNear was not the first stage in the pipeline after optimization. Is optimization disabled or inhibited?near+distanceField+spherical: true: titik acuan Menteng, hasil distance dalam meter, perhitungan bola bumi — kombinasi standar untuk data GeoJSON.query: { kota: { $ne: "Jakarta" } }: filter gabungan langsung di dalam stage — toko acuan sendiri dikeluarkan agar jarak 0 tidak memenuhi daftar.distanceMultiplier: 0.001: setiap jarak dikalikan 0.001 sebelum ditulis — meter berubah menjadi kilometer, sehinggajarakKmlangsung terbaca manusia.$geoNearwajib stage pertama: setelah$matchsekalipun ditolak — sama ketatnya dengan$searchdan$text.
Angka-angka jaraknya sendiri layak dipercaya: jarak Jakarta–Bandung ±117 km, Jakarta–Semarang ±406 km, dan Jakarta–Yogyakarta ±430 km adalah jarak garis lurus di permukaan bumi antar titik koordinat kita — bukan jalan darat yang selalu lebih panjang. Sebelum melanjutkan, mari rapikan pilihan ketiga operator “terdekat” yang telah kita pegang:
| Operator | Konteks | Satuan jarak | Jarak sebagai field |
|---|---|---|---|
$near | find(), GeoJSON | meter | tidak |
$geoNear | stage pertama pipeline | meter (distanceMultiplier mengubah) | ya, distanceField |
$nearSphere | find(), pasangan legacy | radian | tidak |
Aturan praktisnya: butuh angka jaraknya atau ingin menyambungkan pipeline — pakai $geoNear; cukup daftar terurut dari find() — $near sudah memadai.
Legacy: $nearSphere dalam Radian #
Untuk collection warisan berpasangan koordinat, query “terdekat” yang tepat adalah $nearSphere. Titik acuan ditulis sebagai array, dan $maxDistance memakai radian — satuannya berbeda dari versi GeoJSON:
db.toko_lama.drop()
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_lama.createIndex({ posisi: "2d" })
db.toko_lama.find({ posisi: { $nearSphere: [106.83, -6.2], $maxDistance: 0.02 } }, { _id: 0, nama: 1, kota: 1 })Output yang Dihasilkan:
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3e2c4451e26835b66b2b8'),
'1': ObjectId('6ad3e2c4451e26835b66b2b9'),
'2': ObjectId('6ad3e2c4451e26835b66b2ba')
}
}
posisi_2d
[
{ nama: 'Kios Cikini', kota: 'Jakarta' },
{ nama: 'Kios Pasar Baru', kota: 'Jakarta' },
{ nama: 'Kios Dago', kota: 'Bandung' }
]Hasilnya tiga kios terurut dari terdekat — Kios Cikini (±2 km), Kios Pasar Baru (±4 km), lalu Kios Dago (±117 km). Angka 0.02 adalah radian: 0,02 × 6.371 km ≈ 127 km, sehingga Bandung masih masuk. Rumus konversinya selalu jarak_km / 6371 — kebalikan dari yang kita pakai di $centerSphere nanti.
Rangkuman #
$nearmensyaratkan index geospatial — tanpa itu, error 291unable to find index for $geoNear query.- Untuk GeoJSON,
$maxDistance/$minDistancedalam meter; hasil otomatis terurut dari terdekat. $geoNearmenulis jarak ke field (distanceField), mendukungquerygabungan,distanceMultiplier, dan wajib menjadi stage pertama.$nearSphereuntuk pasangan legacy dengan$maxDistanceradian:km / 6371.$geoWithin(keanggotaan area) tidak butuh index; seluruh keluarga “terdekat” butuh.
Mencari yang terdekat hanyalah separuh cerita — Chapter berikutnya menangani pertanyaan lawannya: siapa saja yang berada di dalam area tertentu.