From 760144d97a8f30194b0484032dd3cd13c119c2bb Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Wed, 18 Feb 2026 01:24:36 +0530 Subject: [PATCH 1/2] Fix facet-meta runtime rendering by adding meta aggregation fallback --- .../Feature/Facets/Types/Meta/FacetType.php | 124 ++++++++++++++++++ .../Feature/Facets/Types/Meta/Renderer.php | 7 +- 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/includes/classes/Feature/Facets/Types/Meta/FacetType.php b/includes/classes/Feature/Facets/Types/Meta/FacetType.php index 0b8c690325..6aa4afd0ba 100644 --- a/includes/classes/Feature/Facets/Types/Meta/FacetType.php +++ b/includes/classes/Feature/Facets/Types/Meta/FacetType.php @@ -9,6 +9,7 @@ namespace ElasticPress\Feature\Facets\Types\Meta; use ElasticPress\Features; +use WP_Query; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. @@ -333,6 +334,129 @@ public function get_facets_meta_fields() { return apply_filters( 'ep_facet_meta_fields', $facets_meta_fields ); } + /** + * Get aggregation data for a specific meta field. + * + * If the main query does not have aggregation data for the requested field, + * run a facetable fallback query that requests this field and merge the + * aggregation result back into the original query object. + * + * @since 5.3.3 + * @param WP_Query $query Query object. + * @param string $meta_field Meta field key. + * @return array|false + */ + public function get_facet_aggregation_for_field( WP_Query $query, string $meta_field ) { + $feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_name = $this->get_filter_name() . $meta_field; + + $facet_aggregation = $feature->get_facet_aggregation( $query, $facet_name ); + + if ( false !== $facet_aggregation ) { + return $facet_aggregation; + } + + $attempted_fields = (array) $query->get( 'ep_meta_runtime_agg_fields_attempted', [] ); + + if ( in_array( $meta_field, $attempted_fields, true ) ) { + return false; + } + + $attempted_fields[] = $meta_field; + + $query->set( 'ep_meta_runtime_agg_fields_attempted', array_values( array_unique( $attempted_fields ) ) ); + + $runtime_aggregations = $this->query_runtime_aggregations( $query, [ $meta_field ] ); + + if ( empty( $runtime_aggregations[ $facet_name ] ) || ! is_array( $runtime_aggregations[ $facet_name ] ) ) { + return false; + } + + $current_aggregations = $feature->get_query_aggregations( $query ); + + if ( ! is_array( $current_aggregations ) ) { + $current_aggregations = []; + } + + $feature->set_query_aggregations( $query, array_merge( $current_aggregations, $runtime_aggregations ) ); + + return $runtime_aggregations[ $facet_name ]; + } + + /** + * Run a facetable query and return aggregations for selected meta fields. + * + * @since 5.3.3 + * @param WP_Query $query Query object. + * @param array $meta_fields Meta fields. + * @return array + */ + protected function query_runtime_aggregations( WP_Query $query, array $meta_fields ): array { + $meta_fields = array_values( array_unique( array_filter( $meta_fields ) ) ); + + if ( empty( $meta_fields ) ) { + return []; + } + + $feature = Features::factory()->get_registered_feature( 'facets' ); + + $selected_meta_fields = []; + $selected_filters = (array) $feature->get_selected(); + + if ( ! empty( $selected_filters[ $this->get_filter_type() ] ) && is_array( $selected_filters[ $this->get_filter_type() ] ) ) { + $selected_meta_fields = array_keys( $selected_filters[ $this->get_filter_type() ] ); + } + + $runtime_fields = array_values( array_unique( array_merge( $meta_fields, $selected_meta_fields ) ) ); + + $add_meta_fields = function ( $fields ) use ( $runtime_fields ) { + return array_values( array_unique( array_merge( (array) $fields, $runtime_fields ) ) ); + }; + + add_filter( 'ep_facet_meta_fields', $add_meta_fields ); + + $query_args = $query->query_vars; + unset( $query_args['ep_meta_runtime_agg_fields_attempted'] ); + + $query_args = wp_parse_args( + $query_args, + [ + 'ep_is_facetable' => true, + 'ep_integrate' => true, + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'cache_results' => false, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'update_menu_item_cache' => false, + 'lazy_load_term_meta' => false, + 'ignore_sticky_posts' => true, + 'suppress_filters' => false, + ] + ); + + try { + $runtime_query = new WP_Query( $query_args ); + } finally { + remove_filter( 'ep_facet_meta_fields', $add_meta_fields ); + } + + $aggregations = $feature->get_query_aggregations( $runtime_query ); + + if ( ! is_array( $aggregations ) ) { + return []; + } + + $facet_names = array_map( + function ( $field ) { + return $this->get_filter_name() . $field; + }, + $meta_fields + ); + + return array_intersect_key( $aggregations, array_flip( $facet_names ) ); + } + /** * Get all values for the a given meta field. * diff --git a/includes/classes/Feature/Facets/Types/Meta/Renderer.php b/includes/classes/Feature/Facets/Types/Meta/Renderer.php index c6a9578a23..e6ebf6658b 100644 --- a/includes/classes/Feature/Facets/Types/Meta/Renderer.php +++ b/includes/classes/Feature/Facets/Types/Meta/Renderer.php @@ -77,8 +77,11 @@ public function render( $args, $instance ) { */ $raw_values = $facet_type->get_meta_values( $instance['facet'] ); - $facet_name = $facet_type->get_filter_name() . $this->meta_field; - $facet_aggregations = $feature->get_facet_aggregation( $wp_query, $facet_name ); + $facet_aggregations = $facet_type->get_facet_aggregation_for_field( $wp_query, $this->meta_field ); + + if ( ! is_array( $facet_aggregations ) ) { + $facet_aggregations = []; + } $values = []; From de364d764a02d68ca43b8dbc246d23de938fb72f Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Wed, 18 Feb 2026 01:50:16 +0530 Subject: [PATCH 2/2] add phpunit test --- .../Feature/Facets/Types/Meta/FacetType.php | 27 ++++--- tests/php/features/TestFacetTypeMeta.php | 71 +++++++++++++++++++ 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/includes/classes/Feature/Facets/Types/Meta/FacetType.php b/includes/classes/Feature/Facets/Types/Meta/FacetType.php index 6aa4afd0ba..916bd2e7dd 100644 --- a/includes/classes/Feature/Facets/Types/Meta/FacetType.php +++ b/includes/classes/Feature/Facets/Types/Meta/FacetType.php @@ -350,12 +350,14 @@ public function get_facet_aggregation_for_field( WP_Query $query, string $meta_f $feature = Features::factory()->get_registered_feature( 'facets' ); $facet_name = $this->get_filter_name() . $meta_field; + // If aggregation data is already on this query, reuse it and skip a fallback query. $facet_aggregation = $feature->get_facet_aggregation( $query, $facet_name ); if ( false !== $facet_aggregation ) { return $facet_aggregation; } + // Track attempted fields on this query so one miss does not trigger repeated fallback queries. $attempted_fields = (array) $query->get( 'ep_meta_runtime_agg_fields_attempted', [] ); if ( in_array( $meta_field, $attempted_fields, true ) ) { @@ -366,12 +368,14 @@ public function get_facet_aggregation_for_field( WP_Query $query, string $meta_f $query->set( 'ep_meta_runtime_agg_fields_attempted', array_values( array_unique( $attempted_fields ) ) ); + // Run a second query in the same context so late-rendered blocks can still get this field's counts. $runtime_aggregations = $this->query_runtime_aggregations( $query, [ $meta_field ] ); if ( empty( $runtime_aggregations[ $facet_name ] ) || ! is_array( $runtime_aggregations[ $facet_name ] ) ) { return false; } + // Save recovered aggregation data on the original query so renderers read one source of truth. $current_aggregations = $feature->get_query_aggregations( $query ); if ( ! is_array( $current_aggregations ) ) { @@ -392,6 +396,7 @@ public function get_facet_aggregation_for_field( WP_Query $query, string $meta_f * @return array */ protected function query_runtime_aggregations( WP_Query $query, array $meta_fields ): array { + // Normalize input so facet keys are built from non-empty, unique field names. $meta_fields = array_values( array_unique( array_filter( $meta_fields ) ) ); if ( empty( $meta_fields ) ) { @@ -400,6 +405,7 @@ protected function query_runtime_aggregations( WP_Query $query, array $meta_fiel $feature = Features::factory()->get_registered_feature( 'facets' ); + // Include selected meta fields so fallback counts reflect currently selected facet filters. $selected_meta_fields = []; $selected_filters = (array) $feature->get_selected(); @@ -413,34 +419,33 @@ protected function query_runtime_aggregations( WP_Query $query, array $meta_fiel return array_values( array_unique( array_merge( (array) $fields, $runtime_fields ) ) ); }; + // Inject runtime fields so normal facet aggregation building includes them. add_filter( 'ep_facet_meta_fields', $add_meta_fields ); $query_args = $query->query_vars; + + // Remove internal guard state from the cloned query arguments. unset( $query_args['ep_meta_runtime_agg_fields_attempted'] ); + // Force facetable flags because Facets::is_facetable() decides whether aggregations are attached. $query_args = wp_parse_args( $query_args, [ - 'ep_is_facetable' => true, - 'ep_integrate' => true, - 'posts_per_page' => 1, - 'no_found_rows' => true, - 'cache_results' => false, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - 'update_menu_item_cache' => false, - 'lazy_load_term_meta' => false, - 'ignore_sticky_posts' => true, - 'suppress_filters' => false, + 'ep_is_facetable' => true, + 'ep_integrate' => true, + // Keep query filters enabled; suppressing them can change which posts are included in facet counts. + 'suppress_filters' => false, ] ); try { $runtime_query = new WP_Query( $query_args ); } finally { + // Always remove temporary filters to restore global state. remove_filter( 'ep_facet_meta_fields', $add_meta_fields ); } + // Return only requested fields. Any extra selected fields were included only for context. $aggregations = $feature->get_query_aggregations( $runtime_query ); if ( ! is_array( $aggregations ) ) { diff --git a/tests/php/features/TestFacetTypeMeta.php b/tests/php/features/TestFacetTypeMeta.php index 9153c30bd1..1b6b17a932 100644 --- a/tests/php/features/TestFacetTypeMeta.php +++ b/tests/php/features/TestFacetTypeMeta.php @@ -317,6 +317,77 @@ public function testAddQueryFiltersWithNotAllowedParameters() { $this->assertSame( $expected, $new_filters ); } + /** + * Test that meta facet aggregations can be recovered at runtime when the + * main facetable query did not include that meta field aggregation. + * + * This verifies the fallback path used by the renderer to avoid empty + * (zero-count) facet items when aggregation data is missing. + * + * @since 5.3.1 + * @group facets + */ + public function testGetFacetAggregationForFieldRuntimeFallback() { + $meta_field = 'runtime_meta_' . wp_generate_password( 8, false, false ); + + $allow_meta_key = function ( $allowed_meta_keys ) use ( $meta_field ) { + return array_merge( $allowed_meta_keys, [ $meta_field ] ); + }; + add_filter( 'ep_prepare_meta_allowed_keys', $allow_meta_key ); + + try { + $this->ep_factory->post->create_many( + 2, + [ + 'meta_input' => [ + $meta_field => 'alpha', + ], + ] + ); + + $this->ep_factory->post->create( + [ + 'meta_input' => [ + $meta_field => 'beta', + ], + ] + ); + + \ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $query = new \WP_Query( + [ + 'ep_is_facetable' => true, + 'post_type' => 'post', + ] + ); + + // Confirm this query was served by Elasticsearch and can hold facet aggs. + $this->assertTrue( $query->elasticsearch_success ); + + $facet_feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_type = $facet_feature->types['meta']; + $facet_name = $facet_type->get_filter_name() . $meta_field; + + // The requested meta field is intentionally missing before fallback runs. + $this->assertFalse( $facet_feature->get_facet_aggregation( $query, $facet_name ) ); + + $aggregation = $facet_type->get_facet_aggregation_for_field( $query, $meta_field ); + + // Fallback returns real counts for the requested field. + $this->assertIsArray( $aggregation ); + $this->assertArrayHasKey( 'alpha', $aggregation ); + $this->assertArrayHasKey( 'beta', $aggregation ); + $this->assertSame( 2, $aggregation['alpha'] ); + $this->assertSame( 1, $aggregation['beta'] ); + + // Fallback result is merged back into the original query aggregation cache. + $this->assertSame( $aggregation, $facet_feature->get_facet_aggregation( $query, $facet_name ) ); + } finally { + remove_filter( 'ep_prepare_meta_allowed_keys', $allow_meta_key ); + } + } + /** * Test get_facets_meta_fields *