التعامل مع GeoJSON والبيانات الجغرافية في MongoDB

التعامل مع GeoJSON والبيانات الجغرافية في MongoDB

في عالم التطبيقات الحديثة، لم تعد البيانات مجرد أسماء وأرقام ووصف نصي. كثير من التطبيقات اليوم تحتاج أن تفهم المكان: أين يقع المستخدم، ما هي أقرب نقطة خدمة، هل هذا الفرع داخل هذا النطاق الجغرافي، وهل يمكن رسم المسار الأقصر إلى موقع معين، أو إظهار جميع العناصر التي تقع داخل حدود منطقة محددة. هنا تبدأ قصة البيانات الجغرافية، وهنا يظهر MongoDB كأحد الحلول العملية والمحبوبة للتعامل مع هذا النوع من البيانات، خاصة عندما يكون بناؤك قائمًا على السرعة، والمرونة، والنمو التدريجي للتطبيق.

عندما نتحدث عن GeoJSON في MongoDB، فنحن لا نتحدث فقط عن صيغة لتخزين الإحداثيات، بل نتحدث عن طريقة تفكير كاملة في تمثيل المكان داخل قاعدة البيانات. أنت لا تخزن “نقطة” بشكل عشوائي، بل تخزنها ضمن بنية واضحة يمكن لقاعدة البيانات أن تفهمها، وتفهرسها، وتستعلم عنها بكفاءة عالية. وهذا فارق كبير جدًا بين قاعدة بيانات تحفظ البيانات، وقاعدة بيانات تستطيع أن تفهمها وتستخرج منها قيمة عملية.

الجميل في MongoDB أنه يتعامل مع البيانات الجغرافية بطريقة طبيعية جدًا داخل المستندات. فإذا كان لديك تطبيق توصيل، أو منصة عقارية، أو تطبيق خرائط، أو نظام لإدارة الفروع، أو حتى مشروع بسيط يعرض أقرب الأماكن للمستخدم، فإن MongoDB يتيح لك بناء حلول مكانية قوية دون أن تضطر إلى تعقيد البنية أكثر من اللازم. ومع ذلك، فإن النجاح في هذا المجال لا يأتي من حفظ الإحداثيات فقط، بل من فهم نوع البيانات، وطريقة الفهرسة، وطبيعة الاستعلامات الجغرافية، وكيفية اختيار البنية المناسبة من البداية.

ما هو GeoJSON ولماذا يستخدم في MongoDB؟

GeoJSON هو تنسيق قياسي مبني على JSON لتمثيل البيانات الجغرافية. فكر فيه على أنه اللغة التي تتفاهم بها الخرائط مع البيانات. بدلًا من أن تقول “هذا موقع” بشكل مجرد، أنت تصفه بنوع هندسي واضح: نقطة، خط، مضلع، أو مجموعة من هذه الأشكال. MongoDB يعتمد هذا التنسيق لأنه يسمح بتمثيل المعلومات المكانية بطريقة منظمة وقابلة للفهرسة والاستعلام.

أشهر نوع تستخدمه في التطبيقات اليومية هو Point، أي نقطة جغرافية تمثل موقعًا واحدًا بدقة معينة. لكن GeoJSON لا يتوقف عند النقاط. يمكنك أيضًا استخدام LineString لتمثيل مسار، Polygon لتمثيل مساحة مغلقة، MultiPoint وMultiLineString وMultiPolygon لتمثيل مجموعات هندسية، وأحيانًا GeometryCollection عندما تحتاج إلى تجميع أكثر من نوع هندسي في كيان واحد. هذه المرونة تجعل GeoJSON مناسبًا جدًا لمشاريع كثيرة، من أبسطها إلى أكثرها تعقيدًا.

في MongoDB، يتم تخزين الموقع عادة داخل حقل يكون منظمًا بهذه الصورة:

{
  "type": "Point",
  "coordinates": [longitude, latitude]
}

لاحظ أن الترتيب هنا مهم جدًا: أولًا خط الطول Longitude، ثم خط العرض Latitude. وهذه من أكثر الأخطاء شيوعًا عند التعامل مع GeoJSON، لأن كثيرًا من الناس يكتبونها بالعكس بسبب الاعتياد على قراءة الإحداثيات بشكل مختلف. في GeoJSON وMongoDB، الترتيب الصحيح هو دائمًا [lng, lat].

لماذا MongoDB مناسب جدًا للبيانات الجغرافية؟

السبب الأول هو المرونة. MongoDB قاعدة بيانات مستندية، وهذا يعني أنك تستطيع أن تحفظ معها بالإضافة إلى الموقع كل ما يحتاجه التطبيق: اسم المكان، وصفه، نوعه، ساعات العمل، صور، تقييمات، وسمة جغرافية واحدة أو أكثر. لا تحتاج إلى جداول كثيرة وعلاقات معقدة كي تحتفظ ببيانات الموقع. هذا وحده يختصر الكثير من التعقيد.

