📌 نظرة عامّة: MLOps حديث: من النموذج إلى الإنتاج
المشكلة: training/serving skew
تخيّل فريق data يعمل على نموذج كشف احتيال. data scientists يكتبون notebook يُجَمِّع، لكلّ معاملة، عدد المدفوعات الصادرة من نفس المستخدم في آخر 7 أيّام. يستعملون GROUP BY على جدول BigQuery بنافذة منزلقة. النموذج يحصل على AUC 0.93.
عند النشر، الفريق backend يستلم التعليمة: « احسب نفس feature لحظيًّا لكلّ معاملة واردة ». المهندس backend، بـ Node.js، يكتب SQL خاصّ به بفارق نطاق زمني، ينسى استثناء المعاملات المُلغاة، ويُقَرِّب النافذة إلى 7 أيّام تقويمية بدل 7×24 ساعة منزلقة. النتيجة: feature المُقَدَّمة للنموذج في الإنتاج لم تعد بنفس التوزيع كما عند التدريب. النموذج يبدأ التصنيف الخاطئ، دون أيّ تنبيه.
هذا اللاتطابق بين features التدريب وfeatures الإنتاج له اسم: training/serving skew. أحد أكثر الأسباب ذكرًا لإخفاق النماذج الصامت في الإنتاج. الـ feature store يحلّه بكشف تعريف وحيد للـ features، قابل للاستهلاك من العالمين.
معمارية Feast
Feast هو feature store مفتوح المصدر، مكتوب في Python وGo، ترعاه LF AI & Data. خمس كائنات:
- Entity — مفتاح العمل الذي تصفه. مستخدم، بطاقة بنكية، متجر، منتج. لكلّ entity اسم (driver) ونوع (Int64، String).
- Data source — من أين تأتي القيم التاريخية. ملفّ Parquet، جدول BigQuery، Snowflake، Redshift، أو موضوع Kafka.
- Feature view — تجميع features تتقاسم نفس entity ونفس source. الوحدة التي يُسَجِّلها Feast ويُجَسِّدها.
- Offline store — محرّك يحلّ الاستعلامات التاريخية point-in-time correct للتدريب. File (Parquet)، BigQuery، Snowflake، Redshift، Spark، DuckDB.
- Online store — قاعدة مفتاح/قيمة بـ زمن استجابة منخفض للاستنتاج. SQLite (dev)، Redis، DynamoDB، Bigtable، Postgres.
الـ registry ملفّ بيانات وصفية (افتراضيًّا registry.db SQLite، أو Postgres أو S3) يحفظ كلّ تعريفاتك. feast apply يقرأ كود Python، يُسَلسِل التعريفات ويدفعها للـ registry.
المتطلّبات
Python ≥ 3.10 (Feast 0.63 يدعم 3.10 إلى 3.13) وpip. لا قاعدة بيانات خارجية مطلوبة: نبدأ بـ Parquet للـ offline وSQLite للـ online. لـ Redis، يلزم Redis محلّي (أو Docker). للنموذج، scikit-learn ≥ 1.4.
الخطوة 1 — تثبيت Feast وتهيئة المشروع
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install "feast[redis]" pandas scikit-learn
feast version
يُرجع Feast SDK Version: "feast 0.63.x".
feast init driver_demo
اقبل الافتراضي local. مجلّد driver_demo/ يحوي الآن مشروعًا كاملًا.
الخطوة 2 — استكشاف البنية المُوَلَّدة
cd driver_demo/feature_repo
ls -la
ثلاثة ملفّات مفتاحية. الأوّل feature_store.yaml:
project: driver_demo
provider: local
registry: data/registry.db
online_store:
type: sqlite
path: data/online_store.db
offline_store:
type: file
entity_key_serialization_version: 3
المشروع يُسَمَّى driver_demo، registry SQLite، online store SQLite، offline store ملفّ (Parquet). provider: local يُشير إلى أنّ كلّ شيء يدور على حاسوبك.
الثاني example_repo.py حيث تُعلَن entities وfeature views. الثالث مجلّد data/ يحوي driver_stats.parquet بآلاف الأسطر من إحصائيات سائقين اصطناعية.
import pandas as pd
df = pd.read_parquet("data/driver_stats.parquet")
print(df.head())
print(df.dtypes)
خمسة أعمدة: event_timestamp، driver_id، conv_rate، acc_rate، avg_daily_trips. timestamp أساسي: Feast يستعمله لحلّ الاستعلامات point-in-time correct.
الخطوة 3 — تعريف Entity وFeatureView بـ Python
from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64
# 1. Entity العمل: سائق، مُعَرَّف بـ Int64
driver = Entity(
name="driver",
join_keys=["driver_id"],
description="Identifiant unique d'un chauffeur",
)
# 2. مصدر البيانات التاريخية (offline)
driver_stats_source = FileSource(
name="driver_stats_source",
path="data/driver_stats.parquet",
timestamp_field="event_timestamp",
created_timestamp_column="created",
)
# 3. feature view: 3 features مُلحَقة بسائق
driver_stats_fv = FeatureView(
name="driver_hourly_stats",
entities=[driver],
ttl=timedelta(days=1),
schema=[
Field(name="conv_rate", dtype=Float32),
Field(name="acc_rate", dtype=Float32),
Field(name="avg_daily_trips", dtype=Int64),
],
online=True,
source=driver_stats_source,
tags={"team": "data_science"},
)
الـ Entity لها اسم منطقي driver وjoin key فيزيائي driver_id. الـ FileSource يُشير إلى Parquet ويُعَيِّن event_timestamp كعمود مرجع للوقت. الـ FeatureView يجمع 3 features ويُلحق ttl بيوم — حماية ضدّ استعمال بيانات قديمة. online=True يسمح بـ matérialisation نحو الـ online store.
الخطوة 4 — تشغيل feast apply لتسجيل
feast apply
Feast يعرض الكائنات المُنشَأة: Created entity driver، Created feature view driver_hourly_stats. الـ registry يعيش الآن في data/registry.db.
feast feature-views list
feast entities list
كلّ أمر يُرجع جدولًا ASCII بالكائنات المعروفة من registry.
الخطوة 5 — خدمة الـ offline store: get_historical_features
السيناريو: لديك جدول أحداث، كلّ سطر يحمل driver_id وevent_timestamp. تريد، لكلّ سطر، features هذا السائق كما كانت في تلك اللحظة. لا أبكر، لا أبعد. هذا ما يمنع data leakage في التدريب.
from datetime import datetime
import pandas as pd
from feast import FeatureStore
store = FeatureStore(repo_path=".")
entity_df = pd.DataFrame.from_dict({
"driver_id": [1001, 1002, 1003],
"event_timestamp": [
datetime(2021, 4, 12, 10, 59, 42),
datetime(2021, 4, 12, 8, 12, 10),
datetime(2021, 4, 12, 16, 40, 26),
],
})
training_df = store.get_historical_features(
entity_df=entity_df,
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
"driver_hourly_stats:avg_daily_trips",
],
).to_df()
print(training_df.head())
python fetch_historical.py
DataFrame Pandas بـ 5 أعمدة (الاثنان للدخل + الـ 3 features). تحت الغطاء، Feast حمّل Parquet، عمل as-of join بـ driver_id وevent_timestamp، باحترام ttl.
الخطوة 6 — التجسيد إلى الـ online store
للاستنتاج في الإنتاج، لا نريد as-of join على Parquet لكلّ طلب: بطيء جدًّا. نريد قاعدة مفتاح/قيمة تُرجع في بضعة ميلي ثوانٍ آخر قيمة معروفة.
CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S")
feast materialize-incremental $CURRENT_TIME
Feast يعرض اسم feature view، الفترة المُعالَجة، وعدد الأسطر المكتوبة في الـ online store. أوّل تشغيل ينطلق من أقدم تاريخ في Parquet؛ التاليات من آخر cursor مُسَجَّل في registry.
الخطوة 7 — خدمة الـ online store: get_online_features
from feast import FeatureStore
store = FeatureStore(repo_path=".")
features = store.get_online_features(
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
"driver_hourly_stats:avg_daily_trips",
],
entity_rows=[
{"driver_id": 1001},
{"driver_id": 1002},
],
).to_dict()
print(features)
زمن استجابة نمطي على SQLite محلّي: مللي ثانية. على Redis أو DynamoDB، أقلّ من مللي ثانية في الخادم. get_online_features لا يأخذ timestamp: يُرجع آخر قيمة مُجَسَّدة. إن تجاوزت آخر matérialisation الـ ttl، Feast يُرجع None.
الخطوة 8 — تهيئة online store Redis للإنتاج
docker run -d --name feast-redis -p 6379:6379 redis:7-alpine
project: driver_demo
provider: local
registry: data/registry.db
online_store:
type: redis
connection_string: "localhost:6379"
offline_store:
type: file
entity_key_serialization_version: 3
feast apply
feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S")
أعد تشغيل script الخطوة 7. الخرج مطابق لكنّ القراءات تمرّ الآن عبر Redis. تحقّق بـ redis-cli MONITOR: ترى أوامر HMGET.
الخطوة 9 — تشغيل خادم features REST/gRPC
feast serve --host 0.0.0.0 --port 6566
curl -X POST http://localhost:6566/get-online-features \
-H "Content-Type: application/json" \
-d '{
"features": [
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:avg_daily_trips"
],
"entities": {
"driver_id": [1001, 1002]
}
}'
JSON مع القيم لكلّ entity. للأداء، انتقل إلى gRPC بـ --type grpc. في الإنتاج، الخادم يدور في pod Kubernetes مخصَّص خلف Service مع autoscaling أفقي. يقرأ registry وonline store لكن لا يكتب أبدًا.
الخطوة 10 — ربط نموذج scikit-learn
from datetime import datetime
import pandas as pd
from sklearn.linear_model import LinearRegression
from feast import FeatureStore
store = FeatureStore(repo_path=".")
# 1. مرحلة التدريب: نسترد التاريخ
entity_df = pd.DataFrame({
"driver_id": [1001, 1002, 1003, 1004, 1005] * 100,
"event_timestamp": [datetime(2021, 4, 12, 10, 0, 0)] * 500,
"trip_completed": [1, 0, 1, 0, 1] * 100,
})
training_df = store.get_historical_features(
entity_df=entity_df,
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
"driver_hourly_stats:avg_daily_trips",
],
).to_df().dropna()
X = training_df[["conv_rate", "acc_rate", "avg_daily_trips"]]
y = training_df["trip_completed"]
model = LinearRegression().fit(X, y)
# 2. مرحلة الاستنتاج: نسترد features online
online = store.get_online_features(
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
"driver_hourly_stats:avg_daily_trips",
],
entity_rows=[{"driver_id": 1001}],
).to_dict()
features_for_pred = pd.DataFrame({
"conv_rate": online["conv_rate"],
"acc_rate": online["acc_rate"],
"avg_daily_trips": online["avg_daily_trips"],
})
prediction = model.predict(features_for_pred)
print(f"Prédiction pour driver 1001 : {prediction[0]:.3f}")
نفس تعريف features في التدريب والاستنتاج. منطق حساب features يعيش مرّة واحدة، في feature views.
أخطاء شائعة
| العَرَض | السبب | التصحيح |
|---|---|---|
get_online_features يُرجع None في كلّ مكان |
لا matérialisation، أو TTL متجاوز | أعد feast materialize-incremental بـ timestamp محدَّث |
| RegistryNotBuiltException عند الإقلاع | feast apply لم يُشَغَّل في هذا المجلّد |
كن في المجلّد الذي يحوي feature_store.yaml |
| خلايا NaN في DataFrame التدريب | TTL أقصر من الفارق بين event_timestamp وآخر feature متاحة | زد ttl للـ FeatureView |
| ConnectionError على Redis | container Redis غير مُشَغَّل | docker ps، docker start feast-redis |
| FeatureViewNotFoundException في الخادم | الخادم أُطلق قبل feast apply جديد |
أعد تشغيل feast serve |
| خلاف بين online وoffline على نفس المفتاح | matérialisation جزئية | أعد feast materialize كاملة |
مصادر رسمية
- توثيق Feast:
https://docs.feast.dev - Quickstart الرسمي
- مرجع Feature View
- مستودع GitHub:
https://github.com/feast-dev/feast
الأدلّة المرتبطة
- MLOps حديث
- MLflow Model Registry وCI/CD
- تقديم نموذج بـ BentoML
- Kubeflow Pipelines
- كشف drift بـ Evidently
FAQ
هل يلزم feature store عند بدء مشروع ML؟ لا. لنموذج batch منعزل في notebook، إفراط في الهندسة. يصير ضروريًّا فور (أ) نموذج يُقَدَّم لحظيًّا بـ features مشتقّة، (ب) عدّة نماذج تتشارك features، أو (ج) عدّة فِرَق على نفس الإشارات.
الفرق مع Redis مخصَّص؟ Redis مخصَّص يُقَدِّم features online، لكن لا يحلّ المشكلة الرئيسية: تعريف features في مكان آخر. Feast يُوَحِّد التعريف، التاريخ point-in-time correct، matérialisation، والخدمة خلف API وحيد.
هل Feast يحلّ محلّ data warehouse؟ لا. Feast يستند إلى warehouse (BigQuery، Snowflake، Redshift) كـ offline store. لا يُخَزِّن شيئًا جديدًا.
كيف نُؤَتمت matérialisation؟ بجدولتها كـ job batch: CronJob Kubernetes، DAG Airflow، أو Dataflow scheduler GCP يُشَغِّل feast materialize-incremental دوريًّا (5 دقائق إلى ساعة).
كيف نرفع الانتعاش تحت الدقيقة؟ بـ push sources: تكتب بنفسك في online store عبر store.push("source_name", df) فور وصول حدث. مثالي لـ features محسوبة في streaming.
وإصدارات features؟ registry Feast قابل للتسلسل؛ تضعه في Git بتوجيه registry: إلى ملفّ مُؤَرَّخ، أو تستعمل registry SQL (Postgres) مع snapshots.
مقالات ذات صلة
- Coolify الاستضافة الذاتية 2026: الدليل الكامل (PaaS مفتوح المصدر)
- Authentik 2026: الدليل الكامل (Identity Provider مفتوح المصدر للشركات الإفريقية)
- Directus 2026: الدليل الكامل (headless CMS مفتوح المصدر للشركات الإفريقية)
- Wazuh 2026: الدليل الكامل (SIEM مفتوح المصدر للشركات الفرنكوفونية، بديل Splunk)