كيفية إعداد البحث الجغرافي في تطبيقك باستخدام Elasticsearch

دقائق القراءة: 7

مقدمة: لماذا يعد البحث الجغرافي مهماً في التطبيقات الحديثة؟

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

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

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

إعداد البحث الجغرافي في التطبيقات باستخدام Elasticsearch

تثبيت Elasticsearch وتشغيله

يمكنك تثبيت Elasticsearch بسهولة من خلال دليل التثبيت الرسمي المتوفر على موقعه. في المثال الحالي، يعتمد الشرح على الإصدار 7.4.2. ومن المهم الانتباه إلى أن الإصدارات الحديثة من Elasticsearch شهدت تغييرات جوهرية، من بينها إزالة mapping types، لذلك قد تحتاج إلى بعض التعديلات إذا كنت تستخدم إصداراً مختلفاً.

واجهة توضح تثبيت Elasticsearch وتشغيل الخدمة

بعد إتمام التثبيت، احرص على تشغيل الخدمة. في أنظمة Linux، يمكنك تنفيذ الأمر التالي:

./bin/elasticsearch

وللتأكد من أن الخدمة تعمل بشكل صحيح، أرسل طلب GET إلى المنفذ 9200 على جهازك المحلي:

GET http://localhost:9200

إذا حصلت على استجابة من الخادم، فهذا يعني أن Elasticsearch جاهز للاستخدام.

إنشاء فهرس Index لتخزين المدن

في Elasticsearch، يشبه الفهرس index الجدول في قواعد البيانات التقليدية. سننشئ فهرساً باسم cities لتخزين بيانات المدن.

سنستخدم البنية التالية:

  • id: معرف من النوع keyword.
  • name: اسم المدينة من النوع text.
  • coordinate: إحداثيات المدينة من النوع geo_point.

لإنشاء الفهرس، أرسل طلب PUT كما يلي:

PUT http://localhost:9200/cities
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text"
      },
      "coordinate": {
        "type": "geo_point"
      }
    }
  }
}

إذا تم إنشاء الفهرس بنجاح، فستتلقى استجابة مشابهة لما يلي:

{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "cities"
}

بهذا أصبح الفهرس جاهزاً لاستقبال البيانات.

إضافة البيانات إلى Elasticsearch باستخدام Bulk API

الآن سنضيف مجموعة من السجلات إلى الفهرس. في Elasticsearch، تُخزَّن البيانات على شكل مستندات documents، وهي تشبه الصفوف في قواعد بيانات SQL.

ولأننا سنضيف عدة عناصر دفعة واحدة، سنستخدم واجهة Bulk API، وهي مناسبة جداً للإدخال الجماعي وتحسين الأداء.

POST http://localhost:9200/cities/_bulk
{ "index": { "_index": "cities" } }
{ "id": 1, "name": "Jakarta", "coordinate": { "lat": -6.2008, "lon": 106.8456 } }
{ "index": { "_index": "cities" } }
{ "id": 2, "name": "Tokyo", "coordinate": { "lat": 35.6762, "lon": 139.6503 } }
{ "index": { "_index": "cities" } }
{ "id": 3, "name": "Hong Kong", "coordinate": { "lat": 22.3193, "lon": 114.1694 } }
{ "index": { "_index": "cities" } }
{ "id": 4, "name": "New York", "coordinate": { "lat": 40.7128, "lon": -74.0060 } }
{ "index": { "_index": "cities" } }
{ "id": 5, "name": "Paris", "coordinate": { "lat": 48.8566, "lon": 2.3522 } }
{ "index": { "_index": "cities" } }
{ "id": 6, "name": "Bali", "coordinate": { "lat": -8.3405, "lon": 115.0920 } }
{ "index": { "_index": "cities" } }
{ "id": 7, "name": "Berlin", "coordinate": { "lat": 52.5200, "lon": 13.4050 } }
{ "index": { "_index": "cities" } }
{ "id": 8, "name": "San Fransisco", "coordinate": { "lat": 37.7749, "lon": -122.4194 } }
{ "index": { "_index": "cities" } }
{ "id": 9, "name": "Beijing", "coordinate": { "lat": 39.9042, "lon": 166.4074 } }

قد يبدو هذا التنسيق غير معتاد لأنه لا يتبع صيغة JSON التقليدية الكاملة في كامل الحمولة payload، لكن هذا أمر طبيعي في Bulk API.

بعد التنفيذ، يُفترض أن تحصل على رد مشابه لما يلي:

{
  "took": 72,
  "errors": false,
  "items": [
    ...
  ]
}

وجود القيمة false في الحقل errors يعني أن عملية الإدخال تمت بنجاح.