السبب الثاني هو دعم الاستعلامات الجغرافية بشكل مباشر. MongoDB يقدم أدوات مثل $geoNear و$geoWithin و$geoIntersects، وهي تسمح لك بالبحث عن الأقرب، أو داخل منطقة معينة، أو المتقاطع مع شكل هندسي محدد. وهذا يجعل قاعدة البيانات نفسها تشارك في العمل بدلًا من أن تنقل كل البيانات إلى التطبيق ثم تحسب المسافات داخله، وهو أمر مكلف وبطيء عندما تكبر البيانات.

السبب الثالث هو الأداء، بشرط أن تستخدم الفهارس الجغرافية المناسبة. MongoDB لا يعتمد فقط على وجود الموقع، بل على بناء فهرس geospatial يسمح بالوصول السريع جدًا إلى النتائج القريبة أو الواقعة ضمن مساحة محددة. من دون هذا الفهرس، قد تتحول الاستعلامات الجغرافية إلى عمليات بطيئة جدًا عند ازدياد حجم البيانات.

السبب الرابع هو التكامل الممتاز مع تطبيقات الويب والموبايل. سواء كنت تستخدم Node.js أو Laravel أو Python أو Go، فإن MongoDB يسهل عليك إدخال GeoJSON والاستعلام عنه وإرساله إلى الواجهة الأمامية التي قد تعرضه على خريطة Google Maps أو Leaflet أو Mapbox.

فهم أنواع البيانات الجغرافية في GeoJSON

قبل أن ندخل في الكود، من المهم أن نفهم الأنواع الأساسية التي ستتعامل معها. هذا الفهم ليس أكاديميًا فقط، بل عملي جدًا لأن كل نوع له استخدامه المناسب.

1) Point

تمثل موقعًا واحدًا على الخريطة. هذا هو النوع الأكثر استخدامًا في التطبيقات اليومية. مثال: موقع فرع، موقع مستخدم، موقع سائق، أو موقع مطعم.

{
  "type": "Point",
  "coordinates": [31.633, 11.456]
}

2) LineString

تمثل خطًا مكوّنًا من مجموعة نقاط. تستخدم غالبًا للمسارات، الطرق، أو تتبع حركة.

{
  "type": "LineString",
  "coordinates": [
    [31.633, 11.456],
    [31.640, 11.460],
    [31.650, 11.470]
  ]
}

3) Polygon

تمثل مساحة مغلقة. مفيدة جدًا لحدود الأحياء، المدن، المناطق الإدارية، أو نطاق التوصيل.

{
  "type": "Polygon",
  "coordinates": [
    [
      [31.600, 11.430],
      [31.700, 11.430],
      [31.700, 11.500],
      [31.600, 11.500],
      [31.600, 11.430]
    ]
  ]
}

4) MultiPoint وMultiLineString وMultiPolygon

هذه الأنواع مفيدة عندما تريد تخزين أكثر من شكل من النوع نفسه في كيان واحد. مثل مجموعة فروع، أو مسارات متعددة، أو عدة مناطق خدمة.

5) GeometryCollection

نوع يجمع أكثر من نوع هندسي داخل بنية واحدة. يُستخدم في الحالات المتقدمة، لكنه ليس الخيار الأول عادةً في المشاريع اليومية.

بنية المستند الجغرافي داخل MongoDB

من أفضل مميزات MongoDB أن الموقع ليس شيئًا معزولًا، بل جزء من المستند. هذا يعني أنك قد تخزن بيانات المكان بهذه الصورة:

{
  "_id": "66a1f1c2e9b1a4c9a0e12345",
  "name": "Café Atlas",
  "category": "cafe",
  "address": "Rabat",
  "location": {
    "type": "Point",
    "coordinates": [-6.8498, 34.0209]
  },
  "rating": 4.7,
  "isOpen": true,
  "createdAt": "2026-06-12T10:00:00Z"
}

هنا نلاحظ أن الموقع جزء من نفس الوثيقة. وهذه نقطة مهمة جدًا، لأنها تجعل القراءة أسرع وأبسط في كثير من السيناريوهات. عندما تبحث عن أقرب مقهى للمستخدم، فأنت تحتاج أن تسترجع الاسم والموقع والتقييم وربما الصورة أيضًا، وليس مجرد الإحداثيات.

الأسلوب الصحيح عادة هو أن تجعل حقل الموقع واضحًا وثابتًا في كل الوثائق المتشابهة، لأن الفهارس الجغرافية تعتمد على تنسيق متوقع. لا تضع الموقع أحيانًا كنص وأحيانًا ككائن وأحيانًا كمصفوفة منفصلة، لأن هذا سيؤدي إلى ارتباك في الاستعلامات لاحقًا.

