Skip to content

Commit 66fbb51

Browse files
authored
Added .with_name method in FeatureView/OnDemandFeatureView classes for name aliasing. FeatureViewProjection will hold this information (feast-dev#1872)
* Added .with_name method in FV with test case Signed-off-by: David Y Liu <[email protected]> * Correctly sort imports Signed-off-by: David Y Liu <[email protected]> * correct lint errors Signed-off-by: David Y Liu <[email protected]> * Modified FeatureNameCollisionError msg Signed-off-by: David Y Liu <[email protected]> * fixed error msg test Signed-off-by: David Y Liu <[email protected]> * updated code to preserve original FV name in base_name field Signed-off-by: David Y Liu <[email protected]> * Updated proto conversion methods and fixed failures Signed-off-by: David Y Liu <[email protected]> * fixed proto numberings Signed-off-by: David Y Liu <[email protected]> * wip Signed-off-by: David Y Liu <[email protected]> * Cleaned code up Signed-off-by: David Y Liu <[email protected]> * added copy method in FVProjections Signed-off-by: David Y Liu <[email protected]> * use python's copy lib rather than creating new copy method. Signed-off-by: David Y Liu <[email protected]>
1 parent a5b8c26 commit 66fbb51

File tree

9 files changed

+95
-39
lines changed

9 files changed

+95
-39
lines changed

protos/feast/core/FeatureViewProjection.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ message FeatureViewProjection {
1414
// The feature view name
1515
string feature_view_name = 1;
1616

17+
// Alias for feature view name
18+
string feature_view_name_to_use = 3;
19+
1720
// The features of the feature view that are a part of the feature reference.
1821
repeated FeatureSpecV2 feature_columns = 2;
1922
}

sdk/python/feast/errors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,9 @@ def __init__(self, feature_refs_collisions: List[str], full_feature_names: bool)
146146
if full_feature_names:
147147
collisions = [ref.replace(":", "__") for ref in feature_refs_collisions]
148148
error_message = (
149-
"To resolve this collision, please ensure that the features in question "
150-
"have different names."
149+
"To resolve this collision, please ensure that the feature views or their own features "
150+
"have different names. If you're intentionally joining the same feature view twice on "
151+
"different sets of entities, please rename one of the feature views with '.with_name'."
151152
)
152153
else:
153154
collisions = [ref.split(":")[1] for ref in feature_refs_collisions]

sdk/python/feast/feature_store.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ def _get_features(
329329
)
330330
for projection in feature_service_from_registry.feature_view_projections:
331331
_feature_refs.extend(
332-
[f"{projection.name}:{f.name}" for f in projection.features]
332+
[f"{projection.name_to_use}:{f.name}" for f in projection.features]
333333
)
334334
else:
335335
assert isinstance(_features, list)
@@ -905,7 +905,11 @@ def get_online_features(
905905
GetOnlineFeaturesResponse(field_values=result_rows)
906906
)
907907
return self._augment_response_with_on_demand_transforms(
908-
_feature_refs, full_feature_names, initial_response, result_rows
908+
_feature_refs,
909+
all_on_demand_feature_views,
910+
full_feature_names,
911+
initial_response,
912+
result_rows,
909913
)
910914

911915
def _populate_result_rows_from_feature_view(
@@ -935,7 +939,7 @@ def _populate_result_rows_from_feature_view(
935939
if feature_data is None:
936940
for feature_name in requested_features:
937941
feature_ref = (
938-
f"{table.name}__{feature_name}"
942+
f"{table.projection.name_to_use}__{feature_name}"
939943
if full_feature_names
940944
else feature_name
941945
)
@@ -945,7 +949,7 @@ def _populate_result_rows_from_feature_view(
945949
else:
946950
for feature_name in feature_data:
947951
feature_ref = (
948-
f"{table.name}__{feature_name}"
952+
f"{table.projection.name_to_use}__{feature_name}"
949953
if full_feature_names
950954
else feature_name
951955
)
@@ -972,16 +976,12 @@ def _get_needed_request_data_features(self, grouped_odfv_refs) -> Set[str]:
972976
def _augment_response_with_on_demand_transforms(
973977
self,
974978
feature_refs: List[str],
979+
odfvs: List[OnDemandFeatureView],
975980
full_feature_names: bool,
976981
initial_response: OnlineResponse,
977982
result_rows: List[GetOnlineFeaturesResponse.FieldValues],
978983
) -> OnlineResponse:
979-
all_on_demand_feature_views = {
980-
view.name: view
981-
for view in self._registry.list_on_demand_feature_views(
982-
project=self.project, allow_cache=True
983-
)
984-
}
984+
all_on_demand_feature_views = {view.name: view for view in odfvs}
985985
all_odfv_feature_names = all_on_demand_feature_views.keys()
986986

987987
if len(all_on_demand_feature_views) == 0:
@@ -1009,7 +1009,7 @@ def _augment_response_with_on_demand_transforms(
10091009

10101010
for transformed_feature in selected_subset:
10111011
transformed_feature_name = (
1012-
f"{odfv.name}__{transformed_feature}"
1012+
f"{odfv.projection.name_to_use}__{transformed_feature}"
10131013
if full_feature_names
10141014
else transformed_feature
10151015
)

sdk/python/feast/feature_view.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import copy
1415
import re
1516
import warnings
1617
from datetime import datetime, timedelta
@@ -204,6 +205,33 @@ def is_valid(self):
204205
if not self.entities:
205206
raise ValueError("Feature view has no entities.")
206207

208+
def with_name(self, name: str):
209+
"""
210+
Produces a copy of this FeatureView with the passed name.
211+
212+
Args:
213+
name: Name to assign to the FeatureView copy.
214+
215+
Returns:
216+
A copy of this FeatureView with the name replaced with the 'name' input.
217+
"""
218+
fv = FeatureView(
219+
name=self.name,
220+
entities=self.entities,
221+
ttl=self.ttl,
222+
input=self.input,
223+
batch_source=self.batch_source,
224+
stream_source=self.stream_source,
225+
features=self.features,
226+
tags=self.tags,
227+
online=self.online,
228+
)
229+
230+
fv.set_projection(copy.copy(self.projection))
231+
fv.projection.name_to_use = name
232+
233+
return fv
234+
207235
def to_proto(self) -> FeatureViewProto:
208236
"""
209237
Converts a feature view object to its protobuf representation.

sdk/python/feast/feature_view_projection.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
@dataclass
1212
class FeatureViewProjection:
1313
name: str
14+
name_to_use: str
1415
features: List[Feature]
1516

1617
def to_proto(self):
1718
feature_reference_proto = FeatureViewProjectionProto(
18-
feature_view_name=self.name
19+
feature_view_name=self.name, feature_view_name_to_use=self.name_to_use
1920
)
2021
for feature in self.features:
2122
feature_reference_proto.feature_columns.append(feature.to_proto())
@@ -24,7 +25,11 @@ def to_proto(self):
2425

2526
@staticmethod
2627
def from_proto(proto: FeatureViewProjectionProto):
27-
ref = FeatureViewProjection(name=proto.feature_view_name, features=[])
28+
ref = FeatureViewProjection(
29+
name=proto.feature_view_name,
30+
name_to_use=proto.feature_view_name_to_use,
31+
features=[],
32+
)
2833
for feature_column in proto.feature_columns:
2934
ref.features.append(Feature.from_proto(feature_column))
3035

@@ -33,5 +38,7 @@ def from_proto(proto: FeatureViewProjectionProto):
3338
@staticmethod
3439
def from_definition(feature_grouping):
3540
return FeatureViewProjection(
36-
name=feature_grouping.name, features=feature_grouping.features
41+
name=feature_grouping.name,
42+
name_to_use=feature_grouping.name,
43+
features=feature_grouping.features,
3744
)

sdk/python/feast/infra/offline_stores/offline_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def get_feature_view_query_context(
128128
created_timestamp_column = feature_view.input.created_timestamp_column
129129

130130
context = FeatureViewQueryContext(
131-
name=feature_view.name,
131+
name=feature_view.projection.name_to_use,
132132
ttl=ttl_seconds,
133133
entities=join_keys,
134134
features=features,

sdk/python/feast/infra/provider.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,18 +186,14 @@ def _get_requested_feature_views_to_features_dict(
186186
feature_from_ref = ref_parts[1]
187187

188188
found = False
189-
for feature_view_from_registry in feature_views:
190-
if feature_view_from_registry.name == feature_view_from_ref:
189+
for fv in feature_views:
190+
if fv.projection.name_to_use == feature_view_from_ref:
191191
found = True
192-
feature_views_to_feature_map[feature_view_from_registry].append(
193-
feature_from_ref
194-
)
195-
for odfv_from_registry in on_demand_feature_views:
196-
if odfv_from_registry.name == feature_view_from_ref:
192+
feature_views_to_feature_map[fv].append(feature_from_ref)
193+
for odfv in on_demand_feature_views:
194+
if odfv.projection.name_to_use == feature_view_from_ref:
197195
found = True
198-
on_demand_feature_views_to_feature_map[odfv_from_registry].append(
199-
feature_from_ref
200-
)
196+
on_demand_feature_views_to_feature_map[odfv].append(feature_from_ref)
201197

202198
if not found:
203199
raise ValueError(f"Could not find feature view from reference {ref}")

sdk/python/feast/on_demand_feature_view.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
import functools
23
from types import MethodType
34
from typing import Dict, List, Union, cast
@@ -68,6 +69,25 @@ def __init__(
6869
def __hash__(self) -> int:
6970
return hash((id(self), self.name))
7071

72+
def with_name(self, name: str):
73+
"""
74+
Produces a copy of this OnDemandFeatureView with the passed name.
75+
76+
Args:
77+
name: Name to assign to the OnDemandFeatureView copy.
78+
79+
Returns:
80+
A copy of this OnDemandFeatureView with the name replaced with the 'name' input.
81+
"""
82+
odfv = OnDemandFeatureView(
83+
name=self.name, features=self.features, inputs=self.inputs, udf=self.udf
84+
)
85+
86+
odfv.set_projection(copy.copy(self.projection))
87+
odfv.projection.name_to_use = name
88+
89+
return odfv
90+
7191
def to_proto(self) -> OnDemandFeatureViewProto:
7292
"""
7393
Converts an on demand feature view object to its protobuf representation.

sdk/python/tests/unit/test_feature_validation.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ def test_feature_name_collision_on_historical_retrieval():
2020
full_feature_names=False,
2121
)
2222

23-
expected_error_message = (
24-
"Duplicate features named avg_daily_trips found.\n"
25-
"To resolve this collision, either use the full feature name by setting "
26-
"'full_feature_names=True', or ensure that the features in question have different names."
27-
)
23+
expected_error_message = (
24+
"Duplicate features named avg_daily_trips found.\n"
25+
"To resolve this collision, either use the full feature name by setting "
26+
"'full_feature_names=True', or ensure that the features in question have different names."
27+
)
2828

29-
assert str(error.value) == expected_error_message
29+
assert str(error.value) == expected_error_message
3030

3131
# check when feature names collide and 'full_feature_names=True'
3232
with pytest.raises(FeatureNameCollisionError) as error:
@@ -43,9 +43,10 @@ def test_feature_name_collision_on_historical_retrieval():
4343
full_feature_names=True,
4444
)
4545

46-
expected_error_message = (
47-
"Duplicate features named driver_stats__avg_daily_trips found.\n"
48-
"To resolve this collision, please ensure that the features in question "
49-
"have different names."
50-
)
51-
assert str(error.value) == expected_error_message
46+
expected_error_message = (
47+
"Duplicate features named driver_stats__avg_daily_trips found.\n"
48+
"To resolve this collision, please ensure that the feature views or their own features "
49+
"have different names. If you're intentionally joining the same feature view twice on "
50+
"different sets of entities, please rename one of the feature views with '.with_name'."
51+
)
52+
assert str(error.value) == expected_error_message

0 commit comments

Comments
 (0)