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

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

بعد إتمام التثبيت، احرص على تشغيل الخدمة. في أنظمة 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 بالشكل التالي:

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 مثل:
explineargauss
كل دالة منها تتعامل مع الانخفاض في التقييم بطريقة مختلفة. وفي هذا المثال، سنستخدم الدالة linear لأنها مباشرة وسهلة الفهم.

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. ومن الناحية التقنية، كلما أحسنت ضبط التقييم والمسافات، حصلت على تجربة بحث أكثر دقة وفائدة للمستخدم النهائي.