Skip to content

Commit f5f5500

Browse files
authored
Allows registering of features in request data as RequestFeatureView. Refactors common logic into a BaseFeatureView class (feast-dev#1931)
* Initial implementation of Request Feature View without validation, with some refactoring of feature views Signed-off-by: Danny Chiao <[email protected]> * Refactor constructor in base class Signed-off-by: Danny Chiao <[email protected]> * Refactor to have constructor init in base class Signed-off-by: Danny Chiao <[email protected]> * Moving copy methods into base class Signed-off-by: Danny Chiao <[email protected]> * lint Signed-off-by: Danny Chiao <[email protected]> * Implement request feature view validation in historical and online retrieval Signed-off-by: Danny Chiao <[email protected]> * Add tests for request_fv Signed-off-by: Danny Chiao <[email protected]> * Update CLI to understand request feature views Signed-off-by: Danny Chiao <[email protected]> * Fix error to be more generic and apply to all feature views Signed-off-by: Danny Chiao <[email protected]> * Add type of feature view to feature view list command Signed-off-by: Danny Chiao <[email protected]> * Add type of feature view to feature view list command Signed-off-by: Danny Chiao <[email protected]> * Lint imports Signed-off-by: Danny Chiao <[email protected]> * Fix new lines and nits Signed-off-by: Danny Chiao <[email protected]> * Fix repo apply bug Signed-off-by: Danny Chiao <[email protected]> * Fix comments Signed-off-by: Danny Chiao <[email protected]> * format Signed-off-by: Danny Chiao <[email protected]> * fix test Signed-off-by: Danny Chiao <[email protected]> * reverse naming Signed-off-by: Danny Chiao <[email protected]> * reverse naming Signed-off-by: Danny Chiao <[email protected]> * reverse naming Signed-off-by: Danny Chiao <[email protected]> * Add back to cli Signed-off-by: Danny Chiao <[email protected]> * Comments Signed-off-by: Danny Chiao <[email protected]> * Lint Signed-off-by: Danny Chiao <[email protected]> * Remove extra data in response Signed-off-by: Danny Chiao <[email protected]> * revert change Signed-off-by: Danny Chiao <[email protected]> * revert change Signed-off-by: Danny Chiao <[email protected]>
1 parent cbfc72a commit f5f5500

File tree

18 files changed

+799
-284
lines changed

18 files changed

+799
-284
lines changed

protos/feast/core/Registry.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ import "feast/core/FeatureService.proto";
2626
import "feast/core/FeatureTable.proto";
2727
import "feast/core/FeatureView.proto";
2828
import "feast/core/OnDemandFeatureView.proto";
29+
import "feast/core/RequestFeatureView.proto";
2930
import "google/protobuf/timestamp.proto";
3031

3132
message Registry {
3233
repeated Entity entities = 1;
3334
repeated FeatureTable feature_tables = 2;
3435
repeated FeatureView feature_views = 6;
3536
repeated OnDemandFeatureView on_demand_feature_views = 8;
37+
repeated RequestFeatureView request_feature_views = 9;
3638
repeated FeatureService feature_services = 7;
3739

3840
string registry_schema_version = 3; // to support migrations; incremented when schema is changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// Copyright 2021 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+
18+
syntax = "proto3";
19+
package feast.core;
20+
21+
option go_package = "github.com/feast-dev/feast/sdk/go/protos/feast/core";
22+
option java_outer_classname = "RequestFeatureViewProto";
23+
option java_package = "feast.proto.core";
24+
25+
import "feast/core/FeatureView.proto";
26+
import "feast/core/Feature.proto";
27+
import "feast/core/DataSource.proto";
28+
29+
message RequestFeatureView {
30+
// User-specified specifications of this feature view.
31+
RequestFeatureViewSpec spec = 1;
32+
}
33+
34+
message RequestFeatureViewSpec {
35+
// Name of the feature view. Must be unique. Not updated.
36+
string name = 1;
37+
38+
// Name of Feast project that this feature view belongs to.
39+
string project = 2;
40+
41+
// Request data which contains the underlying data schema and list of associated features
42+
DataSource request_data_source = 3;
43+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Copyright 2021 The Feast Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import warnings
15+
from abc import ABC, abstractmethod
16+
from typing import List, Type
17+
18+
from google.protobuf.json_format import MessageToJson
19+
from proto import Message
20+
21+
from feast.feature import Feature
22+
from feast.feature_view_projection import FeatureViewProjection
23+
24+
warnings.simplefilter("once", DeprecationWarning)
25+
26+
27+
class BaseFeatureView(ABC):
28+
"""A FeatureView defines a logical grouping of features to be served."""
29+
30+
@abstractmethod
31+
def __init__(self, name: str, features: List[Feature]):
32+
self._name = name
33+
self._features = features
34+
self._projection = FeatureViewProjection.from_definition(self)
35+
36+
@property
37+
def name(self) -> str:
38+
return self._name
39+
40+
@property
41+
def features(self) -> List[Feature]:
42+
return self._features
43+
44+
@features.setter
45+
def features(self, value):
46+
self._features = value
47+
48+
@property
49+
def projection(self) -> FeatureViewProjection:
50+
return self._projection
51+
52+
@projection.setter
53+
def projection(self, value):
54+
self._projection = value
55+
56+
@property
57+
@abstractmethod
58+
def proto_class(self) -> Type[Message]:
59+
pass
60+
61+
@abstractmethod
62+
def to_proto(self) -> Message:
63+
pass
64+
65+
@classmethod
66+
@abstractmethod
67+
def from_proto(cls, feature_view_proto):
68+
pass
69+
70+
@abstractmethod
71+
def __copy__(self):
72+
"""
73+
Generates a deep copy of this feature view
74+
75+
Returns:
76+
A copy of this FeatureView
77+
"""
78+
pass
79+
80+
def __repr__(self):
81+
items = (f"{k} = {v}" for k, v in self.__dict__.items())
82+
return f"<{self.__class__.__name__}({', '.join(items)})>"
83+
84+
def __str__(self):
85+
return str(MessageToJson(self.to_proto()))
86+
87+
def __hash__(self):
88+
return hash((id(self), self.name))
89+
90+
def __getitem__(self, item):
91+
assert isinstance(item, list)
92+
93+
referenced_features = []
94+
for feature in self.features:
95+
if feature.name in item:
96+
referenced_features.append(feature)
97+
98+
cp = self.__copy__()
99+
cp.projection.features = referenced_features
100+
101+
return cp
102+
103+
def __eq__(self, other):
104+
if not isinstance(other, BaseFeatureView):
105+
raise TypeError(
106+
"Comparisons should only involve BaseFeatureView class objects."
107+
)
108+
109+
if self.name != other.name:
110+
return False
111+
112+
if sorted(self.features) != sorted(other.features):
113+
return False
114+
115+
return True
116+
117+
def ensure_valid(self):
118+
"""
119+
Validates the state of this feature view locally.
120+
121+
Raises:
122+
ValueError: The feature view is invalid.
123+
"""
124+
if not self.name:
125+
raise ValueError("Feature view needs a name.")
126+
127+
def with_name(self, name: str):
128+
"""
129+
Renames this feature view by returning a copy of this feature view with an alias
130+
set for the feature view name. This rename operation is only used as part of query
131+
operations and will not modify the underlying FeatureView.
132+
133+
Args:
134+
name: Name to assign to the FeatureView copy.
135+
136+
Returns:
137+
A copy of this FeatureView with the name replaced with the 'name' input.
138+
"""
139+
cp = self.__copy__()
140+
cp.projection.name_alias = name
141+
142+
return cp
143+
144+
def set_projection(self, feature_view_projection: FeatureViewProjection) -> None:
145+
"""
146+
Setter for the projection object held by this FeatureView. A projection is an
147+
object that stores the modifications to a FeatureView that is applied to the FeatureView
148+
when the FeatureView is used such as during feature_store.get_historical_features.
149+
This method also performs checks to ensure the projection is consistent with this
150+
FeatureView before doing the set.
151+
152+
Args:
153+
feature_view_projection: The FeatureViewProjection object to set this FeatureView's
154+
'projection' field to.
155+
"""
156+
if feature_view_projection.name != self.name:
157+
raise ValueError(
158+
f"The projection for the {self.name} FeatureView cannot be applied because it differs in name. "
159+
f"The projection is named {feature_view_projection.name} and the name indicates which "
160+
"FeatureView the projection is for."
161+
)
162+
163+
for feature in feature_view_projection.features:
164+
if feature not in self.features:
165+
raise ValueError(
166+
f"The projection for {self.name} cannot be applied because it contains {feature.name} which the "
167+
"FeatureView doesn't have."
168+
)
169+
170+
self.projection = feature_view_projection
171+
172+
def with_projection(self, feature_view_projection: FeatureViewProjection):
173+
"""
174+
Sets the feature view projection by returning a copy of this on-demand feature view
175+
with its projection set to the given projection. A projection is an
176+
object that stores the modifications to a feature view that is used during
177+
query operations.
178+
179+
Args:
180+
feature_view_projection: The FeatureViewProjection object to link to this
181+
OnDemandFeatureView.
182+
183+
Returns:
184+
A copy of this OnDemandFeatureView with its projection replaced with the
185+
'feature_view_projection' argument.
186+
"""
187+
if feature_view_projection.name != self.name:
188+
raise ValueError(
189+
f"The projection for the {self.name} FeatureView cannot be applied because it differs in name. "
190+
f"The projection is named {feature_view_projection.name} and the name indicates which "
191+
"FeatureView the projection is for."
192+
)
193+
194+
for feature in feature_view_projection.features:
195+
if feature not in self.features:
196+
raise ValueError(
197+
f"The projection for {self.name} cannot be applied because it contains {feature.name} which the "
198+
"FeatureView doesn't have."
199+
)
200+
201+
cp = self.__copy__()
202+
cp.projection = feature_view_projection
203+
204+
return cp

sdk/python/feast/cli.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from feast import flags, flags_helper, utils
2626
from feast.errors import FeastObjectNotFoundException, FeastProviderLoginError
2727
from feast.feature_store import FeatureStore
28+
from feast.feature_view import FeatureView
29+
from feast.on_demand_feature_view import OnDemandFeatureView
2830
from feast.repo_config import load_repo_config
2931
from feast.repo_operations import (
3032
apply_total,
@@ -261,12 +263,29 @@ def feature_view_list(ctx: click.Context):
261263
cli_check_repo(repo)
262264
store = FeatureStore(repo_path=str(repo))
263265
table = []
264-
for feature_view in store.list_feature_views():
265-
table.append([feature_view.name, feature_view.entities])
266+
for feature_view in [
267+
*store.list_feature_views(),
268+
*store.list_request_feature_views(),
269+
*store.list_on_demand_feature_views(),
270+
]:
271+
entities = set()
272+
if isinstance(feature_view, FeatureView):
273+
entities.update(feature_view.entities)
274+
elif isinstance(feature_view, OnDemandFeatureView):
275+
for backing_fv in feature_view.inputs.values():
276+
if isinstance(backing_fv, FeatureView):
277+
entities.update(backing_fv.entities)
278+
table.append(
279+
[
280+
feature_view.name,
281+
entities if len(entities) > 0 else "n/a",
282+
type(feature_view).__name__,
283+
]
284+
)
266285

267286
from tabulate import tabulate
268287

269-
print(tabulate(table, headers=["NAME", "ENTITIES"], tablefmt="plain"))
288+
print(tabulate(table, headers=["NAME", "ENTITIES", "TYPE"], tablefmt="plain"))
270289

271290

272291
@cli.group(name="on-demand-feature-views")

sdk/python/feast/errors.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ def __init__(self, name, project=None):
5353
class RequestDataNotFoundInEntityDfException(FeastObjectNotFoundException):
5454
def __init__(self, feature_name, feature_view_name):
5555
super().__init__(
56-
f"Feature {feature_name} not found in the entity dataframe, but required by on demand feature view {feature_view_name}"
56+
f"Feature {feature_name} not found in the entity dataframe, but required by feature view {feature_view_name}"
5757
)
5858

5959

6060
class RequestDataNotFoundInEntityRowsException(FeastObjectNotFoundException):
6161
def __init__(self, feature_names):
6262
super().__init__(
63-
f"Required request data source features {feature_names} not found in the entity rows, but required by on demand feature views"
63+
f"Required request data source features {feature_names} not found in the entity rows, but required by feature views"
6464
)
6565

6666

@@ -263,9 +263,10 @@ def __init__(self, entity_type: type):
263263

264264

265265
class ConflictingFeatureViewNames(Exception):
266+
# TODO: print file location of conflicting feature views
266267
def __init__(self, feature_view_name: str):
267268
super().__init__(
268-
f"The feature view name: {feature_view_name} refers to both an on-demand feature view and a feature view"
269+
f"The feature view name: {feature_view_name} refers to feature views of different types."
269270
)
270271

271272

sdk/python/feast/feature_service.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from google.protobuf.json_format import MessageToJson
55

6+
from feast.base_feature_view import BaseFeatureView
67
from feast.feature_table import FeatureTable
78
from feast.feature_view import FeatureView
89
from feast.feature_view_projection import FeatureViewProjection
@@ -59,9 +60,7 @@ def __init__(
5960
self.feature_view_projections.append(
6061
FeatureViewProjection.from_definition(feature_grouping)
6162
)
62-
elif isinstance(feature_grouping, FeatureView) or isinstance(
63-
feature_grouping, OnDemandFeatureView
64-
):
63+
elif isinstance(feature_grouping, BaseFeatureView):
6564
self.feature_view_projections.append(feature_grouping.projection)
6665
else:
6766
raise ValueError(

0 commit comments

Comments
 (0)