forked from feast-dev/feast
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfeature_service.py
More file actions
433 lines (380 loc) · 16.8 KB
/
Copy pathfeature_service.py
File metadata and controls
433 lines (380 loc) · 16.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
from datetime import datetime
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from google.protobuf.json_format import MessageToJson
from typeguard import typechecked
from feast.base_feature_view import BaseFeatureView
from feast.errors import (
FeastObjectNotFoundException,
FeatureViewMissingDuringFeatureServiceInference,
)
from feast.feature_logging import LoggingConfig
from feast.feature_view import FeatureView
from feast.feature_view_projection import FeatureViewProjection
from feast.field import Field
from feast.labeling.label_view import LabelView
from feast.on_demand_feature_view import OnDemandFeatureView
from feast.protos.feast.core.FeatureService_pb2 import (
FeatureService as FeatureServiceProto,
)
from feast.protos.feast.core.FeatureService_pb2 import (
FeatureServiceMeta as FeatureServiceMetaProto,
)
from feast.protos.feast.core.FeatureService_pb2 import (
FeatureServiceSpec as FeatureServiceSpecProto,
)
if TYPE_CHECKING:
from feast.infra.registry.base_registry import BaseRegistry
@typechecked
class FeatureService:
"""
A feature service defines a logical group of features from one or more feature views.
This group of features can be retrieved together during training or serving.
Attributes:
name: The unique name of the feature service.
feature_view_projections: A list containing feature views and feature view
projections, representing the features in the feature service.
description: A human-readable description.
tags: A dictionary of key-value pairs to store arbitrary metadata.
owner: The owner of the feature service, typically the email of the primary
maintainer.
created_timestamp: The time when the feature service was created.
last_updated_timestamp: The time when the feature service was last updated.
"""
name: str
_features: List[Union[FeatureView, OnDemandFeatureView, LabelView]]
feature_view_projections: List[FeatureViewProjection]
description: str
tags: Dict[str, str]
owner: str
created_timestamp: Optional[datetime] = None
last_updated_timestamp: Optional[datetime] = None
logging_config: Optional[LoggingConfig] = None
precompute_online: bool = False
def __init__(
self,
*,
name: str,
features: List[Union[FeatureView, OnDemandFeatureView, LabelView]],
tags: Optional[Dict[str, str]] = None,
description: str = "",
owner: str = "",
logging_config: Optional[LoggingConfig] = None,
precompute_online: bool = False,
):
"""
Creates a FeatureService object.
Args:
name: The unique name of the feature service.
features: A list containing feature views and feature view
projections, representing the features in the feature service.
description (optional): A human-readable description.
tags (optional): A dictionary of key-value pairs to store arbitrary metadata.
owner (optional): The owner of the feature view, typically the email of the
primary maintainer.
precompute_online (optional): When True, a pre-computed feature vector is
maintained per entity for single-read online retrieval.
"""
self.name = name
self._features = features
self.feature_view_projections = []
self.description = description
self.tags = tags or {}
self.owner = owner
self.created_timestamp = None
self.last_updated_timestamp = None
self.logging_config = logging_config
self.precompute_online = precompute_online
for feature_grouping in self._features:
if isinstance(feature_grouping, BaseFeatureView):
self.feature_view_projections.append(feature_grouping.projection)
def infer_features(
self, fvs_to_update: Dict[str, Union[FeatureView, BaseFeatureView]]
):
"""
Infers the features for the projections of this feature service, and updates this feature
service in place.
This method is necessary since feature services may rely on feature views which require
feature inference.
Args:
fvs_to_update: A mapping of feature view names to corresponding feature views that
contains all the feature views necessary to run inference.
"""
for feature_grouping in self._features:
if isinstance(feature_grouping, BaseFeatureView):
projection = feature_grouping.projection
if projection.desired_features:
# The projection wants to select a specific set of inferred features.
# Example: FeatureService(features=[fv[["inferred_feature"]]]), where
# 'fv' is a feature view that was defined without a schema.
if feature_grouping.name in fvs_to_update:
# First we validate that the selected features have actually been inferred.
desired_features = set(projection.desired_features)
actual_features = set(
[
f.name
for f in fvs_to_update[feature_grouping.name].features
]
)
assert desired_features.issubset(actual_features)
# Then we extract the selected features and add them to the projection.
projection.features = []
for f in fvs_to_update[feature_grouping.name].features:
if f.name in desired_features:
projection.features.append(f)
else:
raise FeatureViewMissingDuringFeatureServiceInference(
feature_view_name=feature_grouping.name,
feature_service_name=self.name,
)
continue
if projection.features:
# The projection has already selected features from a feature view with a
# known schema, so no action needs to be taken.
# Example: FeatureService(features=[fv[["existing_feature"]]]), where
# 'existing_feature' was defined as part of the schema of 'fv'.
# Example: FeatureService(features=[fv]), where 'fv' was defined with a schema.
continue
# The projection wants to select all possible inferred features.
# Example: FeatureService(features=[fv]), where 'fv' is a feature view that
# was defined without a schema.
if feature_grouping.name in fvs_to_update:
projection.features = fvs_to_update[feature_grouping.name].features
else:
raise FeatureViewMissingDuringFeatureServiceInference(
feature_view_name=feature_grouping.name,
feature_service_name=self.name,
)
else:
raise ValueError(
f"The feature service {self.name} has been provided with an invalid type "
f'{type(feature_grouping)} as part of the "features" argument.)'
)
def prepare_for_apply(
self,
registry: "BaseRegistry",
project: str,
allow_cache: bool = False,
) -> "FeatureService":
"""
Materialize feature view projections before registry apply.
Uses the same FeatureService construction and ``infer_features`` path as
``FeatureStore.apply`` for SDK-defined services.
When the service is already fully resolved (SDK path where _features is
set and projections already have features populated via infer_features,
OR the proto deserialization path where projections carry full dtype
info), this is a no-op.
"""
from feast.types import Invalid
if self._features and all(p.features for p in self.feature_view_projections):
return self
if (
not self._features
and self.feature_view_projections
and all(
p.features and all(f.dtype != Invalid for f in p.features)
for p in self.feature_view_projections
)
):
return self
fvs_to_update: Dict[str, Union[FeatureView, BaseFeatureView]] = {}
if self._features:
for feature_grouping in self._features:
if isinstance(feature_grouping, BaseFeatureView):
fvs_to_update[feature_grouping.name] = (
registry.get_any_feature_view(
feature_grouping.name, project, allow_cache=allow_cache
)
)
self.infer_features(fvs_to_update=fvs_to_update)
return self
resolved_features: List[Union[FeatureView, OnDemandFeatureView, LabelView]] = []
for projection in self.feature_view_projections:
try:
feature_view = registry.get_any_feature_view(
projection.name, project, allow_cache=allow_cache
)
except FeastObjectNotFoundException as exc:
raise FeastObjectNotFoundException(
f"Feature view '{projection.name}' not found in project '{project}'"
) from exc
if not isinstance(
feature_view, (FeatureView, OnDemandFeatureView, LabelView)
):
raise ValueError(
f"Cannot resolve projection for feature view '{projection.name}'"
)
fvs_to_update[feature_view.name] = feature_view
features_by_name = {
feature.name: feature for feature in feature_view.features
}
if self._projection_matches_registry_features(projection, features_by_name):
resolved_features.append(feature_view.with_projection(projection))
elif projection.desired_features:
resolved_features.append(
feature_view[list(projection.desired_features)]
)
elif not projection.features:
resolved_features.append(feature_view)
else:
resolved_features.append(
feature_view[[feature.name for feature in projection.features]]
)
prepared = FeatureService(
name=self.name,
features=resolved_features,
tags=self.tags,
description=self.description,
owner=self.owner,
logging_config=self.logging_config,
precompute_online=self.precompute_online,
)
prepared.created_timestamp = self.created_timestamp
prepared.last_updated_timestamp = self.last_updated_timestamp
prepared.infer_features(fvs_to_update=fvs_to_update)
self._features = prepared._features
self.feature_view_projections = prepared.feature_view_projections
return self
@staticmethod
def _projection_matches_registry_features(
projection: FeatureViewProjection,
features_by_name: Dict[str, Field],
) -> bool:
if not projection.features:
return False
for feature in projection.features:
if feature.name not in features_by_name:
return False
if feature != features_by_name[feature.name]:
return False
return True
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(self.name)
def __eq__(self, other):
if not isinstance(other, FeatureService):
return False
if (
self.name != other.name
or self.description != other.description
or self.tags != other.tags
or self.owner != other.owner
or self.precompute_online != other.precompute_online
):
return False
if sorted(self.feature_view_projections) != sorted(
other.feature_view_projections
):
return False
return True
@classmethod
def from_proto(cls, feature_service_proto: FeatureServiceProto):
"""
Converts a FeatureServiceProto to a FeatureService object.
Args:
feature_service_proto: A protobuf representation of a FeatureService.
"""
fs = cls(
name=feature_service_proto.spec.name,
features=[],
tags=dict(feature_service_proto.spec.tags),
description=feature_service_proto.spec.description,
owner=feature_service_proto.spec.owner,
logging_config=LoggingConfig.from_proto(
feature_service_proto.spec.logging_config
),
precompute_online=feature_service_proto.spec.precompute_online,
)
fs.feature_view_projections.extend(
[
FeatureViewProjection.from_proto(projection)
for projection in feature_service_proto.spec.features
]
)
if feature_service_proto.meta.HasField("created_timestamp"):
fs.created_timestamp = (
feature_service_proto.meta.created_timestamp.ToDatetime()
)
if feature_service_proto.meta.HasField("last_updated_timestamp"):
fs.last_updated_timestamp = (
feature_service_proto.meta.last_updated_timestamp.ToDatetime()
)
return fs
def to_proto(self) -> FeatureServiceProto:
"""
Converts a feature service to its protobuf representation.
Returns:
A FeatureServiceProto protobuf.
"""
meta = FeatureServiceMetaProto()
if self.created_timestamp:
meta.created_timestamp.FromDatetime(self.created_timestamp)
if self.last_updated_timestamp:
meta.last_updated_timestamp.FromDatetime(self.last_updated_timestamp)
spec = FeatureServiceSpecProto(
name=self.name,
features=[
projection.to_proto() for projection in self.feature_view_projections
],
tags=self.tags,
description=self.description,
owner=self.owner,
logging_config=self.logging_config.to_proto()
if self.logging_config
else None,
precompute_online=self.precompute_online,
)
return FeatureServiceProto(spec=spec, meta=meta)
@classmethod
def build_apply_request(
cls,
*,
name: str,
project: str,
feature_view_refs: List[tuple[str, Optional[List[str]]]],
description: str = "",
tags: Optional[Dict[str, str]] = None,
owner: str = "",
commit: bool = True,
):
"""Build an unresolved ApplyFeatureServiceRequest from feature view refs."""
from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2
from feast.protos.feast.core.FeatureViewProjection_pb2 import (
FeatureViewProjection as FeatureViewProjectionProto,
)
from feast.protos.feast.registry import RegistryServer_pb2
projections = []
for feature_view_name, feature_names in feature_view_refs:
projection = FeatureViewProjectionProto(
feature_view_name=feature_view_name,
)
if feature_names:
for feature_name in feature_names:
projection.feature_columns.append(FeatureSpecV2(name=feature_name))
projections.append(projection)
spec = FeatureServiceSpecProto(
name=name,
features=projections,
tags=tags or {},
description=description,
owner=owner,
)
return RegistryServer_pb2.ApplyFeatureServiceRequest(
feature_service=FeatureServiceProto(spec=spec),
project=project,
commit=commit,
)
def validate(self):
if not self.precompute_online:
return
for fv in self._features:
if isinstance(fv, OnDemandFeatureView) and not fv.write_to_online_store:
raise ValueError(
f"FeatureService '{self.name}' has precompute_online=True but "
f"contains OnDemandFeatureView '{fv.name}' with "
f"write_to_online_store=False. On-demand transforms computed at "
f"serve time cannot be pre-computed."
)