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

  • $near dengan $maxDistance dan $minDistance dalam satuan meter.
  • Stage $geoNear: jarak sebagai field, distanceMultiplier, filter gabungan.
  • $nearSphere untuk 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:

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

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

  1. Error 291 sebelum index: pesan panjang itu intinya satu — unable to find index for $geoNear query. Nama internal $geoNear dipakai server untuk seluruh keluarga query “terdekat”. $geoWithin boleh tanpa index, $near tidak.
  2. $maxDistance: 200000: 200.000 meter — untuk titik GeoJSON, jarak selalu dalam meter, bukan radian. Hasilnya terurut dari terdekat: Toko Menteng (0 km) mendahului Toko Dago (±117 km).
  3. $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:

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

text
[
  { 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?
  1. near + distanceField + spherical: true: titik acuan Menteng, hasil distance dalam meter, perhitungan bola bumi — kombinasi standar untuk data GeoJSON.
  2. query: { kota: { $ne: "Jakarta" } }: filter gabungan langsung di dalam stage — toko acuan sendiri dikeluarkan agar jarak 0 tidak memenuhi daftar.
  3. distanceMultiplier: 0.001: setiap jarak dikalikan 0.001 sebelum ditulis — meter berubah menjadi kilometer, sehingga jarakKm langsung terbaca manusia.
  4. $geoNear wajib stage pertama: setelah $match sekalipun ditolak — sama ketatnya dengan $search dan $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:

OperatorKonteksSatuan jarakJarak sebagai field
$nearfind(), GeoJSONmetertidak
$geoNearstage pertama pipelinemeter (distanceMultiplier mengubah)ya, distanceField
$nearSpherefind(), pasangan legacyradiantidak

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:

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

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

  • $near mensyaratkan index geospatial — tanpa itu, error 291 unable to find index for $geoNear query.
  • Untuk GeoJSON, $maxDistance/$minDistance dalam meter; hasil otomatis terurut dari terdekat.
  • $geoNear menulis jarak ke field (distanceField), mendukung query gabungan, distanceMultiplier, dan wajib menjadi stage pertama.
  • $nearSphere untuk pasangan legacy dengan $maxDistance radian: 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.