اختبار A/B في Rust: كيف لا تكسر الإنتاج وتجد الحقيقة

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

لماذا Rust مثالي لاختبارات A/B؟

\n

عندما نتحدث عن اختبار A/B، عادةً ما يتبادر إلى الذهن Python أو R أو JavaScript. لكن Rust هو الحصان الأسود الذي يظهر في الظروف القتالية الحقيقية أسرع وأكثر أمانًا وأرخص. إذا كان الباك إند الخاص بك مكتوبًا بلغة Rust، فيمكنك تضمين التجارب مباشرة في قلب النظام دون تحميل زائد على الخدمات المصغرة وRedis.

\n

تخيل: أنت تغير خوارزمية التوصيات، أو لون زر "شراء"، أو منطق التحليل. خطأ في كود اختبار A/B بلغة Python قد يودي بالخادم. في Rust - ببساطة لن تتمكن من التجميع إذا فاتك فرع شرط. المترجم هو أفضل ضمان للجودة لديك.

\n

في هذه المقالة، سنشرح اختبار A/B في Rust من البنية إلى الإنتاج. ستتعلم كيفية إجراء اختبارات الانقسام دون ألم، وحساب الإحصائيات، وعدم فقدان المستخدمين.

\n\n

بنية اختبار A/B في Rust: ثلاث ركائز

\n

أي تجربة في تطبيق Rust تعتمد على ثلاثة مكونات:

\n
    \n
  • الموزع العشوائي - من يدخل المجموعة A ومن يدخل المجموعة B؟
  • \n
  • مخزن التعيينات - لضمان عدم انتقال المستخدم بين المجموعات عند كل طلب.
  • \n
  • التسجيل والإحصائيات - نحسب معدل التحويل وقيمة p-value.
  • \n
\n

سنكتب وحدة بسيطة ولكنها عملية. لا أطر عمل - فقط Rust ومكتبتها القياسية.

\n\n

الخطوة 1. التوزيع العشوائي بناءً على التجزئة

\n

التوزيع العشوائي الحتمي هو مفتاح الاستقرار. إذا كنت تستخدم rand::random()، فقد ينتقل المستخدم إلى مجموعة أخرى عند كل إعادة تشغيل للخادم. نستخدم تجزئة مرجحة من معرف المستخدم واسم التجربة.

\n\n
use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\n#[derive(Debug, Clone, Copy, PartialEq)]\npub enum Group {\n    Control,\n    Treatment,\n}\n\npub fn assign_group(user_id: &str, experiment: &str, treatment_weight: f64) -> Group {\n    let mut hasher = DefaultHasher::new();\n    user_id.hash(&mut hasher);\n    experiment.hash(&mut hasher);\n    let hash = hasher.finish();\n\n    let normalized = (hash as f64) / (u64::MAX as f64);\n    if normalized < treatment_weight {\n        Group::Treatment\n    } else {\n        Group::Control\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_deterministic() {\n        let g1 = assign_group("user_42", "button_color_v2", 0.5);\n        let g2 = assign_group("user_42", "button_color_v2", 0.5);\n        assert_eq!(g1, g2);\n    }\n}\n
\n\n

يضمن هذا الكود أن نفس المستخدم سيكون دائمًا في نفس المجموعة لتجربة معينة. treatment_weight هو نسبة حركة المرور التي تذهب إلى مجموعة الاختبار (على سبيل المثال، 0.1 = 10%).

\n\n

الخطوة 2. التخزين: ذاكرة تخزين مؤقت أم قاعدة بيانات؟

\n

للمشاريع عالية التحميل، تخزين التعيينات في الذاكرة هو مخاطرة. إذا تعطل الخادم، سيفقد المستخدم مجموعته. من الأفضل استخدام Redis أو SQLite. لكن للتبسيط، سنعرض ذاكرة تخزين مؤقت داخلية مع TTL.

\n\n
use std::collections::HashMap;\nuse std::time::{Duration, Instant};\n\nstruct AssignmentCache {\n    store: HashMap<String, (Group, Instant)>,\n    ttl: Duration,\n}\n\nimpl AssignmentCache {\n    fn new(ttl_secs: u64) -> Self {\n        Self {\n            store: HashMap::new(),\n            ttl: Duration::from_secs(ttl_secs),\n        }\n    }\n\n    fn get(&mut self, key: &str) -> Option<Group> {\n        if let Some((group, time)) = self.store.get(key) {\n            if time.elapsed() < self.ttl {\n                return Some(*group);\n            }\n        }\n        None\n    }\n\n    fn set(&mut self, key: String, group: Group) {\n        self.store.insert(key, (group, Instant::now()));\n    }\n}\n
\n\n

في مشروع حقيقي، استبدل HashMap بـ dashmap أو Redis - سيوفر ذلك أمان الخيوط والثبات.

\n\n

التكامل مع

المدونات

توصيات الكتب