Skip to content

Commit 9454d7c

Browse files
feat: Feature/adding remote online store (#4226)
1 parent 28a3d24 commit 9454d7c

9 files changed

Lines changed: 433 additions & 10 deletions

File tree

‎docs/SUMMARY.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
* [Datastore](reference/online-stores/datastore.md)
9797
* [DynamoDB](reference/online-stores/dynamodb.md)
9898
* [Bigtable](reference/online-stores/bigtable.md)
99+
* [Remote](reference/online-stores/remote.md)
99100
* [PostgreSQL (contrib)](reference/online-stores/postgres.md)
100101
* [Cassandra + Astra DB (contrib)](reference/online-stores/cassandra.md)
101102
* [MySQL (contrib)](reference/online-stores/mysql.md)

‎docs/reference/online-stores/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ Please see [Online Store](../../getting-started/architecture-and-components/onli
6161
{% content-ref url="scylladb.md" %}
6262
[scylladb.md](scylladb.md)
6363
{% endcontent-ref %}
64+
65+
{% content-ref url="remote.md" %}
66+
[remote.md](remote.md)
67+
{% endcontent-ref %}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Remote online store
2+
3+
## Description
4+
5+
This remote online store will let you interact with remote feature server. At this moment this only supports the read operation. You can use this online store and able retrieve online features `store.get_online_features` from remote feature server.
6+
7+
## Examples
8+
9+
The registry is pointing to registry of remote feature store. If it is not accessible then should be configured to use remote registry.
10+
11+
{% code title="feature_store.yaml" %}
12+
```yaml
13+
project: my-local-project
14+
registry: /remote/data/registry.db
15+
provider: local
16+
online_store:
17+
path: http://localhost:6566
18+
type: remote
19+
entity_key_serialization_version: 2
20+
```
21+
{% endcode %}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Copyright 2021 The Feast Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import json
15+
import logging
16+
from datetime import datetime
17+
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple
18+
19+
import requests
20+
from pydantic import StrictStr
21+
22+
from feast import Entity, FeatureView, RepoConfig
23+
from feast.infra.online_stores.online_store import OnlineStore
24+
from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto
25+
from feast.protos.feast.types.Value_pb2 import Value as ValueProto
26+
from feast.repo_config import FeastConfigBaseModel
27+
from feast.type_map import python_values_to_proto_values
28+
from feast.value_type import ValueType
29+
30+
logger = logging.getLogger(__name__)
31+
32+
33+
class RemoteOnlineStoreConfig(FeastConfigBaseModel):
34+
"""Remote Online store config for remote online store"""
35+
36+
type: Literal["remote"] = "remote"
37+
"""Online store type selector"""
38+
39+
path: StrictStr = "http://localhost:6566"
40+
""" str: Path to metadata store.
41+
If type is 'remote', then this is a URL for registry server """
42+
43+
44+
class RemoteOnlineStore(OnlineStore):
45+
"""
46+
remote online store implementation wrapper to communicate with feast online server.
47+
"""
48+
49+
def online_write_batch(
50+
self,
51+
config: RepoConfig,
52+
table: FeatureView,
53+
data: List[
54+
Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]]
55+
],
56+
progress: Optional[Callable[[int], Any]],
57+
) -> None:
58+
raise NotImplementedError
59+
60+
def online_read(
61+
self,
62+
config: RepoConfig,
63+
table: FeatureView,
64+
entity_keys: List[EntityKeyProto],
65+
requested_features: Optional[List[str]] = None,
66+
) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]:
67+
assert isinstance(config.online_store, RemoteOnlineStoreConfig)
68+
config.online_store.__class__ = RemoteOnlineStoreConfig
69+
70+
req_body = self._construct_online_read_api_json_request(
71+
entity_keys, table, requested_features
72+
)
73+
response = requests.post(
74+
f"{config.online_store.path}/get-online-features", data=req_body
75+
)
76+
if response.status_code == 200:
77+
logger.debug("Able to retrieve the online features from feature server.")
78+
response_json = json.loads(response.text)
79+
event_ts = self._get_event_ts(response_json)
80+
# Iterating over results and converting the API results in column format to row format.
81+
result_tuples: List[
82+
Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]
83+
] = []
84+
for feature_value_index in range(len(entity_keys)):
85+
feature_values_dict: Dict[str, ValueProto] = dict()
86+
for index, feature_name in enumerate(
87+
response_json["metadata"]["feature_names"]
88+
):
89+
if (
90+
requested_features is not None
91+
and feature_name in requested_features
92+
):
93+
if (
94+
response_json["results"][index]["statuses"][
95+
feature_value_index
96+
]
97+
== "PRESENT"
98+
):
99+
message = python_values_to_proto_values(
100+
[
101+
response_json["results"][index]["values"][
102+
feature_value_index
103+
]
104+
],
105+
ValueType.UNKNOWN,
106+
)
107+
feature_values_dict[feature_name] = message[0]
108+
else:
109+
feature_values_dict[feature_name] = ValueProto()
110+
result_tuples.append((event_ts, feature_values_dict))
111+
return result_tuples
112+
else:
113+
error_msg = f"Unable to retrieve the online store data using feature server API. Error_code={response.status_code}, error_message={response.reason}"
114+
logger.error(error_msg)
115+
raise RuntimeError(error_msg)
116+
117+
def _construct_online_read_api_json_request(
118+
self,
119+
entity_keys: List[EntityKeyProto],
120+
table: FeatureView,
121+
requested_features: Optional[List[str]] = None,
122+
) -> str:
123+
api_requested_features = []
124+
if requested_features is not None:
125+
for requested_feature in requested_features:
126+
api_requested_features.append(f"{table.name}:{requested_feature}")
127+
128+
entity_values = []
129+
entity_key = ""
130+
for row in entity_keys:
131+
entity_key = row.join_keys[0]
132+
entity_values.append(
133+
getattr(row.entity_values[0], row.entity_values[0].WhichOneof("val"))
134+
)
135+
136+
req_body = json.dumps(
137+
{
138+
"features": api_requested_features,
139+
"entities": {entity_key: entity_values},
140+
}
141+
)
142+
return req_body
143+
144+
def _get_event_ts(self, response_json) -> datetime:
145+
event_ts = ""
146+
if len(response_json["results"]) > 1:
147+
event_ts = response_json["results"][1]["event_timestamps"][0]
148+
return datetime.fromisoformat(event_ts.replace("Z", "+00:00"))
149+
150+
def update(
151+
self,
152+
config: RepoConfig,
153+
tables_to_delete: Sequence[FeatureView],
154+
tables_to_keep: Sequence[FeatureView],
155+
entities_to_delete: Sequence[Entity],
156+
entities_to_keep: Sequence[Entity],
157+
partial: bool,
158+
):
159+
pass
160+
161+
def teardown(
162+
self,
163+
config: RepoConfig,
164+
tables: Sequence[FeatureView],
165+
entities: Sequence[Entity],
166+
):
167+
pass

