Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions includes/classes/Feature/Facets/Types/Meta/FacetType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace ElasticPress\Feature\Facets\Types\Meta;

use ElasticPress\Features;
use WP_Query;

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
Expand Down Expand Up @@ -333,6 +334,134 @@ 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;

// 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 ) ) {
return false;
}

$attempted_fields[] = $meta_field;

$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 ) ) {
$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 {
// 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 ) ) {
return [];
}

$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();

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 ) ) );
};

// 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,
// 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 ) ) {
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.
*
Expand Down
7 changes: 5 additions & 2 deletions includes/classes/Feature/Facets/Types/Meta/Renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down
71 changes: 71 additions & 0 deletions tests/php/features/TestFacetTypeMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
Loading