إنشاء فهرس جغرافي في MongoDB

الاستعلامات الجغرافية تصبح فعّالة جدًا عندما تضيف الفهرس المناسب. في MongoDB، يمكنك إنشاء فهرس 2dsphere للحقل الجغرافي الذي يستخدم GeoJSON.

db.places.createIndex({ location: "2dsphere" })

هذا الفهرس يخبر MongoDB أن الحقل location يحتوي على بيانات جغرافية على سطح الكرة الأرضية. وهذا مهم لأن الأرض ليست سطحًا مستويًا تمامًا، والاستعلامات الجغرافية تحتاج إلى حسابات تأخذ هذا في الاعتبار.

إذا كنت تستخدم MongoDB مع Mongoose في Node.js، يمكنك تعريف الفهرس داخل الـ schema كالتالي:

const mongoose = require('mongoose');

const placeSchema = new mongoose.Schema({
  name: String,
  category: String,
  rating: Number,
  location: {
    type: {
      type: String,
      enum: ['Point'],
      required: true
    },
    coordinates: {
      type: [Number],
      required: true
    }
  }
});

placeSchema.index({ location: '2dsphere' });

module.exports = mongoose.model('Place', placeSchema);

وجود الفهرس هنا ليس تفصيلًا ثانويًا، بل هو ما يجعل بحثك عن الأماكن القريبة ذا معنى عملي. بدونه، قد يعمل الكود، لكن الأداء سيكون ضعيفًا جدًا عندما تكبر البيانات.

أول إدخال عملي: حفظ مكان جديد

لنأخذ مثالًا مباشرًا بلغة Node.js باستخدام MongoDB Native Driver. لنفترض أننا نريد حفظ مطعم جديد:

const { MongoClient } = require('mongodb');

async function run() {
  const client = new MongoClient('mongodb://localhost:27017');
  await client.connect();

  const db = client.db('geo_app');
  const places = db.collection('places');

  await places.createIndex({ location: '2dsphere' });

  const result = await places.insertOne({
    name: 'Al Andalus Restaurant',
    category: 'restaurant',
    location: {
      type: 'Point',
      coordinates: [-6.8416, 34.0201]
    },
    address: 'Rabat',
    rating: 4.5
  });

  console.log('Inserted:', result.insertedId);
  await client.close();
}

run().catch(console.error);

لاحظ في هذا المثال أننا نستخدم إحداثيات واضحة وصحيحة. نقطة المطعم هنا محفوظة ضمن GeoJSON، والفهرس الجغرافي مهيأ مسبقًا. هذه الخطوة البسيطة تفتح أمامك بعد ذلك باب الاستعلامات القوية.

البحث عن الأقرب باستخدام $near و$geoNear

من أكثر الاستعلامات شيوعًا في التطبيقات الجغرافية هو البحث عن الأقرب. المستخدم يفتح التطبيق، ويطلب “أقرب مطعم”، أو “أقرب مركز خدمة”، أو “أقرب صيدلية”. هنا يأتي دور $near و$geoNear.

باستخدام find مع $near

const nearbyPlaces = await places.find({
  location: {
    $near: {
      $geometry: {
        type: 'Point',
        coordinates: [-6.848, 34.02]
      },
      $maxDistance: 2000
    }
  }
}).toArray();

console.log(nearbyPlaces);

هذا الاستعلام يبحث عن الأماكن القريبة من نقطة محددة، بحد أقصى 2000 متر. المهم هنا أن MongoDB يفهم المسافة بالمتر عندما تستخدم 2dsphere.

باستخدام aggregation مع $geoNear

const nearby = await places.aggregate([
  {
    $geoNear: {
      near: {
        type: 'Point',
        coordinates: [-6.848, 34.02]
      },
      distanceField: 'distance',
      spherical: true,
      maxDistance: 2000,
      query: { category: 'restaurant' }
    }
  }
]).toArray();

console.log(nearby);

هذا الأسلوب أقوى عندما تريد ترتيب النتائج حسب المسافة، أو عندما تحتاج إلى تضمين المسافة نفسها داخل النتائج. ستلاحظ أن MongoDB أضافت حقلًا اسمه distance لكل نتيجة. وهذا مفيد جدًا عند عرض أقرب النتائج أولًا.

العثور على العناصر داخل منطقة معينة

ليس كل بحث جغرافي يعتمد على النقطة الأقرب. أحيانًا تريد جميع العناصر التي تقع داخل منطقة معينة. مثل: كل الفروع داخل مدينة، كل العناوين داخل نطاق توصيل، كل العقارات ضمن حي محدد.

هنا يستخدم MongoDB الاستعلام $geoWithin.

