Skip to content

Commit fce2b7c

Browse files
refactor: fold pre-computed path into _apply_bfv_transformations per review
Signed-off-by: abhijeet-dhumal <abhijeetdhumal652@gmail.com>
1 parent d630d86 commit fce2b7c

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 = [
@@ -1543,9 +1462,16 @@ def _apply_bfv_transformations(
15431462
query_contexts: List[offline_utils.FeatureViewQueryContext],
15441463
) -> List[offline_utils.FeatureViewQueryContext]:
15451464
"""
1546-
For BatchFeatureViews with a UDF, read the raw source into a Spark DataFrame,
1547-
invoke the transformation, register the result as a temp view, and replace the
1548-
table_subquery in the query context so the PIT join reads transformed data.
1465+
For BatchFeatureViews, update each query context in one of two ways:
1466+
1467+
1. Pre-computed path shortcut: if ``offline=True`` and
1468+
``batch_source.path`` is set, read the pre-materialized parquet
1469+
directly — avoids re-running the UDF on every training call.
1470+
2. UDF execution: if the BFV has a transformation, run it against
1471+
the raw source and register the result as a temp view.
1472+
1473+
Plain FeatureViews and BFVs with neither a path nor a UDF pass
1474+
through unchanged.
15491475
"""
15501476
from dataclasses import replace
15511477

@@ -1560,11 +1486,41 @@ def _apply_bfv_transformations(
15601486
updated_contexts = []
15611487
for ctx in query_contexts:
15621488
fv = fv_by_name.get(ctx.name)
1489+
if fv is None or not isinstance(fv, BatchFeatureView):
1490+
updated_contexts.append(ctx)
1491+
continue
1492+
1493+
# 1. Pre-computed path shortcut
15631494
if (
1564-
fv is not None
1565-
and isinstance(fv, BatchFeatureView)
1566-
and has_transformation(fv)
1495+
getattr(fv, "offline", False)
1496+
and isinstance(fv.batch_source, SparkSource)
1497+
and fv.batch_source.path
15671498
):
1499+
tmp_view = f"__feast_offline_{ctx.name}_{uuid.uuid4().hex[:8]}"
1500+
file_format = fv.batch_source.file_format or "parquet"
1501+
try:
1502+
df = spark_session.read.format(file_format).load(fv.batch_source.path)
1503+
df.createOrReplaceTempView(tmp_view)
1504+
updated_contexts.append(replace(ctx, table_subquery=tmp_view))
1505+
continue
1506+
except (FileNotFoundError, PermissionError) as e:
1507+
warnings.warn(
1508+
f"Offline path '{fv.batch_source.path}' not accessible for "
1509+
f"'{ctx.name}': {e}; falling back to source query.",
1510+
RuntimeWarning,
1511+
stacklevel=2,
1512+
)
1513+
except Exception as e:
1514+
warnings.warn(
1515+
f"Unexpected error loading offline path "
1516+
f"'{fv.batch_source.path}' for '{ctx.name}': {e}; "
1517+
f"falling back to source query.",
1518+
RuntimeWarning,
1519+
stacklevel=2,
1520+
)
1521+
1522+
# 2. UDF execution fallback
1523+
if has_transformation(fv):
15681524
udf = get_transformation_function(fv)
15691525
if udf is not None:
15701526
source_info = resolve_feature_view_source_with_fallback(fv)
@@ -1580,12 +1536,9 @@ def _apply_bfv_transformations(
15801536
source_df = spark_session.sql(
15811537
f"SELECT * FROM {source_query} WHERE {timestamp_filter}"
15821538
)
1583-
15841539
transformed_df = udf(source_df)
1585-
15861540
tmp_view_name = "feast_bfv_" + uuid.uuid4().hex
15871541
transformed_df.createOrReplaceTempView(tmp_view_name)
1588-
15891542
ctx = replace(ctx, table_subquery=tmp_view_name)
15901543

15911544
updated_contexts.append(ctx)

0 commit comments

Comments
 (0)