يا جماعة الخير، السلام عليكم ورحمة الله. اسمحولي اليوم أحكيلكم قصة صارت معي قبل كم سنة، قصة علمتني درس ما بنساه في البرمجة وتحسين الأداء.
كنا وقتها شغالين على تطبيق اجتماعي جديد، فكرته بسيطة: تربط الناس ببعضها بناءً على الاهتمامات المشتركة والموقع الجغرافي. ومن أهم الميزات، طبعًا، كانت ميزة “الأشخاص القريبون منك”. في البداية، ومع قاعدة بيانات صغيرة فيها مية ولا ميتين مستخدم للتجربة، كانت الأمور “لوز”، ماشية زي الحلاوة. الكود كان بسيط وواضح: لما المستخدم يفتح الشاشة، بنجيب موقعه، وبنلف على كل المستخدمين في قاعدة البيانات، نحسب المسافة بينه وبين كل واحد فيهم، وبنعرضله اللي ضمن نطاق 5 كيلومتر.
شعرت وقتها بفخر المبرمج اللي “بيخلّص شغله” بسرعة. لكن الفرحة، يا أحبائي، لم تدم طويلاً. في يوم الإطلاق التجريبي، ومع أول دفعة من المستخدمين الحقيقيين (حوالي 10 آلاف مستخدم)، صارت الكارثة. التطبيق صار “يعلّق”، والخوادم بتصيح من الضغط، والمعالج (CPU) واصل 100% ومش راضي ينزل. وصلني اتصال من مدير المشروع، وصوته كان كفيل يوضح حجم المصيبة: “شو يا أبو عمر؟! التطبيق ميت! الناس مش قادرين يفتحوا صفحة ‘القريبون مني’!”.
هنا بدأت رحلتي المحمومة لاكتشاف سبب هذا الجحيم، وكيف أنقذتني خوارزمية بسيطة وعبقرية اسمها “Geohash”.
المشكلة: جحيم استعلامات المسافة
لما رجعت للكود، كان واضح وين المشكلة. المشكلة ما كانت في الكود نفسه، بل في “الفكرة” من وراه. الطريقة اللي كنت بستخدمها، واللي بيسموها “Brute Force” أو القوة الغاشمة، كانت كارثية على نطاق واسع.
تخيل معي السيناريو: عندك 100,000 مستخدم في قاعدة البيانات. كل مرة مستخدم واحد بيطلب يشوف مين جنبه، الخادم المسكين كان مضطر يعمل التالي:
- يسحب كل الـ 100,000 مستخدم من قاعدة البيانات.
- لكل مستخدم منهم، يقوم بعملية حسابية معقدة (باستخدام معادلة هافرساين – Haversine formula) لحساب المسافة على سطح الكرة الأرضية.
- يقارن النتيجة مع المسافة المطلوبة (مثلاً 5 كم).
- يجمع النتائج ويرجعها للمستخدم.
هذا يعني 100,000 عملية حسابية لكل طلب واحد! ولو 10 مستخدمين طلبوا بنفس الثانية، فإنت بتحكي عن مليون عملية حسابية! المشكلة الأكبر كانت إن الفهرسة (Indexing) على أعمدة خطوط الطول (longitude) والعرض (latitude) تقريبًا ما إلها أي فايدة في هاي الحالة، لأن الاستعلام مش بسيط (مثل `WHERE city = ‘Amman’`)، بل هو استعلام مكاني معقد على بُعدين. قاعدة البيانات كانت بتعمل “Full Table Scan”، يعني بتمر على كل سجل في الجدول، وهذا هو التعريف الحرفي لـ “جحيم الأداء”.
نصيحة من أبو عمر: القاعدة الأولى في تحسين الأداء هي “لا تقم بعمل لا داعي له”. سؤالي لنفسي كان: هل أنا مضطر أحسب المسافة مع شخص في أمريكا عشان أظهر لناس قريبين مني في عمّان؟ طبعًا لأ. كان لازم ألاقي طريقة أستثني فيها الـ 99.9% من البيانات البعيدة قبل ما أبدأ أي حسابات.
وميض الأمل: فك شيفرة الـ Geohash
بعد ساعات من البحث والقراءة عن “Spatial Indexes” والحلول المعقدة مثل R-trees و Quad-trees (اللي غالبًا بتحتاج قواعد بيانات متخصصة زي PostGIS)، وقعت عيني على مصطلح “Geohash”. الفكرة بدت بسيطة لدرجة شككت فيها في البداية، لكنها كانت عبقرية بشكل لا يصدق.
الفكرة الأساسية: تحويل الخريطة إلى شبكة نصّية
خوارزمية Geohash بتقوم بفعل شيء واحد بسيط وساحر: تحويل إحداثيات (خط طول، خط عرض) إلى سلسلة نصية (string).
الفكرة هي تقسيم العالم كله إلى شبكة من المستطيلات. كل مستطيل في هاي الشبكة له رمز فريد. ولو قسمنا هذا المستطيل لمستطيلات أصغر، بنضيف حرف جديد للرمز. النتيجة؟
- كلما زاد طول السلسلة النصية للـ Geohash، كلما كانت المنطقة الجغرافية اللي بتمثلها أصغر وأكثر دقة.
- وهنا السحر: الأماكن المتقاربة جغرافيًا غالبًا ما يكون لها Geohash متشابه في بدايته (prefix).
مثلاً، لو كان الـ Geohash لموقعك هو sv8w1z4، فالمطعم اللي جنبك ممكن يكون الـ Geohash تبعه sv8w1z6. لاحظت التشابه؟ هذا التشابه هو مفتاح الحل كله.
هيك، الخوارزمية حولت مشكلة البحث في فضاء ثنائي الأبعاد (خطوط الطول والعرض) إلى مشكلة بحث نصي بسيطة في بُعد واحد!
كيف أنقذنا الـ Geohash؟ التطبيق العملي
بعد ما فهمت المبدأ، التطبيق كان أسهل مما توقعت. العملية تمت على خطوتين:
الخطوة الأولى: تخزين الـ Geohash
أول شيء عملته هو تعديل جدول المستخدمين في قاعدة البيانات. أضفت عمود جديد اسمه geohash من نوع string.
بعدها، كتبت سكربت بسيط بيمر على كل المستخدمين، يحسب الـ Geohash لموقع كل واحد فيهم (باستخدام مكتبة برمجية جاهزة، ما في داعي تخترع العجلة)، ويخزنه في العمود الجديد. طبعًا، أي مستخدم جديد أو أي مستخدم بيحدّث موقعه، بيتم حساب وتحديث الـ Geohash تبعه تلقائيًا.
أهم خطوة: قمت بإضافة فهرس (Index) على عمود الـ geohash. هذا الفهرس هو اللي رح يعطينا السرعة الخارقة.
الخطوة الثانية: الاستعلام الذكي (The Smart Query)
الآن، بدل الاستعلام القديم البطيء، صار الاستعلام الجديد كالتالي:
- لما المستخدم يطلب “الأشخاص القريبون”، بنجيب موقعه الحالي.
- بنحسب الـ Geohash لموقعه، ولكن بنأخذ أول 5 أو 6 حروف منه فقط. ليش؟ لأنه كلما قل طول الـ prefix، زادت المساحة الجغرافية اللي بغطيها. 6 حروف بتعطيك مساحة تقريبًا 0.6 كيلومتر مربع، وهذا مناسب كنقطة بداية.
- نقوم بتنفيذ استعلام نصي بسيط جدًا في قاعدة البيانات.
-- Let's say the user's geohash prefix (6 chars) is 'sv8w1z'
SELECT user_id, name, latitude, longitude
FROM users
WHERE geohash LIKE 'sv8w1z%';
هذا الاستعلام سريع جدًا جدًا بفضل الفهرس على عمود الـ `geohash`. هو فعليًا بيطلب من قاعدة البيانات: “أعطيني كل المستخدمين اللي الـ geohash تبعهم بيبدأ بـ ‘sv8w1z'”. قاعدة البيانات بتستخدم الفهرس وبترجعلك مجموعة صغيرة جدًا من النتائج في أجزاء من الثانية، بدل ما كانت تمسح الجدول كله.
تحدي الحواف والمناطق المجاورة
بعد أول تجربة ناجحة، لاحظت مشكلة صغيرة. ماذا لو كان المستخدم على حافة منطقة Geohash، وأقرب شخص له موجود في المنطقة المجاورة مباشرة؟ استعلام الـ `LIKE` البسيط ما رح يجيبه.
الحل، لحسن الحظ، بسيط ومدمج في كل مكتبات الـ Geohash. بالإضافة للـ Geohash تبع المستخدم، بنطلب من المكتبة تعطينا الـ Geohashes للمناطق الثمانية المجاورة (شمال، جنوب، شرق، غرب، والاتجاهات البينية).
فبيصير الاستعلام النهائي يجمع كل المستخدمين من منطقة المستخدم والمناطق الثمانية المحيطة به.
-- geohash_prefix = 'sv8w1z'
-- neighbors = ['sv8w1y', 'sv8w3b', 'sv8w38', ...etc]
-- all_prefixes = ['sv8w1z', 'sv8w1y', 'sv8w3b', 'sv8w38', ...]
SELECT user_id, name, latitude, longitude
FROM users
WHERE LEFT(geohash, 6) IN ('sv8w1z', 'sv8w1y', 'sv8w3b', ...);
نصيحة عملية: هذا الاستعلام سيجلب لك مجموعة مرشحين (candidates) أكبر بقليل من حاجتك. بعد أن تحصل على هذه القائمة الصغيرة والسريعة (ربما 30-50 مستخدم بدلاً من 100,000)، يمكنك الآن أن تقوم بتطبيق معادلة هافرساين الدقيقة عليهم لترتيبهم حسب المسافة الفعلية وعرض الأقرب فقط. لقد قمنا بتحويل مشكلة ضخمة إلى مشكلة صغيرة جدًا.
أمثلة كود بسيطة
لتقريب الصورة، هاي أمثلة بسيطة بلغة Python باستخدام مكتبة `pygeohash` لإظهار سهولة الموضوع:
import pygeohash as pgh
# إحداثيات مدينة القدس
lat, lon = 31.7683, 35.2137
# 1. تشفير الإحداثيات إلى Geohash
# precision=7 يعطي دقة حوالي 150 متر
geohash_code = pgh.encode(lat, lon, precision=7)
print(f"Geohash for Jerusalem: {geohash_code}")
# Output: Geohash for Jerusalem: sv8wr2t
# 2. إيجاد المناطق المجاورة
neighbors = pgh.neighbors(geohash_code)
print(f"Neighbors: {neighbors}")
# Output: {'n': 'sv8wr2v', 'ne': 'sv8wr2z', 'e': 'sv8wr2y', ...}
# 3. فك تشفير الـ Geohash لإرجاع الإحداثيات (تقريبية)
decoded_lat, decoded_lon = pgh.decode(geohash_code)
print(f"Decoded Lat/Lon: {decoded_lat}, {decoded_lon}")
الخلاصة: نصيحة من أبو عمر
في ذلك اليوم، تحول تطبيقنا من سلحفاة بالكاد تتحرك إلى غزال يركض بسرعة البرق. انخفض استخدام المعالج من 100% إلى أقل من 5% تحت نفس الضغط. كل هذا بفضل فهم المشكلة الحقيقية واختيار الخوارزمية المناسبة.
خوارزمية Geohash هي مثال رائع على كيف يمكن لحل بسيط وأنيق أن يتفوق على الحلول المعقدة في 90% من الحالات. هي ليست مثالية لكل السيناريوهات المكانية المعقدة، لكن بالنسبة لمشكلة “الأماكن القريبة”، فهي أداة لا تقدر بثمن.
يا جماعة، البرمجة مش بس كتابة كود، هي فن حل المشاكل. لما تواجهك مشكلة أداء، ما تستسلم للطريقة الواضحة. ابحث، اقرأ، جرب. الحل العبقري ممكن يكون أبسط مما بتتخيل، وممكن يكون موجود من سنين زي خوارزمية الـ Geohash. خليكم فضوليين، والله يوفقكم. 😉