‎sdk/python/feast/repo_config.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"hazelcast": "feast.infra.online_stores.contrib.hazelcast_online_store.hazelcast_online_store.HazelcastOnlineStore",
6565
"ikv": "feast.infra.online_stores.contrib.ikv_online_store.ikv.IKVOnlineStore",
6666
"elasticsearch": "feast.infra.online_stores.contrib.elasticsearch.ElasticSearchOnlineStore",
67+
"remote": "feast.infra.online_stores.remote.RemoteOnlineStore",
6768
}
6869

6970
OFFLINE_STORE_CLASS_FOR_TYPE = {

‎sdk/python/tests/conftest.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
create_basic_driver_dataset,
3333
create_document_dataset,
3434
)
35-
from tests.integration.feature_repos.integration_test_repo_config import ( # noqa: E402
36-
IntegrationTestRepoConfig,
35+
from tests.integration.feature_repos.integration_test_repo_config import (
36+
IntegrationTestRepoConfig, # noqa: E402
3737
)
3838
from tests.integration.feature_repos.repo_configuration import ( # noqa: E402
3939
AVAILABLE_OFFLINE_STORES,
@@ -45,8 +45,8 @@
4545
construct_universal_feature_views,
4646
construct_universal_test_data,
4747
)
48-
from tests.integration.feature_repos.universal.data_sources.file import ( # noqa: E402
49-
FileDataSourceCreator,
48+
from tests.integration.feature_repos.universal.data_sources.file import (
49+
FileDataSourceCreator, # noqa: E402
5050
)
5151
from tests.integration.feature_repos.universal.entities import ( # noqa: E402
5252
customer,
@@ -173,7 +173,7 @@ def simple_dataset_2() -> pd.DataFrame:
173173

174174
def start_test_local_server(repo_path: str, port: int):
175175
fs = FeatureStore(repo_path)
176-
fs.serve("localhost", port, no_access_log=True)
176+
fs.serve(host="localhost", port=port)
177177

178178

179179
@pytest.fixture

‎sdk/python/tests/integration/feature_repos/repo_configuration.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,7 @@
136136

137137
AVAILABLE_ONLINE_STORES: Dict[
138138
str, Tuple[Union[str, Dict[Any, Any]], Optional[Type[OnlineStoreCreator]]]
139-
] = {
140-
"sqlite": ({"type": "sqlite"}, None),
141-
}
139+
] = {"sqlite": ({"type": "sqlite"}, None)}
142140

143141
# Only configure Cloud DWH if running full integration tests
144142
if os.getenv("FEAST_IS_LOCAL_TEST", "False") != "True":
@@ -155,7 +153,6 @@
155153
AVAILABLE_ONLINE_STORES["datastore"] = ("datastore", None)
156154
AVAILABLE_ONLINE_STORES["snowflake"] = (SNOWFLAKE_CONFIG, None)
157155
AVAILABLE_ONLINE_STORES["bigtable"] = (BIGTABLE_CONFIG, None)
158-
159156
# Uncomment to test using private Rockset account. Currently not enabled as
160157
# there is no dedicated Rockset instance for CI testing and there is no
161158
# containerized version of Rockset.
@@ -489,7 +486,6 @@ def construct_test_environment(
489486
"arn:aws:iam::402087665549:role/lambda_execution_role",
490487
),
491488
)
492-
493489
else:
494490
feature_server = LocalFeatureServerConfig(
495491
feature_logging=FeatureLoggingConfig(enabled=True)

0 commit comments

Comments
 (0)