Skip to content

Commit 8ff71c0

Browse files
committed
API, CLI changes for historical features retrieval without entity_df, FAQ update
Signed-off-by: jyejare <jyejare@redhat.com>
1 parent e092160 commit 8ff71c0

6 files changed

Lines changed: 203 additions & 117 deletions

File tree

docs/getting-started/faq.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,16 @@ Yes, this is possible. For example, you can use BigQuery as an offline store and
3939

4040
### How do I run `get_historical_features` without providing an entity dataframe?
4141

42-
Feast does not provide a way to do this right now. This is an area we're actively interested in contributions for. See [GitHub issue](https://github.com/feast-dev/feast/issues/1611)
42+
Feast does supports fetching historical features without passing an entity dataframe with the request.
43+
- As of today, only `postgres offline feature store` is supported for entity dataframe less retrieval. Remaining offline stores would be gradually updated to support the entity df less retrieval. The stores would be selected based on priorities and user base/request.
44+
- The retrieval is based on `start_date` and `end_date` parameters to the function. Here are some combinations supported.
45+
- Both params are given, Returns data during the given start to end timerange.
46+
- Only start_date param is given, Returns data from the start date to `now` time.
47+
- Only end_date param is given, Returns data during the end_date minus TTL time in feature view.
48+
- Both params are `not` given, Returns data during the TTL time in feature view to now time.
49+
- When multiple features are requested from multiple feature-views it is required to have entity ids in both of them for `JOIN` so that
50+
51+
This is an area we're actively interested in contributions for. See [GitHub issue](https://github.com/feast-dev/feast/issues/1611)
4352

4453
### Does Feast provide security or access control?
4554

sdk/python/feast/cli/features.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from datetime import datetime
23
from typing import List
34

