|
| 1 | +# \[Alpha\] Feature View Versioning |
| 2 | + |
| 3 | +{% hint style="warning" %} |
| 4 | +**Warning**: This is an _experimental_ feature. It is stable but there are still rough edges. Contributions are welcome! |
| 5 | +{% endhint %} |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +Feature view versioning automatically tracks schema and UDF changes to feature views. Every time `feast apply` detects a change, a versioned snapshot is saved to the registry. This enables: |
| 10 | + |
| 11 | +- **Audit trail** — see what a feature view looked like at any point in time |
| 12 | +- **Safe rollback** — pin serving to a prior version with `version="v0"` in your definition |
| 13 | +- **Multi-version serving** — serve both old and new schemas simultaneously using `@v<N>` syntax |
| 14 | +- **Staged publishing** — use `feast apply --no-promote` to publish a new version without making it the default |
| 15 | + |
| 16 | +## How It Works |
| 17 | + |
| 18 | +Version tracking is fully automatic. You don't need to set any version parameter — just use `feast apply` as usual: |
| 19 | + |
| 20 | +1. **First apply** — Your feature view definition is saved as **v0**. |
| 21 | +2. **Change something and re-apply** — Feast detects the change, saves the old definition as a snapshot, and saves the new one as **v1**. The version number auto-increments on each real change. |
| 22 | +3. **Re-apply without changes** — Nothing happens. Feast compares the new definition against the active one and skips creating a version if they're identical (idempotent). |
| 23 | +4. **Another change** — Creates **v2**, and so on. |
| 24 | + |
| 25 | +``` |
| 26 | +feast apply # First apply → v0 |
| 27 | +# ... edit schema ... |
| 28 | +feast apply # Detects change → v1 |
| 29 | +feast apply # No change detected → still v1 (no new version) |
| 30 | +# ... edit source ... |
| 31 | +feast apply # Detects change → v2 |
| 32 | +``` |
| 33 | + |
| 34 | +**Key details:** |
| 35 | + |
| 36 | +* **Automatic snapshots**: Versions are created only when Feast detects an actual change to the feature view definition (schema or UDF). Metadata-only changes (description, tags, TTL) update in place without creating a new version. |
| 37 | +* **Separate history storage**: Version history is stored separately from the active feature view definition, keeping the main registry lightweight. |
| 38 | +* **Backward compatible**: The `version` parameter is fully optional. Omitting it (or setting `version="latest"`) preserves existing behavior — you get automatic versioning with zero changes to your code. |
| 39 | + |
| 40 | +## Configuration |
| 41 | + |
| 42 | +{% hint style="info" %} |
| 43 | +Version history tracking is **always active** — no configuration needed. Every `feast apply` that changes a feature view automatically records a version snapshot. |
| 44 | + |
| 45 | +To enable **versioned online reads** (e.g., `fv@v2:feature`), add `enable_online_feature_view_versioning: true` to your registry config in `feature_store.yaml`: |
| 46 | + |
| 47 | +```yaml |
| 48 | +registry: |
| 49 | + path: data/registry.db |
| 50 | + enable_online_feature_view_versioning: true |
| 51 | +``` |
| 52 | +
|
| 53 | +When this flag is off, version-qualified refs (e.g., `fv@v2:feature`) in online reads will raise errors, but version history, version listing, version pinning, and version lookups all work normally. |
| 54 | +{% endhint %} |
| 55 | + |
| 56 | +## Pinning to a Specific Version |
| 57 | + |
| 58 | +You can pin a feature view to a specific historical version by setting the `version` parameter. When pinned, `feast apply` replaces the active feature view with the snapshot from that version. This is useful for reverting to a known-good definition. |
| 59 | + |
| 60 | +```python |
| 61 | +from feast import FeatureView |
| 62 | +
|
| 63 | +# Default behavior: always use the latest version (auto-increments on changes) |
| 64 | +driver_stats = FeatureView( |
| 65 | + name="driver_stats", |
| 66 | + entities=[driver], |
| 67 | + schema=[...], |
| 68 | + source=my_source, |
| 69 | +) |
| 70 | +
|
| 71 | +# Pin to a specific version (reverts the active definition to v2's snapshot) |
| 72 | +driver_stats = FeatureView( |
| 73 | + name="driver_stats", |
| 74 | + entities=[driver], |
| 75 | + schema=[...], |
| 76 | + source=my_source, |
| 77 | + version="v2", # also accepts "version2" |
| 78 | +) |
| 79 | +``` |
| 80 | + |
| 81 | +When pinning, the feature view definition (schema, source, transformations, etc.) must match the currently active definition. If you've also modified the definition alongside the pin, `feast apply` will raise a `FeatureViewPinConflict` error. To apply changes, use `version="latest"`. To revert, only change the `version` parameter. |
| 82 | + |
| 83 | +The snapshot's content replaces the active feature view. Version history is not modified by a pin; the existing v0, v1, v2, etc. snapshots remain intact. |
| 84 | + |
| 85 | +After reverting with a pin, you can go back to normal auto-incrementing behavior by removing the `version` parameter (or setting it to `"latest"`) and running `feast apply` again. If the restored definition differs from the pinned snapshot, a new version will be created. |
| 86 | + |
| 87 | +### Version string formats |
| 88 | + |
| 89 | +| Format | Meaning | |
| 90 | +|--------|---------| |
| 91 | +| `"latest"` (or omitted) | Always use the latest version (auto-increments on changes) | |
| 92 | +| `"v0"`, `"v1"`, `"v2"`, ... | Pin to a specific version number | |
| 93 | +| `"version0"`, `"version1"`, ... | Equivalent long form (case-insensitive) | |
| 94 | + |
| 95 | +## Staged Publishing (`--no-promote`) |
| 96 | + |
| 97 | +By default, `feast apply` atomically saves a version snapshot **and** promotes it to the active definition. For breaking schema changes, you may want to stage the new version without disrupting unversioned consumers. |
| 98 | + |
| 99 | +The `--no-promote` flag saves the version snapshot without updating the active feature view definition. The new version is accessible only via explicit `@v<N>` reads and `--version` materialization. |
| 100 | + |
| 101 | +**CLI usage:** |
| 102 | + |
| 103 | +```bash |
| 104 | +feast apply --no-promote |
| 105 | +``` |
| 106 | + |
| 107 | +**Python SDK equivalent:** |
| 108 | + |
| 109 | +```python |
| 110 | +store.apply([entity, feature_view], no_promote=True) |
| 111 | +``` |
| 112 | + |
| 113 | +### Phased rollout workflow |
| 114 | + |
| 115 | +1. **Stage the new version:** |
| 116 | + ```bash |
| 117 | + feast apply --no-promote |
| 118 | + ``` |
| 119 | + This publishes v2 without promoting it. All unversioned consumers continue using v1. |
| 120 | + |
| 121 | +2. **Populate the v2 online table:** |
| 122 | + ```bash |
| 123 | + feast materialize --views driver_stats --version v2 ... |
| 124 | + ``` |
| 125 | + |
| 126 | +3. **Migrate consumers one at a time:** |
| 127 | + - Consumer A switches to `driver_stats@v2:trips_today` |
| 128 | + - Consumer B switches to `driver_stats@v2:avg_rating` |
| 129 | + |
| 130 | +4. **Promote v2 as the default:** |
| 131 | + ```bash |
| 132 | + feast apply |
| 133 | + ``` |
| 134 | + Or pin to v2: set `version="v2"` in the definition and run `feast apply`. |
| 135 | + |
| 136 | +## Listing Version History |
| 137 | + |
| 138 | +Use the CLI to inspect version history: |
| 139 | + |
| 140 | +```bash |
| 141 | +feast feature-views list-versions driver_stats |
| 142 | +``` |
| 143 | + |
| 144 | +```text |
| 145 | +VERSION TYPE CREATED VERSION_ID |
| 146 | +v0 feature_view 2024-01-15 10:30:00 a1b2c3d4-... |
| 147 | +v1 feature_view 2024-01-16 14:22:00 e5f6g7h8-... |
| 148 | +v2 feature_view 2024-01-20 09:15:00 i9j0k1l2-... |
| 149 | +``` |
| 150 | + |
| 151 | +Or programmatically via the Python SDK: |
| 152 | + |
| 153 | +```python |
| 154 | +store = FeatureStore(repo_path=".") |
| 155 | +versions = store.list_feature_view_versions("driver_stats") |
| 156 | +for v in versions: |
| 157 | + print(f"{v['version']} created at {v['created_timestamp']}") |
| 158 | +``` |
| 159 | + |
| 160 | +## Version-Qualified Feature References |
| 161 | + |
| 162 | +You can read features from a **specific version** of a feature view by using version-qualified feature references with the `@v<N>` syntax: |
| 163 | + |
| 164 | +```python |
| 165 | +online_features = store.get_online_features( |
| 166 | + features=[ |
| 167 | + "driver_stats:trips_today", # latest version (default) |
| 168 | + "driver_stats@v2:trips_today", # specific version |
| 169 | + "driver_stats@latest:trips_today", # explicit latest |
| 170 | + ], |
| 171 | + entity_rows=[{"driver_id": 1001}], |
| 172 | +) |
| 173 | +``` |
| 174 | + |
| 175 | +**How it works:** |
| 176 | + |
| 177 | +* `driver_stats:trips_today` is equivalent to `driver_stats@latest:trips_today` — it reads from the currently active version |
| 178 | +* `driver_stats@v2:trips_today` reads from the v2 snapshot stored in version history, using a version-specific online store table |
| 179 | +* Multiple versions of the same feature view can be queried in a single request (e.g., `driver_stats@v1:trips` and `driver_stats@v2:trips_daily`) |
| 180 | + |
| 181 | +**Backward compatibility:** |
| 182 | + |
| 183 | +* The unversioned online store table (e.g., `project_driver_stats`) is treated as v0 |
| 184 | +* Only versions >= 1 get `_v{N}` suffixed tables (e.g., `project_driver_stats_v1`) |
| 185 | +* Pre-versioning users' existing data continues to work without changes — `@latest` resolves to the active version, which for existing unversioned FVs is v0 |
| 186 | + |
| 187 | +**Materialization:** Each version requires its own materialization. After applying a new version, run `feast materialize` to populate the versioned table before querying it with `@v<N>`. |
| 188 | + |
| 189 | +## Supported Feature View Types |
| 190 | + |
| 191 | +Versioning is supported on all three feature view types: |
| 192 | + |
| 193 | +* `FeatureView` (and `BatchFeatureView`) |
| 194 | +* `StreamFeatureView` |
| 195 | +* `OnDemandFeatureView` |
| 196 | + |
| 197 | +## Online Store Support |
| 198 | + |
| 199 | +{% hint style="info" %} |
| 200 | +**Currently, version-qualified online reads (`@v<N>`) are only supported with the SQLite online store.** Support for additional online stores (Redis, DynamoDB, Bigtable, Postgres, etc.) will be added based on community priority. |
| 201 | + |
| 202 | +If you need versioned online reads for a specific online store, please [open a GitHub issue](https://github.com/feast-dev/feast/issues/new) describing your use case and which store you need. This helps us prioritize development. |
| 203 | +{% endhint %} |
| 204 | + |
| 205 | +Version history tracking in the registry (listing versions, pinning, `--no-promote`) works with **all** registry backends (file, SQL, Snowflake). |
| 206 | + |
| 207 | +## Full Details |
| 208 | + |
| 209 | +For the complete design, concurrency semantics, and feature service interactions, see the [Feature View Versioning RFC](../rfcs/feature-view-versioning.md). |
| 210 | + |
| 211 | +## Naming Restrictions |
| 212 | + |
| 213 | +Feature references use a structured format: `feature_view_name@v<N>:feature_name`. To avoid |
| 214 | +ambiguity, the following characters are reserved and must not appear in feature view or feature names: |
| 215 | + |
| 216 | +- **`@`** — Reserved as the version delimiter (e.g., `driver_stats@v2:trips_today`). `feast apply` |
| 217 | + will reject feature views with `@` in their name. If you have existing feature views with `@` in |
| 218 | + their names, they will continue to work for unversioned reads, but we recommend renaming them to |
| 219 | + avoid ambiguity with the `@v<N>` syntax. |
| 220 | +- **`:`** — Reserved as the separator between feature view name and feature name in fully qualified |
| 221 | + feature references (e.g., `driver_stats:trips_today`). |
| 222 | + |
| 223 | +## Known Limitations |
| 224 | + |
| 225 | +- **Online store coverage** — Version-qualified reads (`@v<N>`) are SQLite-only today. Other online stores are follow-up work. |
| 226 | +- **Offline store versioning** — Versioned historical retrieval is not yet supported. |
| 227 | +- **Version deletion** — There is no mechanism to prune old versions from the registry. |
| 228 | +- **Cross-version joins** — Joining features from different versions of the same feature view in `get_historical_features` is not supported. |
| 229 | +- **Feature services** — Feature services always resolve to the active (promoted) version. `--no-promote` versions are not served until promoted. |