@@ -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-
17695class 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