الجواب باختصار.
قاعدة البيانات التي يعمل عليها LebaneseLegislation.com للقراءة فقط. تُبنى من جديد من ملفات البيانات كل ليلة وتُحفظ داخل صورة حاوية، ولا يكتب فيها شيء أثناء الخدمة. لذلك يمكن حساب أي نتيجة لا تتغيّر من مستخدم إلى آخر مرة واحدة، عند بناء الصورة، ثم قراءتها في جزء من الميلي ثانية. في 9 تشرين الثاني 2026 نقلنا أربع نتائج من هذا النوع من لحظة الطلب إلى لحظة البناء. بقي الخادم نفسه، بمعالجه الواحد، وبقيت الأجوبة نفسها.
لماذا كان بطيئاً.
يتشارك خادم MCP وPostgreSQL حاوية واحدة بمعالج واحد. في 8 تشرين الثاني أرسل مساعد ذكاء اصطناعي 10 طلبات get_article خلال نحو 60 ميلي ثانية. استغرقت ستة منها بين 3.2 و3.4 ثوانٍ لكل طلب ورُفضت أربعة بالرمز 503. كان الطلب الواحد وحده يستغرق نحو 230 ميلي ثانية، يذهب معظمها إلى سؤال واحد: أي أحكام إلغاء عامة قد تمسّ هذه المادة؟ كان الجواب يأتي من view، أي استعلام محفوظ يربط شبكة الروابط بين النصوص بنفسها عند كل طلب. على نسخة من البيانات استغرق 214 من أصل 232 ميلي ثانية.
ما يصنعه البناء الآن.
ملف SQL واحد، db/prod.sql، يعمل مرة واحدة عند بناء الصورة. يكتب صفوف ذلك الاستعلام في جدول مفهرس. ويكتب بطاقة لكل قانون، هي الخلاصة التي يحملها كل ردّ (49 ميغابايت)، وبطاقة لكل مادة مع صيغتها النافذة محسومة سلفاً (340 ميغابايت). ويكتب لكل قانون قائمتي النصوص التي عدّلته والتي عدّلها، مجمّعة ومرتّبة: 37,672 صفاً تُبنى في 2.5 ثانية. ويكتب أعداداً تراكمية للنصوص بحسب النوع ويوم النشر: 23,975 صفاً تُبنى في 0.1 ثانية. كبرت الصورة المضغوطة من 2.149 إلى 2.434 غيغابايت.
العدّ يصير عملية طرح.
تعيد list_legislation عدد النصوص المطابقة لنوع وفترة. كان عدّها يعني قراءة النصوص الـ103,112 كلها، بين 26 و40 ميلي ثانية في كل مرة. صار البناء يخزّن، لكل نوع ولكل يوم، عدد النصوص المنشورة حتى ذلك اليوم. المجموع هو رقم مخزّن ناقص رقم آخر: قراءتان من الفهرس، بين 0.1 و0.2 ميلي ثانية. على الخادم الحي انخفضت ستة طلبات متزامنة من هذا النوع من 243 إلى 290 ميلي ثانية لكل طلب إلى ما بين 9 و16.
ليست ذاكرة مؤقتة.
لا يُحمَّل شيء في الذاكرة عند تشغيل الخادم، ولا تسخين مسبقاً، ولا شيء تنتهي صلاحيته. الجداول موجودة في قاعدة البيانات كأي جدول آخر، ويقرأها PostgreSQL من القرص عند الحاجة. لا تحمل بيانات جديدة: يستطيع db/prod.sql إعادة بنائها من الجداول المنشورة في أي وقت، وصورة البيانات العامة لا تتضمنها. يحمل كل جدول علامة إصدار. يفحص الخادم العلامات مرة واحدة، وإذا كان جدول ما ناقصاً أو أقدم حسب الجواب بالطريقة السابقة. لذلك ما زالت الصور المستقلة تعمل على قاعدة البيانات العادية، ولا يمكن لصورة قديمة أن تعيد خطأ.
إصلاح واحد لم يكن في قاعدة البيانات.
في get_law كان 60% من وقت معالج الخادم نفسه يذهب إلى كتابة JSON. كان كل مستوى من التداخل يعيد قراءة بايتات المستوى الذي تحته، فيُمسح ردّ حجمه 284 كيلوبايت مرة لكل مستوى. صار الردّ يُكتب في مرور واحد، ويقارنه اختبار بالمرمِّز السابق على 20,000 قيمة عشوائية. ومع القوائم الجاهزة للنصوص المعدِّلة انخفض المئين 95 لستة طلبات get_law متزامنة من ما بين 177 و185 ميلي ثانية إلى ما بين 69 و80.
الأجوبة نفسها، بايتاً ببايت.
كان على كل تغيير أن يجتاز اختباراً واحداً قبل دمجه: 622 طلباً تُعاد على الخادم القديم والخادم الجديد ويجب أن تعطي ردوداً متطابقة بايتاً ببايت. تضم المجموعة 322 طلباً على خمس أدوات، و100 طلب get_law إضافي، و200 سؤال بحث حقيقي. كانت بصمة SHA-256 لكل الردود هي نفسها قبل كل تغيير وبعده. وقيست السرعة ضمن حدود الخادم الحي، معالج واحد و6 غيغابايت من الذاكرة، بطلبات منفردة ثم ستة معاً ثم عشرة معاً.
ما حصل عليه المستخدمون.
في الأيام السبعة السابقة للتغيير كان وسيط طلبات get_article الحقيقية 128 ميلي ثانية، والمئين 95 (الزمن الذي ينتهي ضمنه 95 من كل 100 طلب) 1,060 ميلي ثانية، وأسوأ حالة 3,242. وفي الطلبات الحقيقية الـ25 خلال أول 12 ساعة بعده: 13 و131 و199 ميلي ثانية. شملت دفعة من 10 طلبات في دقيقة واحدة، وهو النمط الذي فشل في اليوم السابق. استغرق أبطأ طلب فيها 199 ميلي ثانية ولم يُرفض أي طلب.
ما زال بطيئاً.
الدقيقة الأولى بعد كل نشر. الحاوية الجديدة لم تقرأ شيئاً من القرص بعد: ستة طلبات بحث أُرسلت بعد ثانية واحدة من التبديل استغرقت بين 104 و298 ميلي ثانية، مقابل 17 إلى 90 بعد أن تسخن. والردود الكبيرة تحتاج وقتاً لتنتقل. يعيد get_law لقانون كبير حتى 284 كيلوبايت، ومن جهاز الاختبار لدينا يستغرق الطلب كله بين 0.4 و0.9 ثانية مهما فعل الخادم.