Skip to content

Feature store

A feature store turns your certified derivations into reusable, retrievable features. It adds two things derivations alone don't have: a point-in-time-correct historical join for building training data without leakage, and a low-latency online store for serving the latest feature values at inference. Because a feature is just a certified derivation, every feature value is verified and versioned, and the same derivation feeds training and serving, so there is no train/serve skew.

The API follows Feast: entities, feature views, get_historical_features, get_online_features, and materialize.

Entities and feature views

An entity names a join key. A feature view groups feature columns produced by a certified derivation, keyed by entities and (optionally) an event-time column:

{
  "specVersion": "1.0",
  "kind": "FeatureStore",
  "entities": [
    { "name": "user", "joinKey": "user_id" }
  ],
  "featureViews": [
    {
      "name": "user_stats",
      "entities": ["user"],
      "source": "user_activity_features",
      "timestampField": "event_timestamp",
      "ttlSeconds": 604800,
      "features": [{ "name": "clicks_7d" }, { "name": "sessions_7d" }]
    }
  ]
}

source is a certified derivation whose rows carry the join key (user_id), the event time (event_timestamp), and the feature columns. If features is omitted, every source column that is not a join key or the timestamp is exposed.

Point-in-time historical retrieval

To build training data, you join features onto an entity dataframe: rows of join keys plus the timestamp each label was observed:

get_historical_features(
    store,
    entity_df=[{"user_id": "u1", "event_timestamp": "2024-03-15"}],
    features=["user_stats:clicks_7d"],
    run=run_derivation,
)

For each entity row, the value joined is the latest feature snapshot at or before that row's timestamp, within the view's TTL. A snapshot recorded after the label's timestamp is never joined, so future information stays out of training data. The prediction gate enforces the same leakage guarantee.

Training sets

A point-in-time join is how you build training data, but a join you have to re-run identically to reproduce a model is not itself an artifact. A training set freezes the join output into a named, reusable training source:

POST /api/features/training-sets
{ "name": "churn_2024", "features": ["user_stats:clicks_7d"],
  "entity_df": [{"user_id": "u1", "event_timestamp": "2024-03-15", "label": 1}],
  "label": "label" }

The result is materialized and persisted, then offered alongside bound datasets and certified derivations as a model training source (kind: "training_set"): pick it in the training form, name it in the train_model tool, or POST {"training_set": "..."} to /api/registry/train. Because the join is leakage-free, so is every model trained on the set. GET /api/features/training-sets lists them; the agent builds one with create_training_set.

Online retrieval

materialize snapshots the newest value per entity into the online store; inference reads it back in a single lookup:

materialize(store, online, run=run_derivation)
get_online_features(store, [{"user_id": "u1"}], ["user_stats:clicks_7d"], online=online)

The online value equals the historical value joined at the present moment, because both come from the same derivation.

Monitoring: statistics and drift

Certification proves a feature view's derivation is sound; it says nothing about whether this week's values have moved. Monitoring closes that gap. A statistics snapshot profiles a view's feature columns (completeness, distinct count, and range per feature) and records it, so you can watch a distribution over time:

POST /api/features/views/user_stats/statistics      # snapshot now
POST /api/features/views/user_stats/statistics {"set_baseline": true}
GET  /api/features/views/user_stats/statistics       # history, newest first

Setting a baseline also keeps a capped row sample as the reference a drift check compares against. The check reuses the same Evidently data-drift engine the model monitor uses, so a feature view and a model report drift the same way, and records which features moved and whether the view as a whole drifted:

POST /api/features/views/user_stats/drift-check      # compare current vs baseline
GET  /api/features/views/user_stats/drift            # drift history, newest first

A dataset-drift outcome fires the feature.drift_detected webhook. The maintenance scheduler re-checks every view that has a baseline once per FEATURE_DRIFT_INTERVAL_HOURS (default 24), so monitoring is opt-in per view and runs unattended once a baseline is set. Drift monitoring rides on the ML stack, which the app ships installed; in the SDK it needs the ml extra (pip install 'elbi[ml]').

Expectations: a data contract on a view

Drift asks whether values moved; a data contract asks whether they meet a declared bar at all: types, ranges, non-null, uniqueness. A view can carry one contract (the platform's own DataContract, the same quality bar derivations use), and its feature values are checked against it with the same three-valued verdict the oracle speaks:

POST /api/features/views/user_stats/contract/suggest   # profile → proposed contract
PUT  /api/features/views/user_stats/contract           # attach { "contract": {...} }
POST /api/features/views/user_stats/expectations-check  # sound / unsound / inconclusive
GET  /api/features/views/user_stats/expectations        # check history

An unsound outcome fires the feature.expectations_failed webhook. The agent verifies a view with check_feature_expectations; a human attaches the contract (the UI can suggest one from a profile and attach it in a click). This reuses the contract engine wholesale, with no separate expectations system to learn.

Serving and governance

The app exposes the store under /api/features: register entities and feature views, materialize, retrieve online and point-in-time historical features, and browse the registry for discovery. The registry is a searchable list; each view opens a detail page showing its schema (feature name, type, description), materialization freshness (how many entity keys are materialized and when), statistics, drift, expectations, a point-in-time or online lookup, and, through the lineage graph, the models that consume it via its training sets. A certification gate refuses to materialize or serve a view whose source derivation is not certified, so a served feature value is always verified.

Serving and train/serve skew

A model served here does not read the online store to enrich a request. Instead it re-applies the same certified derivation that engineered its training features to the raw request rows (see models). That is a stronger skew guarantee than an online feature store gives: training and serving share the derivation itself, not merely a store both sides hope was written consistently. The online store's job is low-latency lookup of precomputed values for callers that want it, not a hidden dependency of the model server.

Deliberately out of scope

Two capabilities other feature stores advertise are intentionally absent, because the platform meets the same need elsewhere:

  • Request-time (on-demand) transformations. Rather than compute a feature from request data inside the store, the model server re-applies the feature's certified derivation to the request rows, as described above. A feature is the derivation, so there is no separate request-time transform to define or keep in sync.
  • Streaming ingestion. materialize refreshes the online store on demand, and the maintenance scheduler re-runs monitoring unattended; there is no always-on streaming materializer.

Feature views are also not independently versioned: a view names a derivation, and the derivation is already content-addressed and versioned, so the view inherits that history rather than keeping a parallel one.

See the Feature Store Spec for the normative schema.