Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allows registering of features in request data as RequestFeatureView. Refactors common logic into a BaseFeatureView class #1931

Merged
merged 26 commits into from
Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
adchia marked this conversation as resolved.
Show resolved Hide resolved
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):
adchia marked this conversation as resolved.
Show resolved Hide resolved
"""
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."
adchia marked this conversation as resolved.
Show resolved Hide resolved
)


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