@@ -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 = [
@@ -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