const insideArea = await places.find({
  location: {
    $geoWithin: {
      $geometry: {
        type: 'Polygon',
        coordinates: [[
          [-6.90, 34.00],
          [-6.80, 34.00],
          [-6.80, 34.05],
          [-6.90, 34.05],
          [-6.90, 34.00]
        ]]
      }
    }
  }
}).toArray();

console.log(insideArea);

في هذا المثال، نحن نبحث عن كل الأماكن الواقعة داخل مضلع جغرافي محدد. يمكن أن يمثل هذا المضلع منطقة خدمة، أو حدود حي، أو مساحة معينة تحتاج أن تتحقق منها.

واحدة من النقاط المهمة هنا هي أن الدقة الجغرافية تعتمد على صحة المضلع. إذا كانت النقاط مرتبة بشكل خاطئ، أو لم تُغلق الحلقة بآخر نقطة تعود إلى الأولى، فقد تحصل على نتائج غير متوقعة. لذلك، عند بناء مناطق جغرافية منطقية، من الأفضل التحقق منها بصريًا على الخريطة قبل الاعتماد عليها داخل التطبيق.

التحقق من التقاطع مع حدود جغرافية باستخدام $geoIntersects

أحيانًا لا تريد معرفة ما إذا كانت النقطة داخل المنطقة، بل تريد معرفة ما إذا كان الشكل الجغرافي يتقاطع مع شكل آخر. مثلًا: هل هذا الطريق يمر داخل منطقة معينة؟ هل هذا المسار يتقاطع مع الحدود المرسومة؟ هل موقع الخدمة يلامس نطاق التوصيل؟

هنا يأتي $geoIntersects.

const intersects = await places.find({
  location: {
    $geoIntersects: {
      $geometry: {
        type: 'Polygon',
        coordinates: [[
          [-6.90, 34.00],
          [-6.80, 34.00],
          [-6.80, 34.05],
          [-6.90, 34.05],
          [-6.90, 34.00]
        ]]
      }
    }
  }
}).toArray();

الفكرة هنا أن MongoDB يبحث عن العناصر التي تتقاطع هندسيًا مع الشكل المحدد. هذا النوع من الاستعلامات مهم جدًا في التطبيقات اللوجستية، والتخطيط العمراني، وإدارة الموارد، وحتى في ألعاب تعتمد على المكان.

احتساب المسافة بين نقطة وأخرى

عندما تعرض النتائج للمستخدم، فإن مجرد وجود المواقع القريبة لا يكفي أحيانًا. المستخدم يريد أن يعرف كم يبعد المكان عنه. MongoDB تستطيع مساعدتك هنا أيضًا.

باستخدام $geoNear، يمكنك تخزين المسافة في النتائج:

const results = await places.aggregate([
  {
    $geoNear: {
      near: {
        type: 'Point',
        coordinates: [-6.848, 34.02]
      },
      distanceField: 'distance',
      spherical: true
    }
  },
  { $limit: 10 }
]).toArray();

results.forEach(place => {
  console.log(place.name, place.distance);
});

يمكنك بعد ذلك تحويل المسافة إلى كيلومترات في التطبيق:

const km = place.distance / 1000;

هذا بسيط جدًا، لكنه يخلق تجربة مستخدم أكثر وضوحًا، لأن عرض “1.8 كم” أفضل بكثير من عرض قيمة خام غير مفهومة.

التعامل مع GeoJSON في Mongoose

Mongoose يجعل العمل أكثر تنظيمًا، خاصة إذا كان تطبيقك كبيرًا أو فريقك متعدد الأشخاص. يمكنك تعريف المخطط الجغرافي بشكل منظم:

const mongoose = require('mongoose');

const locationSchema = new mongoose.Schema({
  type: {
    type: String,
    enum: ['Point'],
    required: true
  },
  coordinates: {
    type: [Number],
    required: true
  }
}, { _id: false });

const storeSchema = new mongoose.Schema({
  name: { type: String, required: true },
  city: String,
  openingHours: String,
  location: { type: locationSchema, required: true }
});

storeSchema.index({ location: '2dsphere' });

const Store = mongoose.model('Store', storeSchema);

ثم يمكنك الإدخال بهذه الصورة:

await Store.create({
  name: 'Tech Corner',
  city: 'Nador',
  openingHours: '09:00 - 22:00',
  location: {
    type: 'Point',
    coordinates: [-3.003, 35.168]
  }
});

ويمكنك البحث عن الأقرب:

const stores = await Store.aggregate([
  {
    $geoNear: {
      near: { type: 'Point', coordinates: [-3.003, 35.168] },
      distanceField: 'distance',
      spherical: true
    }
  }
]);

هذا الأسلوب يضيف مستوى من الوضوح والتنظيم، ويجعل بنية البيانات الجغرافية أسهل في الصيانة على المدى الطويل.

أكثر الأخطاء شيوعًا عند استخدام GeoJSON

