PluginProbe
ElasticPress / 4.4.0
ElasticPress v4.4.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.4.0, at includes/classes/Feature/InstantResults/InstantResults.php

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