Skip to content

Commit

Permalink
Allows registering of features in request data as RequestFeatureView.…
Browse files Browse the repository at this point in the history
… Refactors common logic into a BaseFeatureView class (#1931)

* Initial implementation of Request Feature View without validation, with some refactoring of feature views

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Refactor constructor in base class

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Refactor to have constructor init in base class

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Moving copy methods into base class

Signed-off-by: Danny Chiao <danny@tecton.ai>

* lint

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Implement request feature view validation in historical and online retrieval

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Add tests for request_fv

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Update CLI to understand request feature views

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Fix error to be more generic and apply to all feature views

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Add type of feature view to feature view list command

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Add type of feature view to feature view list command

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Lint imports

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Fix new lines and nits

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Fix repo apply bug

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Fix comments

Signed-off-by: Danny Chiao <danny@tecton.ai>

* format

Signed-off-by: Danny Chiao <danny@tecton.ai>

* fix test

Signed-off-by: Danny Chiao <danny@tecton.ai>

* reverse naming

Signed-off-by: Danny Chiao <danny@tecton.ai>

* reverse naming

Signed-off-by: Danny Chiao <danny@tecton.ai>

* reverse naming

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Add back to cli

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Comments

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Lint

Signed-off-by: Danny Chiao <danny@tecton.ai>

* Remove extra data in response

Signed-off-by: Danny Chiao <danny@tecton.ai>

* revert change

Signed-off-by: Danny Chiao <danny@tecton.ai>

* revert change

Signed-off-by: Danny Chiao <danny@tecton.ai>
  • Loading branch information
adchia authored Oct 15, 2021
1 parent cbfc72a commit f5f5500
Show file tree
Hide file tree
Showing 18 changed files with 799 additions and 284 deletions.
2 changes: 2 additions & 0 deletions protos/feast/core/Registry.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ import "feast/core/FeatureService.proto";
import "feast/core/FeatureTable.proto";
import "feast/core/FeatureView.proto";
import "feast/core/OnDemandFeatureView.proto";
import "feast/core/RequestFeatureView.proto";
import "google/protobuf/timestamp.proto";

message Registry {
repeated Entity entities = 1;
repeated FeatureTable feature_tables = 2;
repeated FeatureView feature_views = 6;
repeated OnDemandFeatureView on_demand_feature_views = 8;
repeated RequestFeatureView request_feature_views = 9;
repeated FeatureService feature_services = 7;

string registry_schema_version = 3; // to support migrations; incremented when schema is changed
Expand Down
43 changes: 43 additions & 0 deletions protos/feast/core/RequestFeatureView.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// Copyright 2021 The Feast Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//


syntax = "proto3";
package feast.core;

option go_package = "github.com/feast-dev/feast/sdk/go/protos/feast/core";
option java_outer_classname = "RequestFeatureViewProto";
option java_package = "feast.proto.core";

import "feast/core/FeatureView.proto";
import "feast/core/Feature.proto";
import "feast/core/DataSource.proto";

message RequestFeatureView {
// User-specified specifications of this feature view.
RequestFeatureViewSpec spec = 1;
}

message RequestFeatureViewSpec {
// Name of the feature view. Must be unique. Not updated.
string name = 1;

// Name of Feast project that this feature view belongs to.
string project = 2;

// Request data which contains the underlying data schema and list of associated features
DataSource request_data_source = 3;
}
204 changes: 204 additions & 0 deletions sdk/python/feast/base_feature_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Copyright 2021 The Feast Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import warnings
from abc import ABC, abstractmethod
from typing import List, Type

from google.protobuf.json_format import MessageToJson
from proto import Message

from feast.feature import Feature
from feast.feature_view_projection import FeatureViewProjection

warnings.simplefilter("once", DeprecationWarning)


class BaseFeatureView(ABC):
"""A FeatureView defines a logical grouping of features to be served."""

@abstractmethod
def __init__(self, name: str, features: List[Feature]):
self._name = name
self._features = features
self._projection = FeatureViewProjection.from_definition(self)

@property
def name(self) -> str:
return self._name

@property
def features(self) -> List[Feature]:
return self._features

@features.setter
def features(self, value):
self._features = value

@property
def projection(self) -> FeatureViewProjection:
return self._projection

@projection.setter
def projection(self, value):
self._projection = value

@property
@abstractmethod
def proto_class(self) -> Type[Message]:
pass

@abstractmethod
def to_proto(self) -> Message:
pass

@classmethod
@abstractmethod
def from_proto(cls, feature_view_proto):
pass

@abstractmethod
def __copy__(self):
"""
Generates a deep copy of this feature view
Returns:
A copy of this FeatureView
"""
pass

def __repr__(self):
items = (f"{k} = {v}" for k, v in self.__dict__.items())
return f"<{self.__class__.__name__}({', '.join(items)})>"

def __str__(self):
return str(MessageToJson(self.to_proto()))

def __hash__(self):
return hash((id(self), self.name))

def __getitem__(self, item):
assert isinstance(item, list)

referenced_features = []
for feature in self.features:
if feature.name in item:
referenced_features.append(feature)

cp = self.__copy__()
cp.projection.features = referenced_features

return cp

def __eq__(self, other):
if not isinstance(other, BaseFeatureView):
raise TypeError(
"Comparisons should only involve BaseFeatureView class objects."
)

if self.name != other.name:
return False

if sorted(self.features) != sorted(other.features):
return False

return True

def ensure_valid(self):
"""
Validates the state of this feature view locally.
Raises:
ValueError: The feature view is invalid.
"""
if not self.name:
raise ValueError("Feature view needs a name.")

def with_name(self, name: str):
"""
Renames this feature view by returning a copy of this feature view with an alias
set for the feature view name. This rename operation is only used as part of query
operations and will not modify the underlying FeatureView.
Args:
name: Name to assign to the FeatureView copy.
Returns:
A copy of this FeatureView with the name replaced with the 'name' input.
"""
cp = self.__copy__()
cp.projection.name_alias = name

return cp

def set_projection(self, feature_view_projection: FeatureViewProjection) -> None:
"""
Setter for the projection object held by this FeatureView. A projection is an
object that stores the modifications to a FeatureView that is applied to the FeatureView
when the FeatureView is used such as during feature_store.get_historical_features.
This method also performs checks to ensure the projection is consistent with this
FeatureView before doing the set.
Args:
feature_view_projection: The FeatureViewProjection object to set this FeatureView's
'projection' field to.
"""
if feature_view_projection.name != self.name:
raise ValueError(
f"The projection for the {self.name} FeatureView cannot be applied because it differs in name. "
f"The projection is named {feature_view_projection.name} and the name indicates which "
"FeatureView the projection is for."
)

for feature in feature_view_projection.features:
if feature not in self.features:
raise ValueError(
f"The projection for {self.name} cannot be applied because it contains {feature.name} which the "
"FeatureView doesn't have."
)

self.projection = feature_view_projection

def with_projection(self, feature_view_projection: FeatureViewProjection):
"""
Sets the feature view projection by returning a copy of this on-demand feature view
with its projection set to the given projection. A projection is an
object that stores the modifications to a feature view that is used during
query operations.
Args:
feature_view_projection: The FeatureViewProjection object to link to this
OnDemandFeatureView.
Returns:
A copy of this OnDemandFeatureView with its projection replaced with the
'feature_view_projection' argument.
"""
if feature_view_projection.name != self.name:
raise ValueError(
f"The projection for the {self.name} FeatureView cannot be applied because it differs in name. "
f"The projection is named {feature_view_projection.name} and the name indicates which "
"FeatureView the projection is for."
)

for feature in feature_view_projection.features:
if feature not in self.features:
raise ValueError(
f"The projection for {self.name} cannot be applied because it contains {feature.name} which the "
"FeatureView doesn't have."
)

cp = self.__copy__()
cp.projection = feature_view_projection

return cp
25 changes: 22 additions & 3 deletions sdk/python/feast/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from feast import flags, flags_helper, utils
from feast.errors import FeastObjectNotFoundException, FeastProviderLoginError
from feast.feature_store import FeatureStore
from feast.feature_view import FeatureView
from feast.on_demand_feature_view import OnDemandFeatureView
from feast.repo_config import load_repo_config
from feast.repo_operations import (
apply_total,
Expand Down Expand Up @@ -261,12 +263,29 @@ def feature_view_list(ctx: click.Context):
cli_check_repo(repo)
store = FeatureStore(repo_path=str(repo))
table = []
for feature_view in store.list_feature_views():
table.append([feature_view.name, feature_view.entities])
for feature_view in [
*store.list_feature_views(),
*store.list_request_feature_views(),
*store.list_on_demand_feature_views(),
]:
entities = set()
if isinstance(feature_view, FeatureView):
entities.update(feature_view.entities)
elif isinstance(feature_view, OnDemandFeatureView):
for backing_fv in feature_view.inputs.values():
if isinstance(backing_fv, FeatureView):
entities.update(backing_fv.entities)
table.append(
[
feature_view.name,
entities if len(entities) > 0 else "n/a",
type(feature_view).__name__,
]
)

from tabulate import tabulate

print(tabulate(table, headers=["NAME", "ENTITIES"], tablefmt="plain"))
print(tabulate(table, headers=["NAME", "ENTITIES", "TYPE"], tablefmt="plain"))


@cli.group(name="on-demand-feature-views")
Expand Down
7 changes: 4 additions & 3 deletions sdk/python/feast/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ def __init__(self, name, project=None):
class RequestDataNotFoundInEntityDfException(FeastObjectNotFoundException):
def __init__(self, feature_name, feature_view_name):
super().__init__(
f"Feature {feature_name} not found in the entity dataframe, but required by on demand feature view {feature_view_name}"
f"Feature {feature_name} not found in the entity dataframe, but required by feature view {feature_view_name}"
)


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


Expand Down Expand Up @@ -263,9 +263,10 @@ def __init__(self, entity_type: type):


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


Expand Down
5 changes: 2 additions & 3 deletions sdk/python/feast/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from google.protobuf.json_format import MessageToJson

from feast.base_feature_view import BaseFeatureView
from feast.feature_table import FeatureTable
from feast.feature_view import FeatureView
from feast.feature_view_projection import FeatureViewProjection
Expand Down Expand Up @@ -59,9 +60,7 @@ def __init__(
self.feature_view_projections.append(
FeatureViewProjection.from_definition(feature_grouping)
)
elif isinstance(feature_grouping, FeatureView) or isinstance(
feature_grouping, OnDemandFeatureView
):
elif isinstance(feature_grouping, BaseFeatureView):
self.feature_view_projections.append(feature_grouping.projection)
else:
raise ValueError(
Expand Down
Loading

0 comments on commit f5f5500

Please sign in to comment.