Skip to content

Commit 9ae2eed

Browse files
committed
feat: Feast First-Class LabelView Implementation
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent c559889 commit 9ae2eed

46 files changed

Lines changed: 2597 additions & 618 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* [Data ingestion](getting-started/concepts/data-ingestion.md)
2525
* [Entity](getting-started/concepts/entity.md)
2626
* [Feature view](getting-started/concepts/feature-view.md)
27+
* [\[Alpha\] Label view](getting-started/concepts/label-view.md)
2728
* [Feature retrieval](getting-started/concepts/feature-retrieval.md)
2829
* [Point-in-time joins](getting-started/concepts/point-in-time-joins.md)
2930
* [\[Alpha\] Saved dataset](getting-started/concepts/dataset.md)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Label View
2+
3+
{% hint style="info" %}
4+
**\[Alpha]** Label views are an alpha feature. The API may change in future releases.
5+
{% endhint %}
6+
7+
A **label view** is a Feast primitive that manages *mutable* labels and annotations, kept separate from the *immutable* feature data stored in regular [feature views](feature-view.md). This separation follows a clean design principle: observational data (features) is append-only, while judgments about that data (labels, scores, reward signals) are updated over time by multiple independent sources.
8+
9+
Label views are especially useful in **RLHF/reward-modeling pipelines**, **multi-annotator workflows**, and **safety monitoring systems** where different labelers — human reviewers, automated scanners, reward models — independently write labels for the same entity keys.
10+
11+
## Key Capabilities
12+
13+
- **Multi-labeler support**: Multiple independent labelers can write labels for the same entity key. A configurable `labeler_field` tracks which source wrote each label.
14+
- **Conflict resolution policies**: When labelers disagree, Feast resolves conflicts according to a `ConflictPolicy` — last-write-wins, labeler priority, or majority vote. See [Alpha limitations](#alpha-limitations) below.
15+
- **History retention**: Optionally retain the full history of label writes per entity key, not just the latest value. See [Alpha limitations](#alpha-limitations) below.
16+
- **Reference feature view**: Optionally link a label view to the `FeatureView` whose entities it annotates, for documentation and lineage.
17+
- **PushSource integration**: Label views are designed to work with `PushSource`, allowing labels to be written in real time via `FeatureStore.push()`.
18+
- **FeatureService composability**: Label views can be included alongside regular feature views in a `FeatureService`, so training pipelines can retrieve features and their labels together.
19+
20+
## When to use Label Views
21+
22+
| Use a **FeatureView** when… | Use a **LabelView** when… |
23+
|---|---|
24+
| Data is observational and append-only (e.g. driver trip counts, page views) | Data is a judgment or annotation about an entity (e.g. reward labels, safety scores) |
25+
| A single source of truth writes the data | Multiple labelers may write conflicting values for the same key |
26+
| History is naturally time-series | You need explicit control over whether history is retained or overwritten |
27+
28+
## Defining a Label View
29+
30+
```python
31+
from datetime import timedelta
32+
33+
from feast import Entity, FeatureService, Field, PushSource
34+
from feast.labeling import ConflictPolicy, LabelView
35+
from feast.types import Float32, String
36+
37+
interaction = Entity(
38+
name="interaction",
39+
join_keys=["interaction_id"],
40+
)
41+
42+
label_source = PushSource(
43+
name="label_push_source",
44+
schema=[
45+
Field(name="interaction_id", dtype=String),
46+
Field(name="reward_label", dtype=String),
47+
Field(name="safety_score", dtype=Float32),
48+
Field(name="labeler", dtype=String),
49+
],
50+
)
51+
52+
interaction_labels = LabelView(
53+
name="interaction_labels",
54+
entities=[interaction],
55+
ttl=timedelta(days=90),
56+
schema=[
57+
Field(name="interaction_id", dtype=String),
58+
Field(name="reward_label", dtype=String),
59+
Field(name="safety_score", dtype=Float32),
60+
Field(name="labeler", dtype=String),
61+
],
62+
source=label_source,
63+
labeler_field="labeler",
64+
conflict_policy=ConflictPolicy.LAST_WRITE_WINS,
65+
retain_history=True,
66+
reference_feature_view="interaction_history",
67+
description="Reward and safety labels on agent interactions.",
68+
owner="ml-safety-team@example.com",
69+
)
70+
```
71+
72+
## Conflict Policies
73+
74+
The `ConflictPolicy` enum controls how conflicting labels from different labelers are **intended** to be resolved at read time:
75+
76+
| Policy | Behavior |
77+
|---|---|
78+
| `LAST_WRITE_WINS` | The most recently written label for a given entity key takes precedence, regardless of which labeler wrote it. This is the default. |
79+
| `LABELER_PRIORITY` | Labels are ranked by a pre-configured labeler priority order. Higher-priority labelers override lower-priority ones. |
80+
| `MAJORITY_VOTE` | The label value that appears most frequently across all labelers is selected. Useful for consensus-based annotation workflows. |
81+
82+
## Alpha Limitations
83+
84+
{% hint style="warning" %}
85+
The following capabilities are **defined and stored** in the label-view metadata but are **not yet enforced** by the Feast runtime. They are persisted in the registry so that future releases can activate them without a schema migration.
86+
{% endhint %}
87+
88+
### Conflict-policy enforcement at read time
89+
90+
`conflict_policy` is stored as part of the `LabelView` definition, but it is **not enforced** during `get_online_features`. The online store currently returns the last-written row for a given entity key regardless of which policy is configured.
91+
92+
Real enforcement will require changes to the online-store query path so that the store can consider multiple rows per entity key and apply the conflict-resolution strategy.
93+
94+
### History retention at write time
95+
96+
`retain_history` is stored but **not acted on**. The online store always overwrites the previous value when a new label is written for the same entity key.
97+
98+
Implementing retention will require changes to the online-store write path so that it appends rather than upserts, along with a compaction or eviction strategy for old entries.
99+
100+
### Batch materialization
101+
102+
Label views are **not included** in `feast materialize` or `feast materialize-incremental`. Labels are ingested via `FeatureStore.push()` (real-time) and do not go through the batch materialization pipeline. Attempting to materialize a label view by name will raise a clear error.
103+
104+
## Using with Feature Services
105+
106+
Label views can be composed with regular feature views in a `FeatureService`, so downstream consumers (training pipelines, batch scoring jobs) get features and labels in a single retrieval call:
107+
108+
```python
109+
training_service = FeatureService(
110+
name="interaction_training_service",
111+
features=[
112+
interaction_history, # regular FeatureView with immutable features
113+
interaction_labels, # LabelView with mutable reward labels
114+
],
115+
)
116+
```
117+
118+
## Pushing Labels
119+
120+
Labels are typically written via `FeatureStore.push()` using the label view's `PushSource`:
121+
122+
```python
123+
import pandas as pd
124+
from feast import FeatureStore
125+
126+
store = FeatureStore(repo_path="feature_repo/")
127+
128+
labels_df = pd.DataFrame({
129+
"interaction_id": ["int-001", "int-002"],
130+
"reward_label": ["positive", "negative"],
131+
"safety_score": [0.95, 0.12],
132+
"labeler": ["nemo_guardrails", "nemo_guardrails"],
133+
"event_timestamp": pd.to_datetime(["2025-01-15", "2025-01-15"]),
134+
})
135+
136+
store.push("label_push_source", labels_df)
137+
```
138+
139+
This writes the labels into both the online and offline stores, making them available for real-time serving and historical training dataset generation.

protos/feast/core/LabelView.proto

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//
2+
// Copyright 2026 The Feast Authors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
syntax = "proto3";
18+
package feast.core;
19+
20+
option go_package = "github.com/feast-dev/feast/go/protos/feast/core";
21+
option java_outer_classname = "LabelViewProto";
22+
option java_package = "feast.proto.core";
23+
24+
import "google/protobuf/duration.proto";
25+
import "google/protobuf/timestamp.proto";
26+
import "feast/core/DataSource.proto";
27+
import "feast/core/Feature.proto";
28+
29+
message LabelView {
30+
LabelViewSpec spec = 1;
31+
LabelViewMeta meta = 2;
32+
}
33+
34+
enum ConflictResolutionPolicy {
35+
LAST_WRITE_WINS = 0;
36+
LABELER_PRIORITY = 1;
37+
MAJORITY_VOTE = 2;
38+
}
39+
40+
// Next available id: 16
41+
message LabelViewSpec {
42+
// Name of the label view. Must be unique. Not updated.
43+
string name = 1;
44+
45+
// Name of Feast project that this label view belongs to.
46+
string project = 2;
47+
48+
// List of names of entities associated with this label view.
49+
repeated string entities = 3;
50+
51+
// List of specifications for each label field defined as part of this label view.
52+
repeated FeatureSpecV2 features = 4;
53+
54+
// User defined metadata.
55+
map<string, string> tags = 5;
56+
57+
// Labels older than ttl will be excluded from online serving.
58+
google.protobuf.Duration ttl = 6;
59+
60+
// DataSource (typically a PushSource) that feeds label data into this view.
61+
DataSource source = 7;
62+
63+
// Whether labels should be served from the online store.
64+
bool online = 8;
65+
66+
// Description of the label view.
67+
string description = 9;
68+
69+
// Owner of the label view.
70+
string owner = 10;
71+
72+
// List of specifications for each entity column.
73+
repeated FeatureSpecV2 entity_columns = 11;
74+
75+
// The schema field that identifies the labeler (e.g. "labeler").
76+
string labeler_field = 12;
77+
78+
// How conflicting labels from different labelers are resolved.
79+
// NOTE: stored but NOT enforced at read time in the current alpha release.
80+
ConflictResolutionPolicy conflict_policy = 13;
81+
82+
// Whether to retain full label history (all writes) or only the latest.
83+
// NOTE: stored but NOT enforced at write time in the current alpha release.
84+
bool retain_history = 14;
85+
86+
// Optional name of the FeatureView whose entities this label view annotates.
87+
string reference_feature_view = 15;
88+
}
89+
90+
message LabelViewMeta {
91+
// Time when this Label View was created.
92+
google.protobuf.Timestamp created_timestamp = 1;
93+
94+
// Time when this Label View was last updated.
95+
google.protobuf.Timestamp last_updated_timestamp = 2;
96+
}

protos/feast/core/Permission.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ message PermissionSpec {
4646
SAVED_DATASET = 8;
4747
PERMISSION = 9;
4848
PROJECT = 10;
49+
LABEL_VIEW = 11;
4950
}
5051

5152
repeated Type types = 3;

protos/feast/core/Registry.proto

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ import "google/protobuf/timestamp.proto";
3535
import "feast/core/Permission.proto";
3636
import "feast/core/Project.proto";
3737
import "feast/core/FeatureViewVersion.proto";
38+
import "feast/core/LabelView.proto";
3839

39-
// Next id: 19
40+
// Next id: 20
4041
message Registry {
4142
repeated Entity entities = 1;
4243
repeated FeatureTable feature_tables = 2;
@@ -57,6 +58,7 @@ message Registry {
5758
repeated Permission permissions = 16;
5859
repeated Project projects = 17;
5960
FeatureViewVersionHistory feature_view_version_history = 18;
61+
repeated LabelView label_views = 19;
6062
}
6163

6264
message ProjectMetadata {

protos/feast/registry/RegistryServer.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import "feast/core/FeatureService.proto";
1414
import "feast/core/SavedDataset.proto";
1515
import "feast/core/ValidationProfile.proto";
1616
import "feast/core/InfraObject.proto";
17+
import "feast/core/LabelView.proto";
1718
import "feast/core/Permission.proto";
1819
import "feast/core/Project.proto";
1920

@@ -220,6 +221,7 @@ message ApplyFeatureViewRequest {
220221
feast.core.FeatureView feature_view = 1;
221222
feast.core.OnDemandFeatureView on_demand_feature_view = 2;
222223
feast.core.StreamFeatureView stream_feature_view = 3;
224+
feast.core.LabelView label_view = 6;
223225
}
224226
string project = 4;
225227
bool commit = 5;

sdk/python/docs/index.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ Stream Feature View
128128
.. autoclass:: feast.stream_feature_view.StreamFeatureView
129129
:members:
130130

131+
Label View
132+
----------------------
133+
134+
.. autoclass:: feast.labeling.label_view.LabelView
135+
:members:
136+
137+
Conflict Policy
138+
----------------------
139+
140+
.. autoclass:: feast.labeling.conflict_policy.ConflictPolicy
141+
:members:
142+
131143
Field
132144
==================
133145

sdk/python/docs/source/feast.protos.feast.core.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@ feast.protos.feast.core.InfraObject\_pb2\_grpc module
180180
:undoc-members:
181181
:show-inheritance:
182182

183+
feast.protos.feast.core.LabelView\_pb2 module
184+
----------------------------------------------
185+
186+
.. automodule:: feast.protos.feast.core.LabelView_pb2
187+
:members:
188+
:undoc-members:
189+
:show-inheritance:
190+
183191
feast.protos.feast.core.OnDemandFeatureView\_pb2 module
184192
-------------------------------------------------------
185193

sdk/python/feast/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .feature_store import FeatureStore
2626
from .feature_view import FeatureView
2727
from .field import Field
28+
from .labeling import ConflictPolicy, LabelView
2829
from .on_demand_feature_view import OnDemandFeatureView
2930
from .project import Project
3031
from .repo_config import RepoConfig
@@ -51,6 +52,8 @@
5152
"FeatureService",
5253
"FeatureStore",
5354
"FeatureView",
55+
"LabelView",
56+
"ConflictPolicy",
5457
"OnDemandFeatureView",
5558
"RepoConfig",
5659
"StreamFeatureView",

sdk/python/feast/base_feature_view.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from feast.feature_view_projection import FeatureViewProjection
2323
from feast.field import Field
2424
from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto
25+
from feast.protos.feast.core.LabelView_pb2 import LabelView as LabelViewProto
2526
from feast.protos.feast.core.OnDemandFeatureView_pb2 import (
2627
OnDemandFeatureView as OnDemandFeatureViewProto,
2728
)
@@ -109,7 +110,12 @@ def proto_class(self) -> Type[Message]:
109110
@abstractmethod
110111
def to_proto(
111112
self,
112-
) -> Union[FeatureViewProto, OnDemandFeatureViewProto, StreamFeatureViewProto]:
113+
) -> Union[
114+
FeatureViewProto,
115+
OnDemandFeatureViewProto,
116+
StreamFeatureViewProto,
117+
LabelViewProto,
118+
]:
113119
pass
114120

115121
@classmethod

0 commit comments

Comments
 (0)