45
import click
@@ -140,37 +141,69 @@ def get_online_features(ctx: click.Context, entities: List[str], features: List[
140141
"--dataframe",
141142
"-d",
142143
type=str,
143-
required=True,
144144
help='JSON string containing entities and timestamps. Example: \'[{"event_timestamp": "2025-03-29T12:00:00", "driver_id": 1001}]\'',
145145
)
146146
@click.option(
147147
"--features",
148148
"-f",
149149
multiple=True,
150-
required=True,
151150
help="Features to retrieve. feature-view:feature-name ex: driver_hourly_stats:conv_rate",
152151
)
152+
@click.option(
153+
"--start-date",
154+
"-s",
155+
type=str,
156+
help="Start date for historical feature retrieval. Format: YYYY-MM-DD HH:MM:SS",
157+
)
158+
@click.option(
159+
"--end-date",
160+
"-e",
161+
type=str,
162+
help="End date for historical feature retrieval. Format: YYYY-MM-DD HH:MM:SS",
163+
)
153164
@click.pass_context
154-
def get_historical_features(ctx: click.Context, dataframe: str, features: List[str]):
165+
def get_historical_features(
166+
ctx: click.Context,
167+
dataframe: str,
168+
features: List[str],
169+
start_date: str,
170+
end_date: str,
171+
):
155172
"""
156173
Fetch historical feature values for a given entity ID
157174
"""
158175
store = create_feature_store(ctx)
159-
try:
160-
entity_list = json.loads(dataframe)
161-
if not isinstance(entity_list, list):
162-
raise ValueError("Entities must be a list of dictionaries.")
163-
164-
entity_df = pd.DataFrame(entity_list)
165-
entity_df["event_timestamp"] = pd.to_datetime(entity_df["event_timestamp"])
176+
if not dataframe and not start_date and not end_date:
177+
click.echo(
178+
"Either --dataframe or --start-date and/or --end-date must be provided."
179+
)
180+
return
166181

167-
except Exception as e:
168-
click.echo(f"Error parsing entities JSON: {e}", err=True)
182+
if dataframe and (start_date or end_date):
183+
click.echo("Cannot specify both --dataframe and --start-date/--end-date.")
169184
return
170185

186+
entity_df = None
187+
if dataframe:
188+
try:
189+
entity_list = json.loads(dataframe)
190+
if not isinstance(entity_list, list):
191+
raise ValueError("Entities must be a list of dictionaries.")
192+
193+
entity_df = pd.DataFrame(entity_list)
194+
entity_df["event_timestamp"] = pd.to_datetime(entity_df["event_timestamp"])
195+
196+
except Exception as e:
197+
click.echo(f"Error parsing entities JSON: {e}", err=True)
198+
return
199+
171200
feature_vector = store.get_historical_features(
172201
entity_df=entity_df,
173202
features=list(features),
203+
start_date=datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
204+
if start_date
205+
else None,
206+
end_date=datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S") if end_date else None,
174207
).to_df()
175208

176209
click.echo(feature_vector.to_json(orient="records", indent=4))

sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from feast.repo_config import RepoConfig
4747
from feast.saved_dataset import SavedDatasetStorage
4848
from feast.type_map import pg_type_code_to_arrow
49-
from feast.utils import make_tzaware
49+
from feast.utils import _utc_now, make_tzaware
5050

5151
from .postgres_source import PostgreSQLSource
5252

@@ -136,7 +136,7 @@ def get_historical_features(
136136
if entity_df is None:
137137
# Default to current time if end_date not provided
138138
if end_date is None:
139-
end_date = datetime.now(tz=timezone.utc)
139+
end_date = _utc_now()
140140
else:
141141
end_date = make_tzaware(end_date)
142142

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def __init__(
116116
client: FeastFlightClient,
117117
api: str,
118118
api_parameters: Dict[str, Any],
119-
entity_df: Union[pd.DataFrame, str] = None,
119+
entity_df: Optional[Union[pd.DataFrame, str]] = None,
120120
table: pa.Table = None,
121121
metadata: Optional[RetrievalMetadata] = None,
122122
):
@@ -193,7 +193,7 @@ def get_historical_features(
193193
config: RepoConfig,
194194
feature_views: List[FeatureView],
195195
feature_refs: List[str],
196-
entity_df: Union[pd.DataFrame, str],
196+
entity_df: Optional[Union[pd.DataFrame, str]],
197197
registry: BaseRegistry,
198198
project: str,
199199
full_feature_names: bool = False,
@@ -482,8 +482,8 @@ def _get_entity_df_event_timestamp_range(
482482
def _send_retrieve_remote(
483483
api: str,
484484
api_parameters: Dict[str, Any],
485-
entity_df: Union[pd.DataFrame, str],
486-
table: pa.Table,
485+
entity_df: Optional[Union[pd.DataFrame, str]],
486+
table: Optional[pa.Table],
487487
client: FeastFlightClient,
488488
):
489489
command_descriptor = _call_put(
@@ -510,7 +510,7 @@ def _call_put(
510510
api: str,
511511
api_parameters: Dict[str, Any],
512512
client: FeastFlightClient,
513-
entity_df: Union[pd.DataFrame, str],
513+
entity_df: Optional[Union[pd.DataFrame, str]],
514514
table: pa.Table,
515515
):
516516
# Generate unique command identifier
@@ -535,7 +535,7 @@ def _call_put(
535535

536536
def _put_parameters(
537537
command_descriptor: fl.FlightDescriptor,
538-
entity_df: Union[pd.DataFrame, str],
538+
entity_df: Optional[Union[pd.DataFrame, str]],
539539
table: pa.Table,
540540
client: FeastFlightClient,
541541
):

sdk/python/feast/offline_server.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66
import traceback
77
from datetime import datetime
8-
from typing import Any, Dict, List, cast
8+
from typing import Any, Dict, List, Optional, cast
99

1010
import click
1111
import pyarrow as pa
@@ -413,20 +413,24 @@ def list_actions(self, context):
413413
),
414414
]
415415

416-
def _validate_get_historical_features_parameters(self, command: dict, key: str):
417-
assert key in self.flights, f"missing key={key}"
416+
def _validate_get_historical_features_parameters(
417+
self, command: dict, key: Optional[str] = None
418+
):
419+
if key:
420+
assert key in self.flights, f"missing key={key}"
418421
assert "feature_view_names" in command, "feature_view_names is mandatory"
419422
assert "name_aliases" in command, "name_aliases is mandatory"
420423
assert "feature_refs" in command, "feature_refs is mandatory"
421424
assert "project" in command, "project is mandatory"
422425
assert "full_feature_names" in command, "full_feature_names is mandatory"
423426

424-
def get_historical_features(self, command: dict, key: str):
427+
def get_historical_features(self, command: dict, key: Optional[str] = None):
425428
self._validate_get_historical_features_parameters(command, key)
426-
427-
# Extract parameters from the internal flights dictionary
428-
entity_df_value = self.flights[key]
429-
entity_df = pa.Table.to_pandas(entity_df_value)
429+
entity_df = None
430+
if key:
431+
# Extract parameters from the internal flights dictionary
432+
entity_df_value = self.flights[key]
433+
entity_df = pa.Table.to_pandas(entity_df_value)
430434

431435
feature_view_names = command["feature_view_names"]
432436
name_aliases = command["name_aliases"]

0 commit comments

Comments
 (0)