تنفيذ بحث جغرافي حسب المسافة

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

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

تنفيذ استعلامات البحث الجغرافي في Elasticsearch

POST http://localhost:9200/cities/_search
{
  "query": {
    "bool": {
      "filter": {
        "geo_distance": {
          "distance": "10km",
          "coordinate": {
            "lat": 37.76,
            "lon": -122.42
          }
        }
      }
    }
  }
}

هذا الاستعلام يُرجع المدينة التي تقع ضمن هذا النطاق، وهي غالباً San Fransisco وفق البيانات التي أضفناها.

مثال على الاستجابة:

{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.0,
    "hits": [
      {
        "_index": "cities",
        "_type": "_doc",
        "_id": "eKPspHYBivyIhfWHb2vl",
        "_score": 0.0,
        "_source": {
          "id": 8,
          "name": "San Fransisco",
          "coordinate": {
            "lat": 37.7749,
            "lon": -122.4194
          }
        }
      }
    ]
  }
}

توسيع نطاق البحث للحصول على نتائج أكثر

إذا أردت استرجاع مزيد من المدن ضمن نطاق أوسع، فيمكنك زيادة المسافة إلى 4500km مثلاً:

{
  "query": {
    "bool": {
      "filter": {
        "geo_distance": {
          "distance": "4500km",
          "coordinate": {
            "lat": 37.76,
            "lon": -122.42
          }
        }
      }
    }
  }
}

في هذه الحالة، ستحصل على أكثر من نتيجة، مثل New York وSan Fransisco.

{
  "took": 8,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.0,
    "hits": [
      {
        "_index": "cities",
        "_type": "_doc",
        "_id": "dKPspHYBivyIhfWHb2vl",
        "_score": 0.0,
        "_source": {
          "id": 4,
          "name": "New York",
          "coordinate": {
            "lat": 40.7128,
            "lon": -74.0060
          }
        }
      },
      {
        "_index": "cities",
        "_type": "_doc",
        "_id": "eKPspHYBivyIhfWHb2vl",
        "_score": 0.0,
        "_source": {
          "id": 8,
          "name": "San Fransisco",
          "coordinate": {
            "lat": 37.7749,
            "lon": -122.4194
          }
        }
      }
    ]
  }
}

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

ترتيب النتائج حسب القرب باستخدام function_score

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

ومن بين الدوال المتاحة داخل هذا النوع، توجد دوال الانحدار decay functions مثل:

  • exp
  • linear
  • gauss

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

رسم يوضح سلوك دوال الانحدار في ترتيب نتائج Elasticsearch الجغرافية

POST http://localhost:9200/cities/_search
{
  "query": {
    "function_score": {
      "functions": [
        {
          "linear": {
            "coordinate": {
              "origin": "37, -122",
              "offset": "100km",
              "scale": "2500km"
            }
          }
        }
      ],
      "min_score": 0.1
    }
  }
}

في هذا الاستعلام:

  • origin يحدد النقطة المرجعية التي سيتم حساب القرب منها.
  • offset يحدد نطاقاً أولياً لا يتأثر فيه التقييم كثيراً.
  • scale يحدد كيف ينخفض التقييم كلما ابتعدت النقطة عن الموقع المرجعي.
  • min_score يستبعد النتائج ذات التقييم الضعيف.

النتيجة المتوقعة ستكون مرتبة حسب أعلى score، أي أن المدينة الأقرب ستظهر أولاً:

{
  "took": 32,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "cities",
        "_type": "_doc",
        "_id": "eKPspHYBivyIhfWHb2vl",
        "_score": 1.0,
        "_source": {
          "id": 8,
          "name": "San Fransisco",
          "coordinate": {
            "lat": 37.7749,
            "lon": -122.4194
          }
        }
      },
      {
        "_index": "cities",
        "_type": "_doc",
        "_id": "dKPspHYBivyIhfWHb2vl",
        "_score": 0.19508117,
        "_source": {
          "id": 4,
          "name": "New York",
          "coordinate": {
            "lat": 40.7128,
            "lon": -74.0060
          }
        }
      }
    ]
  }
}

كما تلاحظ، أصبحت San Fransisco في المقدمة لأنها الأقرب إلى الإحداثية المرجعية مقارنةً بمدينة New York.

أفضل ممارسات عند بناء بحث جغرافي في التطبيقات

اختيار نوع البيانات المناسب

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

الفصل بين التصفية والترتيب

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

اختبار المسافات عملياً

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

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

أي خطأ في قيم lat أو lon قد يؤدي إلى نتائج غير منطقية. لذلك يجب التحقق من الإحداثيات قبل تخزينها.

الخلاصة التقنية

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

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *