<?php
/**
* Instant Search feature
*
* @package elasticpress
*/
namespace ElasticPress\Feature\InstantResults;
use ElasticPress\Elasticsearch;
use ElasticPress\Feature;
use ElasticPress\FeatureRequirementsStatus;
use ElasticPress\Features;
use ElasticPress\Indexables;
use ElasticPress\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Instant Results feature class.
*
* @since 4.0.0
*/
class InstantResults extends Feature {
/**
* Elasticsearch index name.
*
* @var string
*/
protected $index;
/**
* Host URL.
*
* @var string
*/
protected $host;
/**
* WooCommerce is in use.
*
* @var boolean
*/
protected $is_woocommerce;
/**
* Elasticsearch query template.
*
* @var string
*/
protected $search_template = '';
/**
* Feature settings
*
* @var array
*/
protected $settings = [];
/**
* Initialize feature.
*
* @return void
*/
public function __construct() {
$this->slug = 'instant-results';
$this->title = esc_html__( 'Instant Results', 'elasticpress' );
$this->short_title = esc_html__( 'Instant Results', 'elasticpress' );
$this->summary = '<p>' . __( 'WordPress search forms will display results instantly. When the search query is submitted, a modal will open that populates results by querying ElasticPress directly, bypassing WordPress. As the user refines their search, results are refreshed.', 'elasticpress' ) . '</p>' .
'<p>' . __( 'Requires an <a href="https://www.elasticpress.io/" target="_blank">ElasticPress.io plan</a> or a custom proxy to function.', 'elasticpress' ) . '</p>';
$this->docs_url = __( 'https://www.elasticpress.io/documentation/article/configuring-elasticpress-via-the-plugin-dashboard/#instant-results', 'elasticpress' );
$this->host = trailingslashit( Utils\get_host() );
$this->index = Indexables::factory()->get( 'post' )->get_index_name();
$this->is_woocommerce = function_exists( 'WC' );
$this->default_settings = [
'highlight_tag' => 'mark',
'facets' => 'post_type,tax-category,tax-post_tag',
'match_type' => 'all',
'term_count' => '1',
'per_page' => get_option( 'posts_per_page', 6 ),
'search_behavior' => '0',
];
$this->settings = $this->get_settings();
$this->requires_install_reindex = true;
$this->available_during_installation = true;
$this->is_powered_by_epio = Utils\is_epio();
parent::__construct();
}
/**
* Output detailed feature description.
*
* @return void
*/
public function output_feature_box_long() {
?>
<p>
<?php
printf(
/* translators: %s: ElasticPress.io link. */
esc_html__( 'WordPress search forms will display results instantly. When the search query is submitted, a modal will open that populates results by querying ElasticPress directly, bypassing WordPress. As the user refines their search, results are refreshed. Requires an %s or a custom proxy to function.', 'elasticpress' ),
sprintf(
'<a href="%1$s" target="_blank">%2$s</a>',
'https://www.elasticpress.io/',
esc_html__( 'ElasticPress.io plan', 'elasticpress' )
)
);
?>
</p>
<?php
}
/**
* Display feature settings.
*
* @return void
*/
public function output_feature_box_settings() {
if ( ! $this->is_active() ) {
return;
}
$highlight_tags = [ 'mark', 'span', 'strong', 'em', 'i' ];
?>
<div class="field">
<label for="instant-results-highlight-tag" class="field-name status"><?php echo esc_html_e( 'Highlight tag ', 'elasticpress' ); ?></label>
<div class="input-wrap">
<select id="instant-results-highlight-tag" name="settings[highlight_tag]">
<option value=""><?php esc_html_e( 'None', 'elasticpress' ); ?></option>
<?php
foreach ( $highlight_tags as $highlight_tag ) {
printf(
'<option value="%1$s" %2$s>%3$s</option>',
esc_attr( $highlight_tag ),
selected( $this->settings['highlight_tag'], $highlight_tag, false ),
esc_html( $highlight_tag )
);
}
?>
</select>
<p class="field-description"><?php esc_html_e( 'Highlight search terms in results with the selected HTML tag.', 'elasticpress' ); ?></p>
</div>
</div>
<div class="field">
<label for="feature_instant_results_facets" class="field-name status"><?php esc_html_e( 'Filters', 'elasticpress' ); ?></label>
<div class="input-wrap">
<input value="<?php echo esc_attr( $this->settings['facets'] ); ?>" type="text" name="settings[facets]" id="feature_instant_results_facets">
</div>
</div>
<div class="field">
<div class="field-name status"><?php esc_html_e( 'Match Type', 'elasticpress' ); ?></div>
<div class="input-wrap">
<label>
<input name="settings[match_type]" type="radio" <?php checked( $this->settings['match_type'], 'all' ); ?> value="all">
<?php echo wp_kses_post( __( 'Show any content tagged to <strong>all</strong> selected terms', 'elasticpress' ) ); ?>
</label><br>
<label>
<input name="settings[match_type]" type="radio" <?php checked( $this->settings['match_type'], 'any' ); ?> value="any">
<?php echo wp_kses_post( __( 'Show all content tagged to <strong>any</strong> selected term', 'elasticpress' ) ); ?>
</label>
<p class="field-description"><?php esc_html_e( '"All" will only show content that matches all filters. "Any" will show content that matches any filter.', 'elasticpress' ); ?></p>
</div>
</div>
<div class="field">
<div class="field-name status"><?php esc_html_e( 'Term Count', 'elasticpress' ); ?></div>
<div class="input-wrap">
<label>
<input name="settings[term_count]" <?php checked( (bool) $this->settings['term_count'] ); ?> type="radio" value="1"><?php esc_html_e( 'Enabled', 'elasticpress' ); ?>
</label><br>
<label>
<input name="settings[term_count]" <?php checked( ! (bool) $this->settings['term_count'] ); ?> type="radio" value="0"><?php esc_html_e( 'Disabled', 'elasticpress' ); ?>
</label>
<p class="field-description"><?php esc_html_e( 'When enabled, it will show the term count in the instant results widget.', 'elasticpress' ); ?></p>
</div>
</div>
<?php
$show_suggestions = \ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->is_active();
if ( $show_suggestions ) :
?>
<div class="field">
<div class="field-name status"><?php esc_html_e( 'Search behavior when no result is found', 'elasticpress' ); ?></div>
<div class="input-wrap">
<label><input name="settings[search_behavior]" type="radio" <?php checked( $this->settings['search_behavior'], '0' ); ?> <?php disabled( $show_suggestions, false ); ?> value="0"><?php esc_html_e( 'Display the top suggestion', 'elasticpress' ); ?></label><br>
<label><input name="settings[search_behavior]" type="radio" <?php checked( $this->settings['search_behavior'], 'list' ); ?> <?php disabled( $show_suggestions, false ); ?> value="list"><?php esc_html_e( 'Display all the suggestions', 'elasticpress' ); ?></label><br>
</div>
</div>
<?php
endif;
}
/**
* Tell user whether requirements for feature are met or not.
*
* @return array $status Status array
*/
public function requirements_status() {
$status = new FeatureRequirementsStatus( 2 );
$status->message = [];
if ( Utils\is_epio() ) {
$status->code = 1;
/**
* Whether the feature is available for non ElasticPress.io customers.
*
* Installations using self-hosted Elasticsearch will need to implement an API for
* handling search requests before making the feature available.
*
* @since 4.0.0
* @hook ep_instant_results_available
* @param {string} $available Whether the feature is available.
*/
} elseif ( apply_filters( 'ep_instant_results_available', false ) ) {
$status->code = 1;
$status->message[] = esc_html__( 'You are using a custom proxy. Make sure you implement all security measures needed.', 'elasticpress' );
} else {
$status->message[] = wp_kses_post( __( "To use this feature you need to be an <a href='https://elasticpress.io'>ElasticPress.io</a> customer or implement a <a href='https://github.com/10up/elasticpress-proxy'>custom proxy</a>.", 'elasticpress' ) );
}
/**
* Display a warning if ElasticPress is network activated.
*/
if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
$status->message[] = wp_kses_post(
sprintf(
/* translators: Article URL */
__(
'ElasticPress is network activated. Additional steps are required to ensure Instant Results works for all sites on the network. See our article on <a href="%s" target="_blank">running ElasticPress in network mode</a> for more details.',
'elasticpress'
),
'https://www.elasticpress.io/documentation/article/running-elasticpress-in-a-wordpress-multisite-network-mode/'
)
);
}
return $status;
}
/**
* Setup feature functionality.
*
* @return void
*/
public function setup() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
add_filter( 'ep_after_update_feature', [ $this, 'after_update_feature' ], 10, 3 );
add_filter( 'ep_formatted_args', [ $this, 'maybe_apply_aggs_args' ], 10, 3 );
add_filter( 'ep_post_mapping', [ $this, 'add_mapping_properties' ] );
add_filter( 'ep_post_sync_args', [ $this, 'add_post_sync_args' ], 10, 2 );
add_filter( 'ep_after_sync_index', [ $this, 'epio_save_search_template' ] );
add_filter( 'ep_saved_weighting_configuration', [ $this, 'epio_save_search_template' ] );
add_filter( 'ep_bypass_exclusion_from_search', [ $this, 'maybe_bypass_post_exclusion' ], 10, 2 );
add_action( 'pre_get_posts', [ $this, 'maybe_apply_product_visibility' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ] );
add_action( 'wp_footer', [ $this, 'render' ] );
}
/**
* Output modal markup.
*/
public function render() {
echo '<div id="ep-instant-results"></div>';
}
/**
* Enqueue our autosuggest script.
*/
public function enqueue_frontend_assets() {
if ( Utils\is_indexing() ) {
return;
}
wp_enqueue_style(
'elasticpress-instant-results',
EP_URL . 'dist/css/instant-results-styles.css',
Utils\get_asset_info( 'instant-results-styles', 'dependencies' ),
Utils\get_asset_info( 'instant-results-styles', 'version' )
);
wp_enqueue_script(
'elasticpress-instant-results',
EP_URL . 'dist/js/instant-results-script.js',
Utils\get_asset_info( 'instant-results-script', 'dependencies' ),
Utils\get_asset_info( 'instant-results-script', 'version' ),
true
);
wp_set_script_translations( 'elasticpress-instant-results', 'elasticpress' );
/**
* The search API endpoint.
*
* @since 4.0.0
* @hook ep_instant_results_search_endpoint
* @param {string} $endpoint Endpoint path.
* @param {string} $index Elasticsearch index.
*/
$api_endpoint = apply_filters( 'ep_instant_results_search_endpoint', "api/v1/search/posts/{$this->index}", $this->index );
wp_localize_script(
'elasticpress-instant-results',
'epInstantResults',
array(
'apiEndpoint' => $api_endpoint,
'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? esc_url_raw( $this->host ) : '',
'argsSchema' => $this->get_args_schema(),
'currencyCode' => $this->is_woocommerce ? get_woocommerce_currency() : false,
'facets' => $this->get_facets_for_frontend(),
'highlightTag' => $this->settings['highlight_tag'],
'isWooCommerce' => $this->is_woocommerce,
'locale' => str_replace( '_', '-', get_locale() ),
'matchType' => $this->settings['match_type'],
'paramPrefix' => 'ep-',
'postTypeLabels' => $this->get_post_type_labels(),
'termCount' => $this->settings['term_count'],
'requestIdBase' => Utils\get_request_id_base(),
'showSuggestions' => \ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->is_active(),
'suggestionsBehavior' => $this->settings['search_behavior'],
)
);
}
/**
* Enqueue admin assets.
*
* @param string $hook_suffix The current admin page.
*/
public function enqueue_admin_assets( $hook_suffix ) {
if ( 'toplevel_page_elasticpress' !== $hook_suffix ) {
return;
}
wp_enqueue_style( 'wp-edit-post' );
wp_enqueue_script(
'elasticpress-instant-results-admin',
EP_URL . 'dist/js/instant-results-admin-script.js',
Utils\get_asset_info( 'instant-results-admin-script', 'dependencies' ),
Utils\get_asset_info( 'instant-results-admin-script', 'version' ),
true
);
wp_set_script_translations( 'elasticpress-instant-results-admin', 'elasticpress' );
wp_localize_script(
'elasticpress-instant-results-admin',
'epInstantResultsAdmin',
array(
'facets' => $this->get_facets_for_admin(),
)
);
}
/**
* Save or delete the search template on ElasticPress.io based on whether
* the Instant Results feature is being activated or deactivated.
*
* @param string $feature Feature slug
* @param array $settings Feature settings
* @param array $data Feature activation data
*
* @return void
*
* @since 4.3.0
*/
public function after_update_feature( $feature, $settings, $data ) {
if ( $feature !== $this->slug ) {
return;
}
if ( true === $data['active'] ) {
$this->epio_save_search_template();
} else {
$this->epio_delete_search_template();
}
}
/**
* Get the endpoint for the Instant Results search template.
*
* @return string Instant Results search template endpoint.
*/
public function get_template_endpoint() {
/**
* Filters the search template API endpoint.
*
* @since 4.0.0
* @hook ep_instant_results_template_endpoint
* @param {string} $endpoint Endpoint path.
* @param {string} $index Elasticsearch index.
* @returns {string} Search template API endpoint.
*/
return apply_filters( 'ep_instant_results_template_endpoint', "api/v1/search/posts/{$this->index}/template/", $this->index );
}
/**
* Save the search template to ElasticPress.io.
*
* @return void
*/
public function epio_save_search_template() {
$endpoint = $this->get_template_endpoint();
$template = $this->get_search_template();
Elasticsearch::factory()->remote_request(
$endpoint,
[
'blocking' => false,
'body' => $template,
'method' => 'PUT',
]
);
/**
* Fires after the request is sent the search template API endpoint.
*
* @since 4.0.0
* @hook ep_instant_results_template_saved
* @param {string} $template The search template (JSON).
* @param {string} $index Index name.
*/
do_action( 'ep_instant_results_template_saved', $template, $this->index );
}
/**
* Delete the search template from ElasticPress.io.
*
* @return void
*
* @since 4.3.0
*/
public function epio_delete_search_template() {
$endpoint = $this->get_template_endpoint();
Elasticsearch::factory()->remote_request(
$endpoint,
[
'blocking' => false,
'method' => 'DELETE',
]
);
/**
* Fires after the request is sent the search template API endpoint.
*
* @since 4.3.0
* @hook ep_instant_results_template_deleted
* @param {string} $index Index name.
*/
do_action( 'ep_instant_results_template_deleted', $this->index );
}
/**
* Get the saved search template from ElasticPress.io.
*
* @return string|WP_Error Search template if found, WP_Error on error.
*
* @since 4.4.0
*/
public function epio_get_search_template() {
$endpoint = $this->get_template_endpoint();
$request = Elasticsearch::factory()->remote_request( $endpoint );
if ( is_wp_error( $request ) ) {
return $request;
}
$response = wp_remote_retrieve_body( $request );
return $response;
}
/**
* Generate a search template.
*
* A search template is the JSON for an Elasticsearch query with a
* placeholder search term. The template is sent to ElasticPress.io where
* it's used to make Elasticsearch queries using search terms sent from
* the front end.
*
* @return string The search template as JSON.
*/
public function get_search_template() {
$post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types();
$post_statuses = get_post_stati(
[
'public' => true,
'exclude_from_search' => false,
]
);
/**
* The ID of the current user when generating the Instant Results
* search template.
*
* By default Instant Results sets the current user as anomnymous when
* generating the search template, so that any filters applied to
* queries for logged-in or specific users are not applied to the
* template. This filter supports setting a specific user as the
* current user while the template is generated.
*
* @since 4.1.0
* @hook ep_search_template_user_id
* @param {int} $user_id User ID to use.
* @return {int} New user ID to use.
*/
$template_user_id = apply_filters( 'ep_search_template_user_id', 0 );
$original_user_id = get_current_user_id();
wp_set_current_user( $template_user_id );
add_filter( 'ep_intercept_remote_request', '__return_true' );
add_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10, 4 );
add_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10, 2 );
$query = new \WP_Query(
array(
'ep_integrate' => true,
'ep_search_template' => true,
'post_status' => array_values( $post_statuses ),
'post_type' => $post_types,
's' => '{{ep_placeholder}}',
)
);
remove_filter( 'ep_intercept_remote_request', '__return_true' );
remove_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10 );
remove_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10 );
wp_set_current_user( $original_user_id );
return $this->search_template;
}
/**
* Return true if a given feature is supported by Instant Results.
*
* Applied as a filter on Utils\is_integrated_request() so that features
* are enabled for the query that is used to generate the search template,
* regardless of the request type. This avoids the need to send a request
* to the front end.
*
* @param bool $is_integrated Whether queries for the request will be
* integrated.
* @param string $context Context for the original check. Usually the
* slug of the feature doing the check.
* @return bool True if the check is for a feature supported by instant
* search.
*/
public function is_integrated_request( $is_integrated, $context ) {
$supported_contexts = [
'autosuggest',
'documents',
'search',
'weighting',
'woocommerce',
];
return in_array( $context, $supported_contexts, true );
}
/**
* Store intercepted request body and return request result.
*
* @param object $response Response
* @param array $query Query
* @param array $args WP_Query argument array
* @param int $failures Count of failures in request loop
* @return object $response Response
*/
public function intercept_search_request( $response, $query = [], $args = [], $failures = 0 ) {
$this->search_template = $query['args']['body'];
return wp_remote_request( $query['url'], $args );
}
/**
* If generating the search template query, do not bypass the post exclusion
*
* @since 4.4.0
* @param bool $bypass_exclusion_from_search Whether the post exclusion from search should be applied or not
* @param WP_Query $query The WP Query
* @return bool
*/
public function maybe_bypass_post_exclusion( $bypass_exclusion_from_search, $query ) {
return true === $query->get( 'ep_search_template' ) ?
false : // not bypass, apply
$bypass_exclusion_from_search;
}
/**
* Apply product visibility taxonomy query to search template queries.
*
* @param \WP_Query $query Query instance.
* @return void
*/
public function maybe_apply_product_visibility( $query ) {
if ( true !== $query->get( 'ep_search_template' ) ) {
return;
}
if ( ! $this->is_woocommerce ) {
return;
}
$this->apply_product_visibility( $query );
}
/**
* Apply product visibility taxonomy query.
*
* Applies filters to exclude products set to be excluded from search. Out
* of stock products will also be excluded if WooCommerce is configured to
* hide those products.
*
* Mimics the logic of WC_Query::get_tax_query().
*
* @param \WP_Query $query Query instance.
* @return void
*/
public function apply_product_visibility( $query ) {
$product_visibility_terms = wc_get_product_visibility_term_ids();
$product_visibility_not_in = (array) $product_visibility_terms['exclude-from-search'];
if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
$product_visibility_not_in[] = $product_visibility_terms['outofstock'];
}
if ( ! empty( $product_visibility_not_in ) ) {
$tax_query = $query->get( 'tax_query', array() );
$tax_query[] = array(
'taxonomy' => 'product_visibility',
'field' => 'term_taxonomy_id',
'terms' => $product_visibility_not_in,
'operator' => 'NOT IN',
);
$query->set( 'tax_query', $tax_query );
}
}
/**
* Apply aggregation args to search templates.
*
* @param array $formatted_args Formatted Elasticsearch query.
* @param array $query_vars Query variables
* @param \WP_Query $query Query instance.
* @return array Formatted Elasticsearch query.
*/
public function maybe_apply_aggs_args( $formatted_args, $query_vars, $query ) {
if ( true !== $query->get( 'ep_search_template' ) ) {
return $formatted_args;
}
return $this->apply_aggs_args( $formatted_args );
}
/**
* Add aggregation args to Elasticsearch query for facets.
*
* @param array $formatted_args Formatted Elasticsearch query.
* @return array Formatted Elasticsearch query.
*/
public function apply_aggs_args( $formatted_args ) {
$filter = $formatted_args['post_filter'];
$facets = $this->get_facets();
foreach ( $facets as $key => $facet ) {
$formatted_args['aggs'][ $key ]['aggs'] = $facet['aggs'];
if ( $filter ) {
$formatted_args['aggs'][ $key ]['filter'] = $filter;
}
}
return $formatted_args;
}
/**
* Add additional fields to post mapping.
*
* @param array $mapping Post mapping.
* @return array Post mapping.
*/
public function add_mapping_properties( $mapping ) {
$elasticsearch_version = Elasticsearch::factory()->get_elasticsearch_version();
$properties = array(
'post_content_plain' => array( 'type' => 'text' ),
'price_html' => array( 'type' => 'text' ),
);
if ( version_compare( (string) $elasticsearch_version, '7.0', '<' ) ) {
$mapping['mappings']['post']['properties'] = array_merge(
$mapping['mappings']['post']['properties'],
$properties
);
} else {
$mapping['mappings']['properties'] = array_merge(
$mapping['mappings']['properties'],
$properties
);
}
return $mapping;
}
/**
* Add data for additional mapping properties.
*
* @param array $post_args Post arguments.
* @param integer $post_id Post ID.
* @return array Post sync args.
*/
public function add_post_sync_args( $post_args, $post_id ) {
$post = get_post( $post_id );
$post_args['post_content_plain'] = $this->prepare_plain_content_arg( $post );
$post_args['price_html'] = $this->prepare_price_html_arg( $post );
return $post_args;
}
/**
* Get data for the plain post content.
*
* @param WP_Post $post Post object.
* @return string Post content.
*/
public function prepare_plain_content_arg( $post ) {
$post_content = apply_filters( 'the_content', $post->post_content );
return wp_strip_all_tags( $post_content );
}
/**
* Get data for the price HTML arg.
*
* @param WP_Post $post Post object.
* @return string|null Price HTML.
*/
public function prepare_price_html_arg( $post ) {
if ( 'product' !== $post->post_type ) {
return null;
}
if ( ! $this->is_woocommerce ) {
return null;
}
$product = wc_get_product( $post );
return $product->get_price_html();
}
/**
* Get post type labels.
*
* Only the post type slug is indexed, so we'll need the labels on the
* front end for display.
*
* @return array Array of post types and their labels.
*/
public function get_post_type_labels() {
$labels = [];
$post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types();
foreach ( $post_types as $post_type ) {
$post_type_object = get_post_type_object( $post_type );
$post_type_labels = get_post_type_labels( $post_type_object );
$labels[ $post_type ] = array(
'plural' => $post_type_labels->name,
'singular' => $post_type_labels->singular_name,
);
}
return $labels;
}
/**
* Get available facets.
*
* @return array Available facets.
*/
public function get_facets() {
$facets = [];
/**
* Post type facet.
*/
$facets['post_type'] = array(
'type' => 'post_type',
'post_types' => [],
'labels' => array(
'admin' => __( 'Post type', 'elasticpress' ),
'frontend' => __( 'Type', 'elasticpress' ),
),
'aggs' => array(
'post_type' => array(
'terms' => array(
'field' => 'post_type.raw',
),
),
),
/**
* The post_type arg needs to be supported regardless of whether
* the Post Type facet is present to be able to support setting the
* post type from the search form.
*
* @see ElasticPress\Feature\InstantResults::get_args_schema()
*/
'args' => array(),
);
/**
* Taxonomy facets.
*/
$taxonomies = get_taxonomies( array( 'public' => true ), 'object' );
$taxonomies = apply_filters( 'ep_facet_include_taxonomies', $taxonomies );
foreach ( $taxonomies as $slug => $taxonomy ) {
$name = 'tax-' . $slug;
$labels = get_taxonomy_labels( $taxonomy );
$admin_label = sprintf(
/* translators: $1$s: Taxonomy name. %2$s: Taxonomy slug. */
esc_html__( '%1$s (%2$s)' ),
$labels->singular_name,
$slug
);
$post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types();
$post_types = array_intersect( $post_types, $taxonomy->object_type );
$post_types = array_values( $post_types );
$facets[ $name ] = array(
'type' => 'taxonomy',
'post_types' => $post_types,
'labels' => array(
'admin' => $admin_label,
'frontend' => $labels->singular_name,
),
'aggs' => array(
$name => array(
'terms' => array(
'field' => 'terms.' . $slug . '.facet',
'size' => apply_filters( 'ep_facet_taxonomies_size', 10000, $taxonomy ),
),
),
),
'args' => array(
$name => array(
'type' => 'strings',
),
),
);
}
/**
* Price facet.
*/
if ( $this->is_woocommerce ) {
$facets['price_range'] = array(
'type' => 'price_range',
'post_types' => [ 'product' ],
'labels' => array(
'admin' => __( 'Price range', 'elasticpress' ),
'frontend' => __( 'Price', 'elasticpress' ),
),
'aggs' => array(
'max_price' => array(
'max' => array(
'field' => 'meta._price.double',
),
),
'min_price' => array(
'min' => array(
'field' => 'meta._price.double',
),
),
),
'args' => array(
'max_price' => array(
'type' => 'number',
),
'min_price' => array(
'type' => 'number',
),
),
);
}
return $facets;
}
/**
* Get facet configuration for the front end.
*
* @return Array Facet configuration for the front end.
*/
public function get_facets_for_frontend() {
$selected_facets = explode( ',', $this->settings['facets'] );
$available_facets = $this->get_facets();
$facets = [];
foreach ( $selected_facets as $key ) {
if ( isset( $available_facets[ $key ] ) ) {
$facet = $available_facets[ $key ];
$facets[] = array(
'name' => $key,
'label' => $facet['labels']['frontend'],
'type' => $facet['type'],
'postTypes' => $facet['post_types'],
);
}
}
return $facets;
}
/**
* Get facet configuration for the admin.
*
* @return Array Facet configuration for the admin.
*/
public function get_facets_for_admin() {
$available_facets = $this->get_facets();
$facets = [];
foreach ( $available_facets as $key => $facet ) {
$facets[ $key ] = array(
'label' => $facet['labels']['admin'],
'value' => $key,
);
}
return $facets;
}
/**
* Get schema for search args.
*
* @return array Search args schema.
*/
public function get_args_schema() {
/**
* The number of results per page for Instant Results.
*
* @since 4.5.0
* @hook ep_instant_results_per_page
* @param {int} $per_page Results per page.
*/
$per_page = apply_filters( 'ep_instant_results_per_page', $this->settings['per_page'] );
$args_schema = array(
'highlight' => array(
'type' => 'string',
'default' => $this->settings['highlight_tag'],
'allowedValues' => [ $this->settings['highlight_tag'] ],
),
'offset' => array(
'type' => 'number',
'default' => 0,
),
'orderby' => array(
'type' => 'string',
'default' => 'relevance',
'allowedValues' => [ 'date', 'price', 'relevance' ],
),
'order' => array(
'type' => 'string',
'default' => 'desc',
'allowedValues' => [ 'asc', 'desc' ],
),
'per_page' => array(
'type' => 'number',
'default' => absint( $per_page ),
),
'post_type' => array(
'type' => 'strings',
),
'search' => array(
'type' => 'string',
'default' => '',
),
'relation' => array(
'type' => 'string',
'default' => 'all' === $this->settings['match_type'] ? 'and' : 'or',
'allowedValues' => [ 'and', 'or' ],
),
);
$selected_facets = explode( ',', $this->settings['facets'] );
$available_facets = $this->get_facets();
foreach ( $selected_facets as $key ) {
if ( isset( $available_facets[ $key ] ) ) {
$args_schema = array_merge( $args_schema, $available_facets[ $key ]['args'] );
}
}
/**
* The schema defining the API arguments used by Instant Results.
*
* The argument schema is used to configure the APISearchProvider
* component used by Instant Results, and should conform to what is
* supported by the API being used. The Instant Results UI expects
* the default list of arguments to be available, so caution is advised
* when adding or removing arguments.
*
* @since 4.5.1
* @hook ep_instant_results_args_schema
* @param {array} $args_schema Results per page.
*/
return apply_filters( 'ep_instant_results_args_schema', $args_schema );
}
/**
* Set the `settings_schema` attribute
*
* @since 5.0.0
*/
protected function set_settings_schema() {
$facets = $this->get_facets_for_admin();
$this->settings_schema = [
[
'default' => 'mark',
'help' => __( 'Select the HTML tag used to highlight search terms.', 'elasticpress' ),
'key' => 'highlight_tag',
'label' => __( 'Highlight tag', 'elasticpress' ),
'options' => [
[
'label' => __( 'None', 'elasticpress' ),
'value' => '',
],
[
'label' => 'mark',
'value' => 'mark',
],
[
'label' => 'span',
'value' => 'span',
],
[
'label' => 'strong',
'value' => 'strong',
],
[
'label' => 'em',
'value' => 'em',
],
[
'label' => 'i',
'value' => 'i',
],
],
'type' => 'select',
],
[
'default' => 'post_type,tax-category,tax-post_tag',
'key' => 'facets',
'label' => __( 'Filters', 'elasticpress' ),
'options' => array_values( $facets ),
'type' => 'multiple',
],
[
'default' => 'all',
'key' => 'match_type',
'label' => __( 'Filter matching', 'elasticpress' ),
'options' => [
[
'label' => __( 'Show results that match <strong>all</strong> selected filters', 'elasticpress' ),
'value' => 'all',
],
[
'label' => __( 'Show results that match <strong>any</strong> selected filter', 'elasticpress' ),
'value' => 'any',
],
],
'type' => 'radio',
],
[
'default' => '1',
'help' => __( 'Enable to show the number of matching results next to filter options.', 'elasticpress' ),
'key' => 'term_count',
'label' => __( 'Show filter counts', 'elasticpress' ),
'type' => 'checkbox',
],
[
'default' => get_option( 'posts_per_page', 6 ),
'key' => 'per_page',
'type' => 'hidden',
],
[
'default' => '0',
'key' => 'search_behavior',
'label' => __( 'Search behavior when no result is found', 'elasticpress' ),
'options' => [
[
'label' => __( 'Display the top suggestion', 'elasticpress' ),
'value' => '0',
],
[
'label' => __( 'Display all the suggestions', 'elasticpress' ),
'value' => 'list',
],
],
'requires_feature' => 'did-you-mean',
'type' => 'radio',
],
];
}
}