Skip to content

Commit 8f187dd

Browse files
committed
fix: RemoteOnlineStore sends all features in a single HTTP request
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent 9b088fe commit 8f187dd

2 files changed

Lines changed: 415 additions & 1 deletion

File tree

sdk/python/feast/infra/online_stores/remote.py

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,36 @@
1616
import uuid as uuid_module
1717
from collections import defaultdict
1818
from datetime import datetime
19-
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple
19+
from typing import (
20+
Any,
21+
Callable,
22+
Dict,
23+
List,
24+
Literal,
25+
Mapping,
26+
Optional,
27+
Sequence,
28+
Tuple,
29+
Union,
30+
)
2031

2132
import requests
2233
from pydantic import StrictStr
2334

2435
from feast import Entity, FeatureView, RepoConfig
36+
from feast.feature_service import FeatureService
2537
from feast.infra.online_stores.helpers import _to_naive_utc
2638
from feast.infra.online_stores.online_store import OnlineStore
39+
from feast.infra.registry.base_registry import BaseRegistry
40+
from feast.online_response import OnlineResponse
2741
from feast.permissions.client.http_auth_requests_wrapper import HttpSessionManager
42+
from feast.protos.feast.serving.ServingService_pb2 import (
43+
FieldStatus,
44+
GetOnlineFeaturesResponse,
45+
GetOnlineFeaturesResponseMetadata,
46+
)
2847
from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto
48+
from feast.protos.feast.types.Value_pb2 import RepeatedValue
2949
from feast.protos.feast.types.Value_pb2 import Value as ValueProto
3050
from feast.repo_config import FeastConfigBaseModel
3151
from feast.rest_error_handler import rest_error_handling_decorator
@@ -132,6 +152,88 @@ def _proto_value_to_transport_value(proto_value: ValueProto) -> Any:
132152

133153
return feast_value_type_to_python_type(proto_value)
134154

155+
_STATUS_MAP = {
156+
"PRESENT": FieldStatus.PRESENT,
157+
"NOT_FOUND": FieldStatus.NOT_FOUND,
158+
"NULL_VALUE": FieldStatus.NULL_VALUE,
159+
"OUTSIDE_MAX_AGE": FieldStatus.OUTSIDE_MAX_AGE,
160+
}
161+
162+
def get_online_features(
163+
self,
164+
config: RepoConfig,
165+
features: Union[List[str], FeatureService],
166+
entity_rows: Union[
167+
List[Dict[str, Any]],
168+
Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]],
169+
],
170+
registry: BaseRegistry,
171+
project: str,
172+
full_feature_names: bool = False,
173+
include_feature_view_version_metadata: bool = False,
174+
) -> OnlineResponse:
175+
assert isinstance(config.online_store, RemoteOnlineStoreConfig)
176+
177+
if isinstance(entity_rows, list):
178+
columnar: Dict[str, List[Any]] = {k: [] for k in entity_rows[0].keys()}
179+
for entity_row in entity_rows:
180+
for key, value in entity_row.items():
181+
columnar[key].append(value)
182+
entity_rows = columnar
183+
184+
entities: Dict[str, List[Any]] = {}
185+
for k, vals in entity_rows.items():
186+
iterable = vals.val if isinstance(vals, RepeatedValue) else vals
187+
entities[k] = [_json_safe(v) for v in iterable]
188+
189+
req_body: Dict[str, Any] = {
190+
"entities": entities,
191+
"full_feature_names": full_feature_names,
192+
"include_feature_view_version_metadata": include_feature_view_version_metadata,
193+
}
194+
195+
if isinstance(features, FeatureService):
196+
req_body["feature_service"] = features.name
197+
else:
198+
req_body["features"] = features
199+
200+
response = get_remote_online_features(config=config, req_body=req_body)
201+
202+
if response.status_code != 200:
203+
raise RuntimeError(
204+
f"Failed to get online features: {response.status_code} {response.text}"
205+
)
206+
207+
resp_json = response.json()
208+
return self._build_online_response_from_json(resp_json)
209+
210+
def _build_online_response_from_json(
211+
self, resp_json: Dict[str, Any]
212+
) -> OnlineResponse:
213+
proto = GetOnlineFeaturesResponse()
214+
215+
metadata = GetOnlineFeaturesResponseMetadata()
216+
feature_names = resp_json.get("metadata", {}).get("feature_names", [])
217+
metadata.feature_names.val.extend(feature_names)
218+
proto.metadata.CopyFrom(metadata)
219+
220+
for result in resp_json.get("results", []):
221+
fv = GetOnlineFeaturesResponse.FeatureVector()
222+
for val in result.get("values", []):
223+
if val is None:
224+
fv.values.append(ValueProto())
225+
else:
226+
protos = python_values_to_proto_values([val])
227+
fv.values.append(protos[0])
228+
for status_str in result.get("statuses", []):
229+
fv.statuses.append(
230+
self._STATUS_MAP.get(status_str, FieldStatus.INVALID)
231+
)
232+
proto.results.append(fv)
233+
234+
proto.status = True
235+
return OnlineResponse(proto)
236+
135237
def online_write_batch(
136238
self,
137239
config: RepoConfig,

0 commit comments

Comments
 (0)