Skip to content

Commit ba032d6

Browse files
feat: read from offline path in get_historical_features for BFVs
1 parent b428d7f commit ba032d6

1 file changed

Lines changed: 70 additions & 2 deletions

File tree

  • sdk/python/feast/infra/offline_stores/contrib/spark_offline_store

sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,72 @@ class SparkFeatureViewQueryContext(offline_utils.FeatureViewQueryContext):
7979
max_date_partition: str
8080

8181

82+
def _apply_bfv_transformations_for_historical(
83+
spark_session: SparkSession,
84+
feature_views: List[FeatureView],
85+
query_context: List[offline_utils.FeatureViewQueryContext],
86+
) -> List[offline_utils.FeatureViewQueryContext]:
87+
"""
88+
For BatchFeatureViews, redirect get_historical_features to read from the
89+
pre-materialized offline store (batch_source.path) when available, avoiding
90+
expensive UDF re-execution on raw data.
91+
92+
Precedence:
93+
1. offline=True + batch_source.path set -> read pre-computed parquet
94+
2. Python/pandas UDF present -> execute UDF on raw source (fallback)
95+
3. Otherwise -> pass through unchanged
96+
"""
97+
from dataclasses import replace
98+
99+
fv_by_name = {fv.projection.name_to_use(): fv for fv in feature_views}
100+
new_contexts = []
101+
102+
for ctx in query_context:
103+
fv = fv_by_name.get(ctx.name)
104+
if fv is None or not isinstance(fv, BatchFeatureView):
105+
new_contexts.append(ctx)
106+
continue
107+
108+
if (
109+
getattr(fv, "offline", False)
110+
and isinstance(fv.batch_source, SparkSource)
111+
and fv.batch_source.path
112+
):
113+
tmp_view = f"__feast_offline_{ctx.name}_{uuid.uuid4().hex[:8]}"
114+
file_format = fv.batch_source.file_format or "parquet"
115+
df = spark_session.read.format(file_format).load(fv.batch_source.path)
116+
df.createOrReplaceTempView(tmp_view)
117+
ctx = replace(ctx, table_subquery=tmp_view)
118+
elif (
119+
hasattr(fv, "feature_transformation")
120+
and fv.feature_transformation is not None
121+
and (
122+
getattr(fv.feature_transformation, "mode", None)
123+
in ("python", "pandas")
124+
or getattr(
125+
getattr(fv.feature_transformation, "mode", None), "value", None
126+
)
127+
in ("python", "pandas")
128+
)
129+
):
130+
udf = getattr(fv.feature_transformation, "udf", None) or getattr(
131+
fv, "udf", None
132+
)
133+
if udf is not None:
134+
temp_view_name = f"__feast_bfv_{ctx.name}_{uuid.uuid4().hex[:8]}"
135+
spark_session.conf.set("spark.sql.runSQLOnFiles", "true")
136+
raw_df = spark_session.sql(
137+
f"SELECT * FROM {ctx.table_subquery}"
138+
)
139+
transformed_df = udf(raw_df)
140+
transformed_df.createOrReplaceTempView(temp_view_name)
141+
ctx = replace(ctx, table_subquery=temp_view_name)
142+
143+
new_contexts.append(ctx)
144+
145+
return new_contexts
146+
147+
82148
class SparkOfflineStore(OfflineStore):
83149
@staticmethod
84150
def pull_latest_from_table_or_query(
@@ -261,8 +327,10 @@ def get_historical_features(
261327
entity_df_event_timestamp_range,
262328
)
263329

264-
query_context = _apply_bfv_transformations(
265-
spark_session, feature_views, query_context
330+
query_context = _apply_bfv_transformations_for_historical(
331+
spark_session=spark_session,
332+
feature_views=feature_views,
333+
query_context=query_context,
266334
)
267335

268336
spark_query_context = [

0 commit comments

Comments
 (0)