كثير من المشاكل التي تواجه المطورين في البيانات الجغرافية لا تأتي من MongoDB نفسها، بل من تفاصيل صغيرة جدًا. وهذه التفاصيل الصغيرة قد تستهلك وقتًا طويلًا إذا لم تنتبه لها من البداية.

1) عكس ترتيب الإحداثيات

الخطأ الأشهر هو كتابة [latitude, longitude] بدل [longitude, latitude]. هذا الخطأ يجعل الموقع يظهر في مكان خاطئ تمامًا على الخريطة. أحيانًا يكون الخطأ “صامتًا”، أي أن الكود يعمل لكن النتائج تكون غير منطقية.

2) نسيان إنشاء الفهرس الجغرافي

من دون 2dsphere ستفقد جزءًا كبيرًا من قوة MongoDB في هذا المجال. قد تعمل الاستعلامات لكنها لن تكون فعالة.

3) استخدام نوع هندسي غير مناسب

إذا كانت البيانات مجرد نقطة، فلا داعي لاستخدام Polygon. وإذا كنت تمثل منطقة، فلا تكتفِ بـ Point. اختيار النوع الصحيح يوفر عليك مشاكل لاحقة.

4) عدم التحقق من صحة البيانات

قد تأتي الإحداثيات من واجهة المستخدم، من جهاز GPS، أو من خدمة خارجية. لذلك يجب دائمًا التحقق من أن القيم رقمية، وأن longitude وlatitude ضمن الحدود الصحيحة.

5) تجاهل دقة المسافات

عند التعامل مع تطبيقات تعتمد على الموقع، قد تكون الدقة مهمة جدًا. أحيانًا فرق صغير في الإحداثيات يغير النتيجة. لهذا السبب يجب اختبار الاستعلامات على أكثر من نقطة حقيقية.

التحقق من صحة الإحداثيات قبل الحفظ

من الأفضل دائمًا التحقق من القيم قبل إدخالها إلى قاعدة البيانات. مثال بسيط في Node.js:

function isValidCoordinates(lng, lat) {
  return (
    typeof lng === 'number' &&
    typeof lat === 'number' &&
    lng >= -180 && lng <= 180 &&
    lat >= -90 && lat <= 90
  );
}

const lng = -6.848;
const lat = 34.02;

if (!isValidCoordinates(lng, lat)) {
  throw new Error('Invalid coordinates');
}

هذه خطوة صغيرة، لكنها تمنع كثيرًا من الأخطاء الغامضة التي تظهر لاحقًا في البحث الجغرافي.

استخدام البيانات الجغرافية في تطبيق توصيل

لنأخذ مثالًا عمليًا جدًا: تطبيق توصيل طعام. لديك مطاعم، وسائقون، ومستخدم يطلب الطعام من موقعه الحالي. هنا تبدأ MongoDB في لعب دور مهم.

أنت تستطيع تخزين موقع كل مطعم:

{
  "name": "Sea View Pizza",
  "location": {
    "type": "Point",
    "coordinates": [-6.8502, 34.0187]
  }
}

وتخزين موقع المستخدم:

{
  "userId": 123,
  "location": {
    "type": "Point",
    "coordinates": [-6.848, 34.02]
  }
}

ثم تبحث عن أقرب المطاعم:

const restaurants = await Restaurant.aggregate([
  {
    $geoNear: {
      near: {
        type: 'Point',
        coordinates: [-6.848, 34.02]
      },
      distanceField: 'distance',
      spherical: true,
      query: { isOpen: true }
    }
  },
  { $limit: 5 }
]);

وبعد ذلك يمكنك ترتيب السائقين الأقرب إلى المطعم أو المستخدم. هذه البنية البسيطة قد تكون أساس نظام كامل يحقق قيمة عملية كبيرة جدًا.

استخدام البيانات الجغرافية في تطبيق عقاري

في التطبيقات العقارية، الموقع هو جزء أساسي من القيمة. العقار لا يُقاس فقط بمساحته وعدد غرفه، بل أيضًا بمكانه. وهنا تصبح GeoJSON في MongoDB أداة ممتازة.

يمكنك تخزين كل عقار مع موقعه، ثم البحث عن العقارات داخل نطاق معين، أو الأقرب إلى مدرسة أو محطة نقل أو مركز مدينة.

const properties = await Property.find({
  location: {
    $geoWithin: {
      $centerSphere: [[-6.848, 34.02], 10 / 6378.1]
    }
  }
});

هذا المثال يستخدم دائرة جغرافية حول نقطة معينة. القيمة 10 / 6378.1 تمثل تقريبًا دائرة بنصف قطر 10 كيلومترات على سطح الأرض. هذا النوع من الاستعلامات مفيد جدًا عندما يريد المستخدم رؤية العقارات ضمن نطاق زمني أو مكاني محدد.

