PluginProbe
ElasticPress / 4.3.0
ElasticPress v4.3.0
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Feature / InstantResults / InstantResults.php

InstantResults.php in ElasticPress 4.3.0, at includes/classes/Feature/InstantResults/InstantResults.php

927 lines 25.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Instant Search feature
4 *
5 * @package elasticpress
6 */
7
8 namespace ElasticPress\Feature\InstantResults;
9
10 use ElasticPress\Elasticsearch as Elasticsearch;
11 use ElasticPress\Feature as Feature;
12 use ElasticPress\FeatureRequirementsStatus;
13 use ElasticPress\Features as Features;
14 use ElasticPress\Indexables as Indexables;
15 use ElasticPress\Utils as Utils;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * Instant Results feature class.
23 *
24 * @since 4.0.0
25 */
26 class InstantResults extends Feature {
27 /**
28 * Elasticsearch index name.
29 *
30 * @var string
31 */
32 protected $index;
33
34 /**
35 * Host URL.
36 *
37 * @var string
38 */
39 protected $host;
40
41 /**
42 * WooCommerce is in use.
43 *
44 * @var boolean
45 */
46 protected $is_woocommerce;
47
48 /**
49 * Elasticsearch query template.
50 *
51 * @var string
52 */
53 protected $search_template = '';
54
55 /**
56 * Feature settings
57 *
58 * @var array
59 */
60 protected $settings = [];
61
62 /**
63 * Initialize feature.
64 *
65 * @return void
66 */
67 public function __construct() {
68 $this->slug = 'instant-results';
69
70 $this->title = esc_html__( 'Instant Results', 'elasticpress' );
71
72 $this->summary = __( 'Search forms display results instantly after submission. A modal opens that populates results by querying ElasticPress directly.', 'elasticpress' );
73
74 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#instant-results', 'elasticpress' );
75
76 $this->host = trailingslashit( Utils\get_host() );
77
78 $this->index = Indexables::factory()->get( 'post' )->get_index_name();
79
80 $this->is_woocommerce = function_exists( 'WC' );
81
82 $this->default_settings = [
83 'highlight_tag' => 'mark',
84 'facets' => 'post_type,category,post_tag',
85 'match_type' => 'all',
86 ];
87
88 $settings = $this->get_settings() ? $this->get_settings() : array();
89
90 $this->settings = wp_parse_args( $settings, $this->default_settings );
91
92 $this->requires_install_reindex = true;
93
94 $this->available_during_installation = true;
95
96 parent::__construct();
97 }
98
99 /**
100 * Output detailed feature description.
101 *
102 * @return void
103 */
104 public function output_feature_box_long() {
105 ?>
106 <p>
107 <?php
108 printf(
109 /* translators: %s: ElasticPress.io link. */
110 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 to function.', 'elasticpress' ),
111 sprintf(
112 '<a href=”%1$s” target="_blank">%2$s</a>',
113 'https://www.elasticpress.io/',
114 esc_html__( 'ElasticPress.io plan', 'elasticpress' )
115 )
116 );
117 ?>
118 </p>
119 <?php
120 }
121
122 /**
123 * Display feature settings.
124 *
125 * @return void
126 */
127 public function output_feature_box_settings() {
128 if ( ! $this->is_active() ) {
129 return;
130 }
131
132 $highlight_tags = array( 'mark', 'span', 'strong', 'em', 'i' );
133 ?>
134
135 <div class="field">
136 <label for="instant-results-highlight-tag" class="field-name status"><?php echo esc_html_e( 'Highlight tag ', 'elasticpress' ); ?></label>
137 <div class="input-wrap">
138 <select id="instant-results-highlight-tag" name="settings[highlight_tag]">
139 <option value=""><?php esc_html_e( 'None', 'elasticpress' ); ?></option>
140 <?php
141 foreach ( $highlight_tags as $highlight_tag ) {
142 printf(
143 '<option value="%1$s" %2$s>%3$s</option>',
144 esc_attr( $highlight_tag ),
145 selected( $this->settings['highlight_tag'], $highlight_tag, false ),
146 esc_html( $highlight_tag )
147 );
148 }
149 ?>
150 </select>
151 <p class="field-description"><?php esc_html_e( 'Highlight search terms in results with the selected HTML tag.', 'elasticpress' ); ?></p>
152 </div>
153 </div>
154 <div class="field">
155 <label for="feature_instant_results_facets" class="field-name status"><?php esc_html_e( 'Facets', 'elasticpress' ); ?></label>
156 <div class="input-wrap">
157 <input value="<?php echo esc_attr( $this->settings['facets'] ); ?>" type="text" name="settings[facets]" id="feature_instant_results_facets">
158 </div>
159 </div>
160 <div class="field">
161 <div class="field-name status"><?php esc_html_e( 'Match Type', 'elasticpress' ); ?></div>
162 <div class="input-wrap">
163 <label>
164 <input name="settings[match_type]" type="radio" <?php checked( $this->settings['match_type'], 'all' ); ?> value="all">
165 <?php echo wp_kses_post( __( 'Show any content tagged to <strong>all</strong> selected terms', 'elasticpress' ) ); ?>
166 </label><br>
167 <label>
168 <input name="settings[match_type]" type="radio" <?php checked( $this->settings['match_type'], 'any' ); ?> value="any">
169 <?php echo wp_kses_post( __( 'Show all content tagged to <strong>any</strong> selected term', 'elasticpress' ) ); ?>
170 </label>
171 <p class="field-description"><?php esc_html_e( '"All" will only show content that matches all facets. "Any" will show content that matches any facet.', 'elasticpress' ); ?></p>
172 </div>
173 </div>
174
175 <?php
176 }
177
178 /**
179 * Tell user whether requirements for feature are met or not.
180 *
181 * @return array $status Status array
182 */
183 public function requirements_status() {
184 $status = new FeatureRequirementsStatus( 2 );
185
186 $status->message = [];
187
188 if ( Utils\is_epio() ) {
189 $status->code = 1;
190
191 /**
192 * Whether the feature is available for non ElasticPress.io customers.
193 *
194 * Installations using self-hosted Elasticsearch will need to implement an API for
195 * handling search requests before making the feature available.
196 *
197 * @since 4.0.0
198 * @hook ep_instant_results_available
199 * @param {string} $available Whether the feature is available.
200 */
201 } elseif ( apply_filters( 'ep_instant_results_available', false ) ) {
202 $status->code = 1;
203 $status->message[] = esc_html__( 'You are using a custom proxy. Make sure you implement all security measures needed.', 'elasticpress' );
204 } else {
205 $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' ) );
206 }
207
208 return $status;
209 }
210
211 /**
212 * Setup feature functionality.
213 *
214 * @return void
215 */
216 public function setup() {
217 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
218 add_filter( 'ep_after_update_feature', [ $this, 'after_update_feature' ], 10, 3 );
219 add_filter( 'ep_formatted_args', [ $this, 'maybe_apply_aggs_args' ], 10, 3 );
220 add_filter( 'ep_post_mapping', [ $this, 'add_mapping_properties' ] );
221 add_filter( 'ep_post_sync_args', [ $this, 'add_post_sync_args' ], 10, 2 );
222 add_filter( 'ep_after_sync_index', [ $this, 'epio_save_search_template' ] );
223 add_filter( 'ep_saved_weighting_configuration', [ $this, 'epio_save_search_template' ] );
224 add_action( 'pre_get_posts', [ $this, 'maybe_apply_product_visibility' ] );
225 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ] );
226 add_action( 'wp_footer', [ $this, 'render' ] );
227 }
228
229 /**
230 * Output modal markup.
231 */
232 public function render() {
233 echo '<div id="ep-instant-results"></div>';
234 }
235
236 /**
237 * Enqueue our autosuggest script.
238 */
239 public function enqueue_frontend_assets() {
240 if ( Utils\is_indexing() ) {
241 return;
242 }
243
244 wp_enqueue_style(
245 'elasticpress-instant-results',
246 EP_URL . 'dist/css/instant-results-styles.min.css',
247 Utils\get_asset_info( 'instant-results-styles', 'dependencies' ),
248 Utils\get_asset_info( 'instant-results-styles', 'version' )
249 );
250
251 wp_enqueue_script(
252 'elasticpress-instant-results',
253 EP_URL . 'dist/js/instant-results-script.min.js',
254 Utils\get_asset_info( 'instant-results-script', 'dependencies' ),
255 Utils\get_asset_info( 'instant-results-script', 'version' ),
256 true
257 );
258
259 /**
260 * The search API endpoint.
261 *
262 * @since 4.0.0
263 * @hook ep_instant_results_search_endpoint
264 * @param {string} $endpoint Endpoint path.
265 * @param {string} $index Elasticsearch index.
266 */
267 $api_endpoint = apply_filters( 'ep_instant_results_search_endpoint', "api/v1/search/posts/{$this->index}", $this->index );
268
269 wp_localize_script(
270 'elasticpress-instant-results',
271 'epInstantResults',
272 array(
273 'apiEndpoint' => $api_endpoint,
274 'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? esc_url_raw( $this->host ) : '',
275 'argsSchema' => $this->get_args_schema(),
276 'currencyCode' => $this->is_woocommerce ? get_woocommerce_currency() : false,
277 'facets' => $this->get_facets_for_frontend(),
278 'highlightTag' => $this->settings['highlight_tag'],
279 'isWooCommerce' => $this->is_woocommerce,
280 'locale' => str_replace( '_', '-', get_locale() ),
281 'matchType' => $this->settings['match_type'],
282 'paramPrefix' => 'ep-',
283 'postTypeLabels' => $this->get_post_type_labels(),
284 )
285 );
286 }
287
288 /**
289 * Enqueue admin assets.
290 *
291 * @param string $hook_suffix The current admin page.
292 */
293 public function enqueue_admin_assets( $hook_suffix ) {
294 if ( 'toplevel_page_elasticpress' !== $hook_suffix ) {
295 return;
296 }
297
298 wp_enqueue_style( 'wp-edit-post' );
299
300 wp_enqueue_script(
301 'elasticpress-instant-results-admin',
302 EP_URL . 'dist/js/instant-results-admin-script.min.js',
303 Utils\get_asset_info( 'instant-results-admin-script', 'dependencies' ),
304 Utils\get_asset_info( 'instant-results-admin-script', 'version' ),
305 true
306 );
307
308 wp_localize_script(
309 'elasticpress-instant-results-admin',
310 'epInstantResultsAdmin',
311 array(
312 'facets' => $this->get_facets_for_admin(),
313 )
314 );
315 }
316
317 /**
318 * Save or delete the search template on ElasticPress.io based on whether
319 * the Instant Results feature is being activated or deactivated.
320 *
321 * @param string $feature Feature slug
322 * @param array $settings Feature settings
323 * @param array $data Feature activation data
324 *
325 * @return void
326 *
327 * @since 4.3.0
328 */
329 public function after_update_feature( $feature, $settings, $data ) {
330 if ( $feature !== $this->slug ) {
331 return;
332 }
333
334 if ( true === $data['active'] ) {
335 $this->epio_save_search_template();
336 } else {
337 $this->epio_delete_search_template();
338 }
339 }
340
341 /**
342 * Save the search template to ElasticPress.io.
343 *
344 * @return void
345 */
346 public function epio_save_search_template() {
347 /**
348 * Filters the search template API endpoint.
349 *
350 * @since 4.0.0
351 * @hook ep_instant_results_template_endpoint
352 * @param {string} $endpoint Endpoint path.
353 * @param {string} $index Elasticsearch index.
354 * @returns {string} Search template API endpoint.
355 */
356 $endpoint = apply_filters( 'ep_instant_results_template_endpoint', "api/v1/search/posts/{$this->index}/template/", $this->index );
357
358 $search_template = $this->get_search_template();
359
360 Elasticsearch::factory()->remote_request(
361 $endpoint,
362 [
363 'blocking' => false,
364 'body' => $search_template,
365 'method' => 'PUT',
366 ]
367 );
368
369 /**
370 * Fires after the request is sent the search template API endpoint.
371 *
372 * @since 4.0.0
373 * @hook ep_instant_results_template_saved
374 * @param {string} $search_template The search template (JSON).
375 * @param {string} $index Index name.
376 */
377 do_action( 'ep_instant_results_template_saved', $search_template, $this->index );
378 }
379
380 /**
381 * Delete the search template from ElasticPress.io.
382 *
383 * @return void
384 *
385 * @since 4.3.0
386 */
387 public function epio_delete_search_template() {
388 /**
389 * Filters the search template API endpoint.
390 *
391 * @since 4.0.0
392 * @hook ep_instant_results_template_endpoint
393 * @param {string} $endpoint Endpoint path.
394 * @param {string} $index Elasticsearch index.
395 * @returns {string} Search template API endpoint.
396 */
397 $endpoint = apply_filters( 'ep_instant_results_template_endpoint', "api/v1/search/posts/{$this->index}/template/", $this->index );
398
399 Elasticsearch::factory()->remote_request(
400 $endpoint,
401 [
402 'blocking' => false,
403 'method' => 'DELETE',
404 ]
405 );
406
407 /**
408 * Fires after the request is sent the search template API endpoint.
409 *
410 * @since 4.3.0
411 * @hook ep_instant_results_template_deleted
412 * @param {string} $index Index name.
413 */
414 do_action( 'ep_instant_results_template_deleted', $this->index );
415 }
416
417 /**
418 * Generate a search template.
419 *
420 * A search template is the JSON for an Elasticsearch query with a
421 * placeholder search term. The template is sent to ElasticPress.io where
422 * it's used to make Elasticsearch queries using search terms sent from
423 * the front end.
424 *
425 * @return string The search template as JSON.
426 */
427 public function get_search_template() {
428 $post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types();
429 $post_statuses = get_post_stati(
430 [
431 'public' => true,
432 'exclude_from_search' => false,
433 ]
434 );
435
436 /**
437 * The ID of the current user when generating the Instant Results
438 * search template.
439 *
440 * By default Instant Results sets the current user as anomnymous when
441 * generating the search template, so that any filters applied to
442 * queries for logged-in or specific users are not applied to the
443 * template. This filter supports setting a specific user as the
444 * current user while the template is generated.
445 *
446 * @since 4.1.0
447 * @hook ep_search_template_user_id
448 * @param {int} $user_id User ID to use.
449 * @return {int} New user ID to use.
450 */
451 $template_user_id = apply_filters( 'ep_search_template_user_id', 0 );
452 $original_user_id = get_current_user_id();
453
454 wp_set_current_user( $template_user_id );
455
456 add_filter( 'ep_intercept_remote_request', '__return_true' );
457 add_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10, 4 );
458 add_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10, 2 );
459
460 $query = new \WP_Query(
461 array(
462 'ep_integrate' => true,
463 'ep_search_template' => true,
464 'post_status' => array_values( $post_statuses ),
465 'post_type' => $post_types,
466 's' => '{{ep_placeholder}}',
467 )
468 );
469
470 remove_filter( 'ep_intercept_remote_request', '__return_true' );
471 remove_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10 );
472 remove_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10 );
473
474 wp_set_current_user( $original_user_id );
475
476 return $this->search_template;
477 }
478
479 /**
480 * Return true if a given feature is supported by Instant Results.
481 *
482 * Applied as a filter on Utils\is_integrated_request() so that features
483 * are enabled for the query that is used to generate the search template,
484 * regardless of the request type. This avoids the need to send a request
485 * to the front end.
486 *
487 * @param bool $is_integrated Whether queries for the request will be
488 * integrated.
489 * @param string $context Context for the original check. Usually the
490 * slug of the feature doing the check.
491 * @return bool True if the check is for a feature supported by instant
492 * search.
493 */
494 public function is_integrated_request( $is_integrated, $context ) {
495 $supported_contexts = [
496 'autosuggest',
497 'documents',
498 'search',
499 'weighting',
500 'woocommerce',
501 ];
502
503 return in_array( $context, $supported_contexts, true );
504 }
505
506 /**
507 * Store intercepted request body and return request result.
508 *
509 * @param object $response Response
510 * @param array $query Query
511 * @param array $args WP_Query argument array
512 * @param int $failures Count of failures in request loop
513 * @return object $response Response
514 */
515 public function intercept_search_request( $response, $query = [], $args = [], $failures = 0 ) {
516 $this->search_template = $query['args']['body'];
517
518 return wp_remote_request( $query['url'], $args );
519 }
520
521 /**
522 * Apply product visibility taxonomy query to search template queries.
523 *
524 * @param \WP_Query $query Query instance.
525 * @return void
526 */
527 public function maybe_apply_product_visibility( $query ) {
528 if ( true !== $query->get( 'ep_search_template' ) ) {
529 return;
530 }
531
532 if ( ! $this->is_woocommerce ) {
533 return;
534 }
535
536 $this->apply_product_visibility( $query );
537 }
538
539 /**
540 * Apply product visibility taxonomy query.
541 *
542 * Applies filters to exclude products set to be excluded from search. Out
543 * of stock products will also be excluded if WooCommerce is configured to
544 * hide those products.
545 *
546 * Mimics the logic of WC_Query::get_tax_query().
547 *
548 * @param \WP_Query $query Query instance.
549 * @return void
550 */
551 public function apply_product_visibility( $query ) {
552 $product_visibility_terms = wc_get_product_visibility_term_ids();
553 $product_visibility_not_in = (array) $product_visibility_terms['exclude-from-search'];
554
555 if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
556 $product_visibility_not_in[] = $product_visibility_terms['outofstock'];
557 }
558
559 if ( ! empty( $product_visibility_not_in ) ) {
560 $tax_query = $query->get( 'tax_query', array() );
561
562 $tax_query[] = array(
563 'taxonomy' => 'product_visibility',
564 'field' => 'term_taxonomy_id',
565 'terms' => $product_visibility_not_in,
566 'operator' => 'NOT IN',
567 );
568
569 $query->set( 'tax_query', $tax_query );
570 }
571 }
572
573 /**
574 * Apply aggregation args to search templates.
575 *
576 * @param array $formatted_args Formatted Elasticsearch query.
577 * @param array $query_vars Query variables
578 * @param \WP_Query $query Query instance.
579 * @return array Formatted Elasticsearch query.
580 */
581 public function maybe_apply_aggs_args( $formatted_args, $query_vars, $query ) {
582 if ( true !== $query->get( 'ep_search_template' ) ) {
583 return $formatted_args;
584 }
585
586 return $this->apply_aggs_args( $formatted_args );
587 }
588
589 /**
590 * Add aggregation args to Elasticsearch query for facets.
591 *
592 * @param array $formatted_args Formatted Elasticsearch query.
593 * @return array Formatted Elasticsearch query.
594 */
595 public function apply_aggs_args( $formatted_args ) {
596 $filter = $formatted_args['post_filter'];
597 $facets = $this->get_facets();
598
599 foreach ( $facets as $key => $facet ) {
600 $formatted_args['aggs'][ $key ]['aggs'] = $facet['aggs'];
601
602 if ( $filter ) {
603 $formatted_args['aggs'][ $key ]['filter'] = $filter;
604 }
605 }
606
607 return $formatted_args;
608 }
609
610 /**
611 * Add additional fields to post mapping.
612 *
613 * @param array $mapping Post mapping.
614 * @return array Post mapping.
615 */
616 public function add_mapping_properties( $mapping ) {
617 $elasticsearch_version = Elasticsearch::factory()->get_elasticsearch_version();
618
619 $properties = array(
620 'post_content_plain' => array( 'type' => 'text' ),
621 'price_html' => array( 'type' => 'text' ),
622 );
623
624 if ( version_compare( $elasticsearch_version, '7.0', '<' ) ) {
625 $mapping['mappings']['post']['properties'] = array_merge(
626 $mapping['mappings']['post']['properties'],
627 $properties
628 );
629 } else {
630 $mapping['mappings']['properties'] = array_merge(
631 $mapping['mappings']['properties'],
632 $properties
633 );
634 }
635
636 return $mapping;
637 }
638
639 /**
640 * Add data for additional mapping properties.
641 *
642 * @param array $post_args Post arguments.
643 * @param integer $post_id Post ID.
644 * @return array Post sync args.
645 */
646 public function add_post_sync_args( $post_args, $post_id ) {
647 $post = get_post( $post_id );
648
649 $post_args['post_content_plain'] = $this->prepare_plain_content_arg( $post );
650 $post_args['price_html'] = $this->prepare_price_html_arg( $post );
651
652 return $post_args;
653 }
654
655
656 /**
657 * Get data for the plain post content.
658 *
659 * @param WP_Post $post Post object.
660 * @return string Post content.
661 */
662 public function prepare_plain_content_arg( $post ) {
663 $post_content = apply_filters( 'the_content', $post->post_content );
664
665 return wp_strip_all_tags( $post_content );
666 }
667
668 /**
669 * Get data for the price HTML arg.
670 *
671 * @param WP_Post $post Post object.
672 * @return string|null Price HTML.
673 */
674 public function prepare_price_html_arg( $post ) {
675 if ( 'product' !== $post->post_type ) {
676 return null;
677 }
678
679 if ( ! $this->is_woocommerce ) {
680 return null;
681 }
682
683 $product = wc_get_product( $post );
684
685 return $product->get_price_html();
686 }
687
688 /**
689 * Get post type labels.
690 *
691 * Only the post type slug is indexed, so we'll need the labels on the
692 * front end for display.
693 *
694 * @return array Array of post types and their labels.
695 */
696 public function get_post_type_labels() {
697 $labels = [];
698
699 $post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types();
700
701 foreach ( $post_types as $post_type ) {
702 $post_type_object = get_post_type_object( $post_type );
703 $post_type_labels = get_post_type_labels( $post_type_object );
704
705 $labels[ $post_type ] = array(
706 'plural' => $post_type_labels->name,
707 'singular' => $post_type_labels->singular_name,
708 );
709 }
710
711 return $labels;
712 }
713
714 /**
715 * Get available facets.
716 *
717 * @return array Available facets.
718 */
719 public function get_facets() {
720 $facets = [];
721
722 /**
723 * Post type facet.
724 */
725 $facets['post_type'] = array(
726 'type' => 'post_type',
727 'post_types' => [],
728 'labels' => array(
729 'admin' => __( 'Post type', 'elasticpress' ),
730 'frontend' => __( 'Type', 'elasticpress' ),
731 ),
732 'aggs' => array(
733 'post_type' => array(
734 'terms' => array(
735 'field' => 'post_type.raw',
736 ),
737 ),
738 ),
739 /**
740 * The post_type arg needs to be supported regardless of whether
741 * the Post Type facet is present to be able to support setting the
742 * post type from the search form.
743 *
744 * @see ElasticPress\Feature\InstantResults::get_args_schema()
745 */
746 'args' => array(),
747 );
748
749 /**
750 * Taxonomy facets.
751 */
752 $taxonomies = get_taxonomies( array( 'public' => true ), 'object' );
753 $taxonomies = apply_filters( 'ep_facet_include_taxonomies', $taxonomies );
754
755 foreach ( $taxonomies as $slug => $taxonomy ) {
756 $name = 'tax-' . $slug;
757 $labels = get_taxonomy_labels( $taxonomy );
758
759 $admin_label = sprintf(
760 /* translators: $1$s: Taxonomy name. %2$s: Taxonomy slug. */
761 esc_html__( '%1$s (%2$s)' ),
762 $labels->singular_name,
763 $slug
764 );
765
766 $facets[ $name ] = array(
767 'type' => 'taxonomy',
768 'post_types' => $taxonomy->object_type,
769 'labels' => array(
770 'admin' => $admin_label,
771 'frontend' => $labels->singular_name,
772 ),
773 'aggs' => array(
774 $name => array(
775 'terms' => array(
776 'field' => 'terms.' . $slug . '.facet',
777 'size' => apply_filters( 'ep_facet_taxonomies_size', 10000, $taxonomy ),
778 ),
779 ),
780 ),
781 'args' => array(
782 $name => array(
783 'type' => 'strings',
784 ),
785 ),
786 );
787 }
788
789 /**
790 * Price facet.
791 */
792 if ( $this->is_woocommerce ) {
793 $facets['price_range'] = array(
794 'type' => 'price_range',
795 'post_types' => [ 'product' ],
796 'labels' => array(
797 'admin' => __( 'Price range', 'elasticpress' ),
798 'frontend' => __( 'Price', 'elasticpress' ),
799 ),
800 'aggs' => array(
801 'max_price' => array(
802 'max' => array(
803 'field' => 'meta._price.double',
804 ),
805 ),
806 'min_price' => array(
807 'min' => array(
808 'field' => 'meta._price.double',
809 ),
810 ),
811 ),
812 'args' => array(
813 'max_price' => array(
814 'type' => 'number',
815 ),
816 'min_price' => array(
817 'type' => 'number',
818 ),
819 ),
820 );
821 }
822
823 return $facets;
824 }
825
826 /**
827 * Get facet configuration for the front end.
828 *
829 * @return Array Facet configuration for the front end.
830 */
831 public function get_facets_for_frontend() {
832 $selected_facets = explode( ',', $this->settings['facets'] );
833 $available_facets = $this->get_facets();
834
835 $facets = [];
836
837 foreach ( $selected_facets as $key ) {
838 if ( isset( $available_facets[ $key ] ) ) {
839 $facet = $available_facets[ $key ];
840
841 $facets[] = array(
842 'name' => $key,
843 'label' => $facet['labels']['frontend'],
844 'type' => $facet['type'],
845 'postTypes' => $facet['post_types'],
846 );
847 }
848 }
849
850 return $facets;
851 }
852
853 /**
854 * Get facet configuration for the admin.
855 *
856 * @return Array Facet configuration for the admin.
857 */
858 public function get_facets_for_admin() {
859 $available_facets = $this->get_facets();
860
861 $facets = [];
862
863 foreach ( $available_facets as $key => $facet ) {
864 $facets[ $key ] = array(
865 'label' => $facet['labels']['admin'],
866 );
867 }
868
869 return $facets;
870 }
871
872 /**
873 * Get schema for search args.
874 *
875 * @return array Search args schema.
876 */
877 public function get_args_schema() {
878 $args = array(
879 'highlight' => array(
880 'type' => 'string',
881 'default' => $this->settings['highlight_tag'],
882 'allowedValues' => [ $this->settings['highlight_tag'] ],
883 ),
884 'offset' => array(
885 'type' => 'number',
886 'default' => 0,
887 ),
888 'orderby' => array(
889 'type' => 'string',
890 'default' => 'relevance',
891 'allowedValues' => [ 'date', 'price', 'relevance' ],
892 ),
893 'order' => array(
894 'type' => 'string',
895 'default' => 'desc',
896 'allowedValues' => [ 'asc', 'desc' ],
897 ),
898 'per_page' => array(
899 'type' => 'number',
900 'default' => 6,
901 ),
902 'post_type' => array(
903 'type' => 'strings',
904 ),
905 'search' => array(
906 'type' => 'string',
907 'default' => '',
908 ),
909 'relation' => array(
910 'type' => 'string',
911 'default' => 'and',
912 'allowedValues' => [ 'and', 'or' ],
913 ),
914 );
915
916 $selected_facets = explode( ',', $this->settings['facets'] );
917 $available_facets = $this->get_facets();
918
919 foreach ( $selected_facets as $key ) {
920 if ( isset( $available_facets[ $key ] ) ) {
921 $args = array_merge( $args, $available_facets[ $key ]['args'] );
922 }
923 }
924 return $args;
925 }
926 }
927