From e707552d51135b84b5fbdc978dd4b0a0dcd1cff6 Mon Sep 17 00:00:00 2001 From: hao-xu5 Date: Wed, 24 Sep 2025 09:55:44 -0700 Subject: [PATCH 1/4] add aggregation in OnDemandFeatureView Signed-off-by: hao-xu5 --- sdk/python/feast/on_demand_feature_view.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 82246d8b3ac..6e578798b9b 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -8,6 +8,7 @@ import pyarrow from typeguard import typechecked +from feast.aggregation import Aggregation from feast.base_feature_view import BaseFeatureView from feast.data_source import RequestSource from feast.entity import Entity @@ -76,6 +77,7 @@ class OnDemandFeatureView(BaseFeatureView): singleton: bool udf: Optional[FunctionType] udf_string: Optional[str] + aggregations: List[Aggregation] def __init__( # noqa: C901 self, @@ -93,6 +95,7 @@ def __init__( # noqa: C901 owner: str = "", write_to_online_store: bool = False, singleton: bool = False, + aggregations: Optional[List[Aggregation]] = None, ): """ Creates an OnDemandFeatureView object. @@ -118,6 +121,7 @@ def __init__( # noqa: C901 the online store for faster retrieval. singleton (optional): A boolean that indicates whether the transformation is executed on a singleton (only applicable when mode="python"). + aggregations (optional): List of aggregations to apply before transformation. """ super().__init__( name=name, @@ -187,6 +191,7 @@ def __init__( # noqa: C901 self.singleton = singleton if self.singleton and self.mode != "python": raise ValueError("Singleton is only supported for Python mode.") + self.aggregations = aggregations or [] def get_feature_transformation(self) -> Transformation: if not self.udf: @@ -251,6 +256,7 @@ def __eq__(self, other): or self.write_to_online_store != other.write_to_online_store or sorted(self.entity_columns) != sorted(other.entity_columns) or self.singleton != other.singleton + or self.aggregations != other.aggregations ): return False From 8d8dcde3e25276849d02bc81c4533beb7bfd3052 Mon Sep 17 00:00:00 2001 From: hao-xu5 Date: Thu, 25 Sep 2025 22:08:28 -0700 Subject: [PATCH 2/4] udpate proto Signed-off-by: hao-xu5 --- protos/feast/core/OnDemandFeatureView.proto | 5 +++ .../protos/feast/core/FeatureService_pb2.pyi | 2 +- .../feast/core/OnDemandFeatureView_pb2.py | 35 ++++++++++--------- .../feast/core/OnDemandFeatureView_pb2.pyi | 8 ++++- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/protos/feast/core/OnDemandFeatureView.proto b/protos/feast/core/OnDemandFeatureView.proto index 3ed8ffe4aed..4b8dabb4f39 100644 --- a/protos/feast/core/OnDemandFeatureView.proto +++ b/protos/feast/core/OnDemandFeatureView.proto @@ -28,6 +28,7 @@ import "feast/core/FeatureViewProjection.proto"; import "feast/core/Feature.proto"; import "feast/core/DataSource.proto"; import "feast/core/Transformation.proto"; +import "feast/core/Aggregation.proto"; message OnDemandFeatureView { // User-specified specifications of this feature view. @@ -70,6 +71,10 @@ message OnDemandFeatureViewSpec { // List of specifications for each entity defined as part of this feature view. repeated FeatureSpecV2 entity_columns = 14; bool singleton = 15; + + // Aggregation definitions + repeated Aggregation aggregations = 16; + } message OnDemandFeatureViewMeta { diff --git a/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi index 91f04c1a7d6..6d5879e52cb 100644 --- a/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi @@ -71,7 +71,7 @@ class FeatureServiceSpec(google.protobuf.message.Message): """Name of Feast project that this Feature Service belongs to.""" @property def features(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.FeatureViewProjection_pb2.FeatureViewProjection]: - """Represents a projection that's to be applied on top of the FeatureView. + """Represents a projection that's to be applied on top of the FeatureView. Contains data such as the features to use from a FeatureView. """ @property diff --git a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py index 926b54df288..5b8ec9b11f6 100644 --- a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py +++ b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py @@ -18,9 +18,10 @@ from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 from feast.protos.feast.core import Transformation_pb2 as feast_dot_core_dot_Transformation__pb2 +from feast.protos.feast.core import Aggregation_pb2 as feast_dot_core_dot_Aggregation__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$feast/core/OnDemandFeatureView.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a&feast/core/FeatureViewProjection.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\"{\n\x13OnDemandFeatureView\x12\x31\n\x04spec\x18\x01 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewSpec\x12\x31\n\x04meta\x18\x02 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewMeta\"\x90\x05\n\x17OnDemandFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12+\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x41\n\x07sources\x18\x04 \x03(\x0b\x32\x30.feast.core.OnDemandFeatureViewSpec.SourcesEntry\x12\x42\n\x15user_defined_function\x18\x05 \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\n \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12;\n\x04tags\x18\x07 \x03(\x0b\x32-.feast.core.OnDemandFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12\x0c\n\x04mode\x18\x0b \x01(\t\x12\x1d\n\x15write_to_online_store\x18\x0c \x01(\x08\x12\x10\n\x08\x65ntities\x18\r \x03(\t\x12\x31\n\x0e\x65ntity_columns\x18\x0e \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x11\n\tsingleton\x18\x0f \x01(\x08\x1aJ\n\x0cSourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.core.OnDemandSource:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x01\n\x17OnDemandFeatureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc8\x01\n\x0eOnDemandSource\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x44\n\x17\x66\x65\x61ture_view_projection\x18\x03 \x01(\x0b\x32!.feast.core.FeatureViewProjectionH\x00\x12\x35\n\x13request_data_source\x18\x02 \x01(\x0b\x32\x16.feast.core.DataSourceH\x00\x42\x08\n\x06source\"H\n\x13UserDefinedFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\x0c\x12\x11\n\tbody_text\x18\x03 \x01(\t:\x02\x18\x01\"X\n\x17OnDemandFeatureViewList\x12=\n\x14ondemandfeatureviews\x18\x01 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureViewB]\n\x10\x66\x65\x61st.proto.coreB\x18OnDemandFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$feast/core/OnDemandFeatureView.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a&feast/core/FeatureViewProjection.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\x1a\x1c\x66\x65\x61st/core/Aggregation.proto\"{\n\x13OnDemandFeatureView\x12\x31\n\x04spec\x18\x01 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewSpec\x12\x31\n\x04meta\x18\x02 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewMeta\"\xbf\x05\n\x17OnDemandFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12+\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x41\n\x07sources\x18\x04 \x03(\x0b\x32\x30.feast.core.OnDemandFeatureViewSpec.SourcesEntry\x12\x42\n\x15user_defined_function\x18\x05 \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\n \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12;\n\x04tags\x18\x07 \x03(\x0b\x32-.feast.core.OnDemandFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12\x0c\n\x04mode\x18\x0b \x01(\t\x12\x1d\n\x15write_to_online_store\x18\x0c \x01(\x08\x12\x10\n\x08\x65ntities\x18\r \x03(\t\x12\x31\n\x0e\x65ntity_columns\x18\x0e \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x11\n\tsingleton\x18\x0f \x01(\x08\x12-\n\x0c\x61ggregations\x18\x10 \x03(\x0b\x32\x17.feast.core.Aggregation\x1aJ\n\x0cSourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.core.OnDemandSource:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x01\n\x17OnDemandFeatureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc8\x01\n\x0eOnDemandSource\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x44\n\x17\x66\x65\x61ture_view_projection\x18\x03 \x01(\x0b\x32!.feast.core.FeatureViewProjectionH\x00\x12\x35\n\x13request_data_source\x18\x02 \x01(\x0b\x32\x16.feast.core.DataSourceH\x00\x42\x08\n\x06source\"H\n\x13UserDefinedFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\x0c\x12\x11\n\tbody_text\x18\x03 \x01(\t:\x02\x18\x01\"X\n\x17OnDemandFeatureViewList\x12=\n\x14ondemandfeatureviews\x18\x01 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureViewB]\n\x10\x66\x65\x61st.proto.coreB\x18OnDemandFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,20 +37,20 @@ _globals['_ONDEMANDFEATUREVIEWSPEC'].fields_by_name['user_defined_function']._serialized_options = b'\030\001' _globals['_USERDEFINEDFUNCTION']._options = None _globals['_USERDEFINEDFUNCTION']._serialized_options = b'\030\001' - _globals['_ONDEMANDFEATUREVIEW']._serialized_start=243 - _globals['_ONDEMANDFEATUREVIEW']._serialized_end=366 - _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_start=369 - _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_end=1025 - _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_start=906 - _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_end=980 - _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=982 - _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=1025 - _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_start=1028 - _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_end=1168 - _globals['_ONDEMANDSOURCE']._serialized_start=1171 - _globals['_ONDEMANDSOURCE']._serialized_end=1371 - _globals['_USERDEFINEDFUNCTION']._serialized_start=1373 - _globals['_USERDEFINEDFUNCTION']._serialized_end=1445 - _globals['_ONDEMANDFEATUREVIEWLIST']._serialized_start=1447 - _globals['_ONDEMANDFEATUREVIEWLIST']._serialized_end=1535 + _globals['_ONDEMANDFEATUREVIEW']._serialized_start=273 + _globals['_ONDEMANDFEATUREVIEW']._serialized_end=396 + _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_start=399 + _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_end=1102 + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_start=983 + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_end=1057 + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=1059 + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=1102 + _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_start=1105 + _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_end=1245 + _globals['_ONDEMANDSOURCE']._serialized_start=1248 + _globals['_ONDEMANDSOURCE']._serialized_end=1448 + _globals['_USERDEFINEDFUNCTION']._serialized_start=1450 + _globals['_USERDEFINEDFUNCTION']._serialized_end=1522 + _globals['_ONDEMANDFEATUREVIEWLIST']._serialized_start=1524 + _globals['_ONDEMANDFEATUREVIEWLIST']._serialized_end=1612 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi index c9fca2f550d..c424c442ee7 100644 --- a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi @@ -18,6 +18,7 @@ limitations under the License. """ import builtins import collections.abc +import feast.core.Aggregation_pb2 import feast.core.DataSource_pb2 import feast.core.FeatureViewProjection_pb2 import feast.core.FeatureView_pb2 @@ -108,6 +109,7 @@ class OnDemandFeatureViewSpec(google.protobuf.message.Message): ENTITIES_FIELD_NUMBER: builtins.int ENTITY_COLUMNS_FIELD_NUMBER: builtins.int SINGLETON_FIELD_NUMBER: builtins.int + AGGREGATIONS_FIELD_NUMBER: builtins.int name: builtins.str """Name of the feature view. Must be unique. Not updated.""" project: builtins.str @@ -139,6 +141,9 @@ class OnDemandFeatureViewSpec(google.protobuf.message.Message): def entity_columns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: """List of specifications for each entity defined as part of this feature view.""" singleton: builtins.bool + @property + def aggregations(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Aggregation_pb2.Aggregation]: + """Aggregation definitions""" def __init__( self, *, @@ -156,9 +161,10 @@ class OnDemandFeatureViewSpec(google.protobuf.message.Message): entities: collections.abc.Iterable[builtins.str] | None = ..., entity_columns: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., singleton: builtins.bool = ..., + aggregations: collections.abc.Iterable[feast.core.Aggregation_pb2.Aggregation] | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["feature_transformation", b"feature_transformation", "user_defined_function", b"user_defined_function"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "owner", b"owner", "project", b"project", "singleton", b"singleton", "sources", b"sources", "tags", b"tags", "user_defined_function", b"user_defined_function", "write_to_online_store", b"write_to_online_store"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["aggregations", b"aggregations", "description", b"description", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "owner", b"owner", "project", b"project", "singleton", b"singleton", "sources", b"sources", "tags", b"tags", "user_defined_function", b"user_defined_function", "write_to_online_store", b"write_to_online_store"]) -> None: ... global___OnDemandFeatureViewSpec = OnDemandFeatureViewSpec From d0aa3d6976ac7b73142cb37dcf2d22aefac88847 Mon Sep 17 00:00:00 2001 From: hao-xu5 Date: Thu, 25 Sep 2025 22:23:32 -0700 Subject: [PATCH 3/4] fix lint Signed-off-by: hao-xu5 --- sdk/python/feast/on_demand_feature_view.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 6e578798b9b..51660a689f6 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -348,6 +348,7 @@ def to_proto(self) -> OnDemandFeatureViewProto: owner=self.owner, write_to_online_store=self.write_to_online_store, singleton=self.singleton if self.singleton else False, + aggregations=self.aggregations ) return OnDemandFeatureViewProto(spec=spec, meta=meta) @@ -457,6 +458,12 @@ def from_proto( if hasattr(on_demand_feature_view_proto.spec, "singleton"): singleton = on_demand_feature_view_proto.spec.singleton + aggregations = [] + if hasattr(on_demand_feature_view_proto.spec, "aggregations"): + aggregations = [ + Aggregation.from_proto(aggregation_proto) + for aggregation_proto in on_demand_feature_view_proto.spec.aggregations + ] on_demand_feature_view_obj = cls( name=on_demand_feature_view_proto.spec.name, schema=[ @@ -477,6 +484,7 @@ def from_proto( owner=on_demand_feature_view_proto.spec.owner, write_to_online_store=write_to_online_store, singleton=singleton, + aggregations=aggregations ) on_demand_feature_view_obj.entities = entities From 175b8d11eb1506799eef108955294a0fc789d213 Mon Sep 17 00:00:00 2001 From: hao-xu5 Date: Thu, 25 Sep 2025 22:29:16 -0700 Subject: [PATCH 4/4] fix lint Signed-off-by: hao-xu5 --- sdk/python/feast/on_demand_feature_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 51660a689f6..c985912a03e 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -348,7 +348,7 @@ def to_proto(self) -> OnDemandFeatureViewProto: owner=self.owner, write_to_online_store=self.write_to_online_store, singleton=self.singleton if self.singleton else False, - aggregations=self.aggregations + aggregations=self.aggregations, ) return OnDemandFeatureViewProto(spec=spec, meta=meta) @@ -484,7 +484,7 @@ def from_proto( owner=on_demand_feature_view_proto.spec.owner, write_to_online_store=write_to_online_store, singleton=singleton, - aggregations=aggregations + aggregations=aggregations, ) on_demand_feature_view_obj.entities = entities