استخدام البيانات الجغرافية في نظام فروع

إذا كنت تدير شركة لديها فروع متعددة، فالسؤال الشائع هو: أي فرع هو الأقرب للعميل؟ وأي فروع تقع داخل مدينة معينة؟ وهل يوجد فرع داخل هذا الحي؟

يمكنك حفظ كل فرع كموقع Point، ثم استخدام $near للعثور على الأقرب. وإذا كانت لديك مناطق خدمة لكل فرع، يمكنك حفظها كمضلعات Polygon، ثم استخدام $geoWithin أو $geoIntersects للتحقق من التغطية.

هذا الأسلوب يختصر الوقت ويجعل قرارات العمل أكثر ذكاءً. بدلًا من أن يعتمد الموظف على التخمين، تصبح قاعدة البيانات نفسها جزءًا من عملية اتخاذ القرار.

ربط MongoDB مع الخرائط في الواجهة الأمامية

غالبًا لن يكتمل العمل الجغرافي إلا إذا عرضت النتائج على خريطة. يمكنك مثلًا إرسال المستندات من MongoDB إلى واجهة تستخدم Leaflet:

const places = await fetch('/api/nearby-places').then(res => res.json());

places.forEach(place => {
  L.marker([place.location.coordinates[1], place.location.coordinates[0]])
    .addTo(map)
    .bindPopup(place.name);
});

هنا لاحظ شيئًا مهمًا: الخرائط الأمامية غالبًا تتوقع [latitude, longitude] عند العرض، بينما GeoJSON يخزن [longitude, latitude]. لذلك عند تحويل البيانات إلى الخريطة، يجب قلب الترتيب بعناية. هذه النقطة بالذات تسبب كثيرًا من الحيرة عند المطورين في المرة الأولى.

تخزين المسارات بدلًا من النقاط فقط

إذا كان تطبيقك يحتاج إلى تتبع المسار، مثل تطبيق رياضي أو خدمة توصيل أو تتبع أسطول، فإن LineString يصبح مناسبًا جدًا.

{
  "type": "LineString",
  "coordinates": [
    [-6.850, 34.020],
    [-6.851, 34.022],
    [-6.853, 34.025],
    [-6.855, 34.028]
  ]
}

يمكنك استخدام هذا النوع لتمثيل الطريق الذي سلكه السائق، أو المسار الذي قطعه المستخدم أثناء التنقل، أو حتى مسار رحلة سياحية. وفي بعض الحالات، قد تخزن أيضًا الوقت عند كل نقطة، لكن ذلك يعتمد على تصميم النظام.

تخزين المناطق كـ Polygon

إذا كان لديك نطاق توصيل، أو منطقة خدمة، أو حدود إدارية، فمن الأفضل حفظها كمضلع. هذا أفضل بكثير من الاكتفاء بقائمة مدن أو أسماء أحياء بشكل نصي، لأن الشكل الهندسي يعطيك استعلامًا مكانيًا حقيقيًا.

مثال لنطاق توصيل:

const deliveryZone = {
  type: 'Polygon',
  coordinates: [[
    [-6.90, 34.00],
    [-6.80, 34.00],
    [-6.78, 34.03],
    [-6.83, 34.06],
    [-6.90, 34.05],
    [-6.90, 34.00]
  ]]
};

ثم يمكنك التحقق مما إذا كان موقع المستخدم داخل هذه المنطقة:

const isInsideZone = await DeliveryZone.findOne({
  area: {
    $geoWithin: {
      $geometry: {
        type: 'Point',
        coordinates: [-6.848, 34.02]
      }
    }
  }
});

أو بالعكس، يمكنك التحقق من المنطقة نفسها إذا كانت تحتوي النقطة.

تحسين الأداء في الاستعلامات الجغرافية

عندما تبدأ قاعدة البيانات تكبر، تصبح بعض التفاصيل مهمة جدًا. أولها الفهرس، وقد تحدثنا عنه. لكن هناك أيضًا نقاط أخرى تساعد على تحسين الأداء.

أولًا، لا تجرِ الاستعلامات الجغرافية من دون حدود واضحة ما أمكن. استخدام $maxDistance أو التصفية المبدئية query داخل $geoNear يساعد على تقليل النتائج.

ثانيًا، حاول ألا تحفظ عددًا هائلًا من التفاصيل غير الضرورية داخل نفس المستند إذا كانت لا تُستخدم في معظم الاستعلامات الجغرافية. المستند الكبير جدًا قد يبطئ القراءة في بعض السيناريوهات.

ثالثًا، إذا كان لديك أكثر من حقل جغرافي في نفس المستند، كن واضحًا في تسمية كل حقل. مثل branchLocation وwarehouseLocation وserviceArea. هذا يمنع الارتباك.

