A production-grade lightweight feature store for applied ML engineering. Handles offline storage with point-in-time correct joins (PostgreSQL), online serving with sub-10ms latency (Redis), feature versioning, drift detection (Evidently AI), a CLI, Python SDK, and Streamlit registry browser — all in one repo.
Machine-learning models run on "features" — the prepared input signals they make decisions from (a customer's average spend, days since last login, a risk score). The catch: each feature has to be computed identically in two very different settings — once in bulk over historical data when the model is trained, and again instantly, one record at a time, when the model is live and serving real users. When those two drift apart even slightly, the model quietly performs worse in production than it did in testing. On top of that, teams keep rebuilding the same features for every new project, and small mistakes in how training data is assembled can let a model accidentally "see the future" and look better than it really is.
This is a single place to define a feature once and reuse it everywhere, consistently and safely. You write a feature's logic one time and the store handles both worlds:
- an offline store (PostgreSQL) for building training datasets, with point-in-time-correct joins so there's no data leakage,
- an online store (Redis) that serves the freshest feature values in under 10 milliseconds when a live model asks,
- versioning so you can document, inspect, and roll any feature back to a previous version,
- drift detection on every ingestion run, so you're warned when incoming data starts to look different,
- and a browsable UI plus a Python SDK (
store.get_features(entity_id, [...])) so anyone can find and use features without reinventing them.
| Layer | Tool |
|---|---|
| Offline store | PostgreSQL + SQLAlchemy (async) |
| Online store | Redis — target <10ms p99 |
| Ingestion | Polars |
| Serving API | FastAPI + Uvicorn |
| CLI | Click |
| Scheduler | APScheduler |
| Drift detection | Evidently AI |
| Monitoring | Prometheus + Grafana |
| UI | Streamlit |
| SDK | Python (sync + async) |
Data Sources (CSV, PostgreSQL, Kafka)
│
▼
Ingestion Pipeline (Polars)
├── Schema validation
├── Feature transforms (@feature decorator)
└── Drift detection (Evidently AI)
│
┌────┴────┐
▼ ▼
Offline Store Online Store
(PostgreSQL) (Redis, <10ms)
│ │
└──────┬─────────┘
▼
Feature Registry (metadata, versions, lineage)
│
▼
Serving API (FastAPI) ← Python SDK
│
▼
Streamlit UI (browse, search, version history, drift)
| Service | URL | Description |
|---|---|---|
| Feature Store API | http://localhost:8000 | Feature retrieval, ingestion, registry |
| API Docs (Swagger) | http://localhost:8000/docs | Interactive endpoint documentation |
| Prometheus metrics | http://localhost:8000/metrics | Latency, throughput, error counters |
| Streamlit UI | http://localhost:8501 | Browse features, stats, version diffs, drift |
Prerequisites: Docker Desktop installed and running, plus the shared infrastructure stack.
# 1. Start shared infrastructure (PostgreSQL + Redis + MinIO + Kafka + Prometheus + Grafana)
git clone https://github.com/Emart29/ml-platform-infra
cd ml-platform-infra && docker compose up -d && cd ..
# 2. Clone and start the feature store
git clone https://github.com/Emart29/ml-feature-store
cd ml-feature-store
docker compose upDocker will automatically:
- Create the database schema in the shared PostgreSQL instance
- Download and ingest the Heart Disease demo dataset (303 patients, 6 features)
- Sync features from PostgreSQL to Redis
- Start the Feature Store API on port 8000
- Start the Streamlit registry browser on port 8501
Open http://localhost:8501 — the feature registry is live.
# Stop containers (keep data)
docker compose down
# Stop and delete all data
docker compose down -vRequires Python 3.10+.
# 1. Start PostgreSQL + Redis (uses the shared infra stack)
git clone https://github.com/Emart29/ml-platform-infra
cd ml-platform-infra && docker compose up -d && cd ..
# 2. Install
git clone https://github.com/Emart29/ml-feature-store
cd ml-feature-store
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux / macOS
pip install -r requirements.txt
pip install -e .
# 3. Configure (optional — defaults connect to localhost)
copy .env.example .env
# 4. Create tables + ingest demo data + start
python examples/heart_disease/ingest.py
featurestore sync --full
featurestore serve # API on :8000 (keep this running)
featurestore ui # Streamlit on :8501 (new terminal)Demonstrates the full feature store lifecycle end-to-end: ingestion → versioning → training → serving → drift detection.
# Full scripted demo (requires API running on :8000)
python examples/heart_disease/demo.py
# Step by step
python examples/heart_disease/ingest.py # register + ingest 6 features
featurestore sync --full # push feature values to Redis
python examples/heart_disease/train.py # train GBM via SDK point-in-time join
uvicorn examples.heart_disease.serve:app --port 8001 # prediction API
curl -X POST http://localhost:8001/predict/1 # predict for patient 1Or open the Jupyter notebook for an interactive walkthrough:
jupyter lab examples/heart_disease/notebook.ipynbThe notebook covers: registry exploration → point-in-time correct training data → model training → online serving latency benchmark (p50/p95/p99) → batch inference → feature statistics and drift history.
# List registered features
featurestore list
featurestore list --search risk --entity-type patient --status active
# Inspect a feature (metadata, statistics, run history)
featurestore describe composite_risk_score
# Compare two versions (code diff + statistics)
featurestore diff composite_risk_score 1 2
# View statistics history
featurestore stats composite_risk_score
featurestore stats composite_risk_score --plot --runs 10
# Push features from a CSV (with custom transform file)
featurestore push age_normalized \
--source-type csv \
--source-path examples/heart_disease/data/heart.csv \
--entity-col patient_id \
--features-file examples/heart_disease/features.py
# Dry-run to validate without writing
featurestore push age_normalized --source-type csv --source-path data.csv --dry-run
# Sync offline store → Redis
featurestore sync # incremental (since last watermark)
featurestore sync --full # full resync
featurestore sync --feature age_normalized # one feature only
# Register a new feature interactively
featurestore register \
--name conversion_rate \
--entity-type user \
--dtype float \
--description "7-day rolling conversion rate" \
--owner ml-team \
--tags conversion,rolling
# Start servers
featurestore serve # API on :8000
featurestore serve --reload # with hot-reload (dev)
featurestore ui # Streamlit on :8501from sdk.client import FeatureStoreClient
store = FeatureStoreClient(url="http://localhost:8000")
# Single entity — online store (Redis, <10ms)
features = store.get_features(
entity_id="42",
features=["age_normalized", "cholesterol_risk_score", "composite_risk_score"],
entity_type="patient",
)
# {"age_normalized": 0.63, "cholesterol_risk_score": 1.21, ...}
# Point-in-time correct training data — no leakage
import pandas as pd
labels_df = pd.DataFrame({
"patient_id": ["1", "2", "3"],
"diagnosis_date": ["2024-01-31T00:00:00Z"] * 3,
"target": [1, 0, 1],
})
train_df = store.get_historical_features(
entity_df=labels_df,
features=["age_normalized", "cholesterol_risk_score", "composite_risk_score"],
entity_column="patient_id",
timestamp_column="diagnosis_date",
entity_type="patient",
)
# pandas DataFrame: original columns + feature columns, aligned by position
# Batch online serving — one Redis round-trip for all entities
results = store.get_features_batch(
requests=[
{"entity_id": str(i), "entity_type": "patient", "features": ["age_normalized"]}
for i in range(1, 11)
]
)
# [{"entity_id": "1", "features": {"age_normalized": 0.63}, "missing_features": []}, ...]
# Trigger ingestion from code
result = store.push_features(
"age_normalized",
source_config={
"type": "csv",
"path": "examples/heart_disease/data/heart.csv",
"entity_column": "patient_id",
"timestamp_column": "diagnosis_date",
},
wait=True,
)
print(result.records_processed, result.drift_detected)Async client — for FastAPI handlers and other async contexts:
from sdk.client import AsyncFeatureStoreClient
async with AsyncFeatureStoreClient(url="http://localhost:8000") as store:
features = await store.get_features("42", ["age_normalized"], entity_type="patient")
batch = await store.get_features_batch(requests=[...])Features are plain Python functions decorated with @feature. The function receives a Polars DataFrame (the raw source data) and returns a DataFrame with three columns: entity_id, value, computed_at.
import polars as pl
from ingestion.transforms import feature
@feature(
name="age_normalized",
entity_type="patient",
data_type="float",
description="Patient age normalized to [0, 1] using min-max scaling",
owner="ml-team",
tags=["demographic", "normalized"],
)
def age_normalized(df: pl.DataFrame) -> pl.DataFrame:
age = pl.col("age").cast(pl.Float64)
age_norm = (age - age.min()) / (age.max() - age.min())
return df.select([
pl.col("patient_id").alias("entity_id"),
age_norm.alias("value"),
pl.lit(None).cast(pl.Datetime("us", "UTC")).alias("computed_at"),
])Every time the transform function body changes, a new version is automatically created (SHA-256 hash of source code). Use featurestore diff to compare code and statistics between versions.
Six features are defined for the Heart Disease demo in examples/heart_disease/features.py:
age_normalized, cholesterol_risk_score, heart_rate_reserve, chest_pain_severity, composite_risk_score, thalassemia_risk.
Settings are read from environment variables or a .env file (copy .env.example to get started):
| Variable | Default | Description |
|---|---|---|
POSTGRES_URL |
postgresql+asyncpg://postgres:postgres@localhost:5432/ml_platform |
PostgreSQL connection string |
REDIS_URL |
redis://localhost:6379 |
Redis connection string |
SYNC_INTERVAL_MINUTES |
15 |
Scheduled offline → online sync interval |
FEATURE_VALUE_TTL_DAYS |
7 |
Redis TTL for online feature values |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING) |
ml-feature-store/
├── store/
│ ├── offline.py # PostgreSQL tables + point-in-time joins
│ ├── online.py # Redis serving + TTL management
│ ├── registry.py # Feature metadata, versions, search, lineage
│ └── sync.py # Scheduled offline → online sync (APScheduler)
├── ingestion/
│ ├── pipeline.py # Orchestrates load → transform → validate → store
│ ├── transforms.py # @feature decorator + FeatureTransformRegistry
│ └── validation.py # Schema checks + Evidently AI drift detection
├── serving/
│ └── api.py # FastAPI: online, offline, registry, ingestion endpoints
├── cli/
│ └── main.py # featurestore CLI (Click)
├── sdk/
│ ├── client.py # FeatureStoreClient + AsyncFeatureStoreClient
│ └── exceptions.py # Typed exceptions
├── ui/
│ ├── app.py # Streamlit registry browser (5 pages)
│ └── api_client.py # HTTP client for the UI
├── db/
│ ├── models.py # SQLAlchemy ORM models
│ └── base.py # Async engine + session factory
├── examples/
│ └── heart_disease/
│ ├── features.py # 6 @feature definitions for cardiac data
│ ├── ingest.py # Download UCI dataset, register + ingest
│ ├── train.py # Train GBM on feature-store training data
│ ├── serve.py # Real-time prediction API (FastAPI, port 8001)
│ ├── demo.py # Scripted end-to-end demo
│ └── notebook.ipynb # Jupyter walkthrough (7 steps)
├── scripts/
│ └── docker_init.py # One-shot Docker setup (tables + ingest + sync)
├── tests/
│ └── test_api.py # Integration test suite (requires live API)
├── docker-compose.yml
├── Dockerfile
├── pyproject.toml
└── .env.example
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check with Redis + PostgreSQL latency |
GET |
/features/online |
Get features for one entity from Redis |
POST |
/features/online/batch |
Get features for multiple entities (pipeline) |
POST |
/features/offline/historical |
Point-in-time correct feature join |
GET |
/registry/features |
List all features (search, filter, paginate) |
POST |
/registry/features |
Register a new feature |
GET |
/registry/features/{name} |
Feature detail with stats + version history |
GET |
/registry/features/{name}/versions |
All versions for a feature |
GET |
/registry/features/{name}/statistics |
Latest distribution statistics |
GET |
/registry/features/{name}/drift |
Drift detection run history |
POST |
/ingestion/run |
Trigger an ingestion run |
GET |
/ingestion/runs |
List ingestion run history |
POST |
/ingestion/sync |
Trigger offline → online sync |
GET |
/metrics |
Prometheus metrics |
Full interactive docs: http://localhost:8000/docs