لماذا 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
سنكتب وحدة بسيطة ولكنها عملية. لا أطر عمل - فقط Rust ومكتبتها القياسية.
\n\nالخطوة 1. التوزيع العشوائي بناءً على التجزئة
\nالتوزيع العشوائي الحتمي هو مفتاح الاستقرار. إذا كنت تستخدم rand::random()، فقد ينتقل المستخدم إلى مجموعة أخرى عند كل إعادة تشغيل للخادم. نستخدم تجزئة مرجحة من معرف المستخدم واسم التجربة.
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%).
الخطوة 2. التخزين: ذاكرة تخزين مؤقت أم قاعدة بيانات؟
\nللمشاريع عالية التحميل، تخزين التعيينات في الذاكرة هو مخاطرة. إذا تعطل الخادم، سيفقد المستخدم مجموعته. من الأفضل استخدام Redis أو SQLite. لكن للتبسيط، سنعرض ذاكرة تخزين مؤقت داخلية مع TTL.
\n\nuse 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 - سيوفر ذلك أمان الخيوط والثبات.