رابعًا، اختبر الاستعلامات على بيانات حقيقية أو قريبة من الحقيقة. البيانات الجغرافية حساسة جدًا لمستوى التوزيع الجغرافي. ما يبدو سريعًا في بيانات صغيرة قد يتغير عندما تنتقل إلى آلاف أو ملايين السجلات.

دمج GeoJSON مع واجهات REST API

إذا كنت تبني API، فمن الأفضل أن تجعل صيغة الإخراج واضحة وموحدة. مثال:

app.get('/api/places/nearby', async (req, res) => {
  const { lng, lat } = req.query;

  const places = await Place.aggregate([
    {
      $geoNear: {
        near: {
          type: 'Point',
          coordinates: [Number(lng), Number(lat)]
        },
        distanceField: 'distance',
        spherical: true,
        maxDistance: 5000
      }
    },
    { $limit: 20 }
  ]);

  res.json(places);
});

بهذه الطريقة، يستطيع تطبيق الموبايل أو الواجهة الأمامية طلب أقرب الأماكن وإظهارها مباشرة على الخريطة أو في قائمة مرتبة.

مثال كامل من البداية إلى النهاية

لنجمع الفكرة كلها في مثال عملي متكامل. سنقوم بإنشاء نموذج بسيط لمخزن بيانات للأماكن، ثم نضيف فهرسًا جغرافيًا، ثم نبحث عن الأماكن القريبة، ثم نرسلها إلى العميل.

نموذج MongoDB

const placeSchema = new mongoose.Schema({
  name: String,
  type: String,
  location: {
    type: {
      type: String,
      enum: ['Point'],
      required: true
    },
    coordinates: {
      type: [Number],
      required: true
    }
  }
});

placeSchema.index({ location: '2dsphere' });

إدخال بيانات

await Place.insertMany([
  {
    name: 'North Pharmacy',
    type: 'pharmacy',
    location: {
      type: 'Point',
      coordinates: [-6.846, 34.021]
    }
  },
  {
    name: 'Green Market',
    type: 'market',
    location: {
      type: 'Point',
      coordinates: [-6.852, 34.017]
    }
  },
  {
    name: 'Ocean Clinic',
    type: 'clinic',
    location: {
      type: 'Point',
      coordinates: [-6.840, 34.025]
    }
  }
]);

البحث عن الأقرب

const currentUserLocation = [-6.848, 34.02];

const nearestPlaces = await Place.aggregate([
  {
    $geoNear: {
      near: {
        type: 'Point',
        coordinates: currentUserLocation
      },
      distanceField: 'distance',
      spherical: true
    }
  },
  { $limit: 10 }
]);

عرض النتائج بشكل مفهوم

nearestPlaces.map(place => ({
  name: place.name,
  type: place.type,
  distanceInKm: (place.distance / 1000).toFixed(2)
}));

بهذا الشكل تصبح البيانات الجغرافية واضحة من لحظة الحفظ حتى لحظة العرض.

عندما تكون البيانات من Google Maps أو GPS

كثير من المشاريع لا تنشئ الإحداثيات يدويًا، بل تستقبلها من خدمات خارجية مثل GPS أو Google Maps أو واجهات الخرائط المختلفة. وهنا يجب الانتباه إلى أن كل خدمة قد تعرض البيانات بصيغة مختلفة في الواجهة، لكنها غالبًا تتحول داخليًا إلى نفس المبدأ: longitude وlatitude.

ما عليك سوى التأكد من أن القيمة القادمة إلى MongoDB متوافقة مع GeoJSON. فإذا وصلت الإحداثيات من الواجهة بشكل lat, lng، قم بإعادة ترتيبها قبل الحفظ:

const lat = 34.02;
const lng = -6.848;

const geojsonLocation = {
  type: 'Point',
  coordinates: [lng, lat]
};

هذا التحويل البسيط هو من أكثر الخطوات تأثيرًا على صحة البيانات.

التحقق من داخل دائرة باستخدام $centerSphere

أحيانًا تحتاج إلى البحث عن العناصر داخل دائرة نصف قطرها محدد حول نقطة. هذا مفيد مثلًا لتطبيقات “الأقرب خلال 5 كم”.

const placesWithinRadius = await places.find({
  location: {
    $geoWithin: {
      $centerSphere: [[-6.848, 34.02], 5 / 6378.1]
    }
  }
}).toArray();

هذه الطريقة مناسبة عندما تكون لديك دائرة واضحة بدل مضلع. ومع الوقت ستجد أن اختيارك بين Polygon وCircle يعتمد على طبيعة المشكلة الحقيقية لا على الجانب التقني فقط.

تمثيل أكثر من موقع داخل نفس الوثيقة

في بعض التطبيقات، قد تحتاج إلى أكثر من موقع داخل نفس المستند. مثل شركة لديها المقر الرئيسي، ومستودعات، وفروع. في هذه الحالة يمكنك استخدام حقول متعددة:

