Skip to content

Commit 16798e4

Browse files
abhijeet-dhumalntkathole
authored andcommitted
refactor: fold pre-computed path into _apply_bfv_transformations per review
Signed-off-by: abhijeet-dhumal <abhijeetdhumal652@gmail.com>
1 parent de321f8 commit 16798e4

1 file changed

Lines changed: 45 additions & 92 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: 45 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -92,87 +92,6 @@ class SparkFeatureViewQueryContext(offline_utils.FeatureViewQueryContext):
9292
max_date_partition: Optional[str]
9393

9494

95-
def _apply_bfv_transformations_for_historical(
96-
spark_session: SparkSession,
97-
feature_views: List[FeatureView],
98-
query_context: List[offline_utils.FeatureViewQueryContext],
99-
) -> List[offline_utils.FeatureViewQueryContext]:
100-
"""
101-
For BatchFeatureViews, redirect get_historical_features to read from the
102-
pre-materialized offline store (batch_source.path) when available, avoiding
103-
expensive UDF re-execution on raw data.
104-
105-
Precedence:
106-
1. offline=True + batch_source.path set -> read pre-computed parquet
107-
2. Python/pandas UDF present -> execute UDF on raw source (fallback)
108-
3. Otherwise -> pass through unchanged
109-
"""
110-
from dataclasses import replace
111-
112-
fv_by_name = {fv.projection.name_to_use(): fv for fv in feature_views}
113-
new_contexts = []
114-
115-
for ctx in query_context:
116-
fv = fv_by_name.get(ctx.name)
117-
if fv is None or not isinstance(fv, BatchFeatureView):
118-
new_contexts.append(ctx)
119-
continue
120-
121-
if (
122-
getattr(fv, "offline", False)
123-
and isinstance(fv.batch_source, SparkSource)
124-
and fv.batch_source.path
125-
):
126-
tmp_view = f"__feast_offline_{ctx.name}_{uuid.uuid4().hex[:8]}"
127-
file_format = fv.batch_source.file_format or "parquet"
128-
try:
129-
df = spark_session.read.format(file_format).load(fv.batch_source.path)
130-
df.createOrReplaceTempView(tmp_view)
131-
ctx = replace(ctx, table_subquery=tmp_view)
132-
new_contexts.append(ctx)
133-
continue
134-
except (FileNotFoundError, PermissionError) as e:
135-
warnings.warn(
136-
f"Offline path '{fv.batch_source.path}' not accessible for "
137-
f"'{ctx.name}': {e}; falling back to source query.",
138-
RuntimeWarning,
139-
stacklevel=2,
140-
)
141-
except Exception as e:
142-
warnings.warn(
143-
f"Unexpected error loading offline path '{fv.batch_source.path}' "
144-
f"for '{ctx.name}': {e}; falling back to source query.",
145-
RuntimeWarning,
146-
stacklevel=2,
147-
)
148-
149-
if (
150-
hasattr(fv, "feature_transformation")
151-
and fv.feature_transformation is not None
152-
and (
153-
getattr(fv.feature_transformation, "mode", None) in ("python", "pandas")
154-
or getattr(
155-
getattr(fv.feature_transformation, "mode", None), "value", None
156-
)
157-
in ("python", "pandas")
158-
)
159-
):
160-
udf = getattr(fv.feature_transformation, "udf", None) or getattr(
161-
fv, "udf", None
162-
)
163-
if udf is not None:
164-
temp_view_name = f"__feast_bfv_{ctx.name}_{uuid.uuid4().hex[:8]}"
165-
spark_session.conf.set("spark.sql.runSQLOnFiles", "true")
166-
raw_df = spark_session.sql(f"SELECT * FROM {ctx.table_subquery}")
167-
transformed_df = udf(raw_df)
168-
transformed_df.createOrReplaceTempView(temp_view_name)
169-
ctx = replace(ctx, table_subquery=temp_view_name)
170-
171-
new_contexts.append(ctx)
172-
173-
return new_contexts
174-
175-
17695
class SparkOfflineStore(OfflineStore):
17796
@staticmethod
17897
def pull_latest_from_table_or_query(
@@ -379,10 +298,10 @@ def get_historical_features(
379298
entity_df_event_timestamp_range,
380299
)
381300

382-
query_context = _apply_bfv_transformations_for_historical(
301+
query_context = _apply_bfv_transformations(
383302
spark_session=spark_session,
384303
feature_views=feature_views,
385-
query_context=query_context,
304+
query_contexts=query_context,
386305
)
387306

388307
spark_query_context = [
@@ -1489,9 +1408,16 @@ def _apply_bfv_transformations(
14891408
query_contexts: List[offline_utils.FeatureViewQueryContext],
14901409
) -> List[offline_utils.FeatureViewQueryContext]:
14911410
"""
1492-
For BatchFeatureViews with a UDF, read the raw source into a Spark DataFrame,
1493-
invoke the transformation, register the result as a temp view, and replace the
1494-
table_subquery in the query context so the PIT join reads transformed data.
1411+
For BatchFeatureViews, update each query context in one of two ways:
1412+
1413+
1. Pre-computed path shortcut: if ``offline=True`` and
1414+
``batch_source.path`` is set, read the pre-materialized parquet
1415+
directly — avoids re-running the UDF on every training call.
1416+
2. UDF execution: if the BFV has a transformation, run it against
1417+
the raw source and register the result as a temp view.
1418+
1419+
Plain FeatureViews and BFVs with neither a path nor a UDF pass
1420+
through unchanged.
14951421
"""
14961422
from dataclasses import replace
14971423

@@ -1506,11 +1432,41 @@ def _apply_bfv_transformations(
15061432
updated_contexts = []
15071433
for ctx in query_contexts:
15081434
fv = fv_by_name.get(ctx.name)
1435+
if fv is None or not isinstance(fv, BatchFeatureView):
1436+
updated_contexts.append(ctx)
1437+
continue
1438+
1439+
# 1. Pre-computed path shortcut
15091440
if (
1510-
fv is not None
1511-
and isinstance(fv, BatchFeatureView)
1512-
and has_transformation(fv)
1441+
getattr(fv, "offline", False)
1442+
and isinstance(fv.batch_source, SparkSource)
1443+
and fv.batch_source.path
15131444
):
1445+
tmp_view = f"__feast_offline_{ctx.name}_{uuid.uuid4().hex[:8]}"
1446+
file_format = fv.batch_source.file_format or "parquet"
1447+
try:
1448+
df = spark_session.read.format(file_format).load(fv.batch_source.path)
1449+
df.createOrReplaceTempView(tmp_view)
1450+
updated_contexts.append(replace(ctx, table_subquery=tmp_view))
1451+
continue
1452+
except (FileNotFoundError, PermissionError) as e:
1453+
warnings.warn(
1454+
f"Offline path '{fv.batch_source.path}' not accessible for "
1455+
f"'{ctx.name}': {e}; falling back to source query.",
1456+
RuntimeWarning,
1457+
stacklevel=2,
1458+
)
1459+
except Exception as e:
1460+
warnings.warn(
1461+
f"Unexpected error loading offline path "
1462+
f"'{fv.batch_source.path}' for '{ctx.name}': {e}; "
1463+
f"falling back to source query.",
1464+
RuntimeWarning,
1465+
stacklevel=2,
1466+
)
1467+
1468+
# 2. UDF execution fallback
1469+
if has_transformation(fv):
15141470
udf = get_transformation_function(fv)
15151471
if udf is not None:
15161472
source_info = resolve_feature_view_source_with_fallback(fv)
@@ -1526,12 +1482,9 @@ def _apply_bfv_transformations(
15261482
source_df = spark_session.sql(
15271483
f"SELECT * FROM {source_query} WHERE {timestamp_filter}"
15281484
)
1529-
15301485
transformed_df = udf(source_df)
1531-
15321486
tmp_view_name = "feast_bfv_" + uuid.uuid4().hex
15331487
transformed_df.createOrReplaceTempView(tmp_view_name)
1534-
15351488
ctx = replace(ctx, table_subquery=tmp_view_name)
15361489

15371490
updated_contexts.append(ctx)

0 commit comments

Comments
 (0)