{
  "companyName": "Atlas Logistics",
  "headOffice": {
    "type": "Point",
    "coordinates": [-6.849, 34.021]
  },
  "warehouse": {
    "type": "Point",
    "coordinates": [-6.860, 34.015]
  },
  "serviceArea": {
    "type": "Polygon",
    "coordinates": [[
      [-6.90, 34.00],
      [-6.80, 34.00],
      [-6.80, 34.05],
      [-6.90, 34.05],
      [-6.90, 34.00]
    ]]
  }
}

هذا التصميم مرن جدًا، لكنه يحتاج إلى تسمية دقيقة للفصول الجغرافية داخل المستند حتى لا تتداخل الأدوار.

متى تستخدم GeoJSON ومتى لا تستخدمه؟

GeoJSON ممتاز عندما يكون الموقع جزءًا جوهريًا من البيانات، وعندما تحتاج إلى استعلامات مكانية أو عرض على الخريطة أو حساب قرب/بعد أو التحقق من تقاطع. لكنه ليس ضروريًا في كل مكان. إذا كنت تخزن مدينة فقط كنص ولا تحتاج إلى أي استعلام مكاني، فقد يكون تخزين اسم المدينة كقيمة نصية كافيًا. أما إذا كنت تخزن “موقعًا” فعليًا أو تحتاج إلى التعامل الجغرافي، فGeoJSON هو الخيار الصحيح غالبًا.

القاعدة العملية البسيطة هي:
إذا كان هناك معنى “لأين يقع هذا الشيء؟” في منطق التطبيق، ففكّر في GeoJSON منذ البداية.

نصائح من واقع المشاريع الحقيقية

هناك بعض الدروس التي تتكرر كثيرًا في المشاريع الواقعية، ويستفيد منها أي مطور يعمل مع MongoDB والبيانات الجغرافية.

أولًا، اكتب وثائق واضحة لفريقك حول ترتيب الإحداثيات. هذا وحده يمنع الكثير من الأخطاء.

ثانيًا، اجعل اختبارك يدويًا على الخريطة جزءًا من التطوير، وليس فقط اختبارًا آليًا خلف الكواليس. أحيانًا ترى المشكلة فورًا بمجرد ظهور العلامة في المكان الخطأ.

ثالثًا, لا تؤجل إضافة الفهرس الجغرافي إلى نهاية المشروع. أضفه مبكرًا حتى تبني على أساس صحيح.

رابعًا، فكّر في مستقبل البيانات. هل ستحتاج لاحقًا إلى مناطق خدمة؟ هل ستحتاج مسارات؟ هل يكفيك Point أم ستحتاج Polygon؟ التصميم المبكر الجيد يوفر الكثير من الوقت لاحقًا.

خامسًا، عند العمل مع فرق متعددة، ضع طبقة خدمة أو Repository تتولى التعامل مع البيانات الجغرافية، بدل أن تتناثر العمليات داخل أكثر من مكان في التطبيق.

خاتمة إنسانية صغيرة

البيانات الجغرافية تبدو أحيانًا كأنها عالم معقد، مليء بالإحداثيات، والمضلعات، والمسافات، والفهارس. لكن الحقيقة أنها تصبح بسيطة جدًا عندما تنظر إليها من منظور عملي: أنت فقط تحاول أن تجعل قاعدة البيانات تفهم المكان كما يفهمه البشر. أين أقرب شيء؟ ما الذي يقع داخل هذه المنطقة؟ ماذا يتقاطع مع هذا الحد؟ كم يبعد هذا الموقع عني؟

MongoDB مع GeoJSON يقدمان لك لغة ممتازة للإجابة عن هذه الأسئلة داخل التطبيق نفسه، بدل أن تتركها منطقًا منفصلًا ومُرهقًا في طبقة التطبيق. وعندما تستخدمهما بشكل صحيح، ستشعر أن البيانات الجغرافية لم تعد عبئًا تقنيًا، بل أصبحت ميزة قوية تضيف قيمة حقيقية للمستخدم النهائي.

في النهاية، أي تطبيق يعتمد على الموقع يحتاج إلى ثلاث أشياء: تمثيل صحيح، فهرسة صحيحة، واستعلام صحيح. وإذا اجتمعت هذه الثلاثة، فسيصبح GeoJSON في MongoDB أداة فعالة جدًا لبناء تجارب مكانية دقيقة وسريعة وسهلة التوسع.

#البيانات الجغرافية #geospatial queries #geospatial index #$geoNear #$geoWithin #$geoIntersects #MongoDB GIS #خرائط #مواقع جغرافية #تطبيقات تعتمد على الموقع #Node.js #Mongoose

اشترك في نشرتنا البريدية

12k+

المشتركون

أسبوعيًا

التكرار

مجاني

دائمًا