PluginProbe
ElasticPress / 4.3.1
ElasticPress v4.3.1
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 / Search / Search.php

Search.php in ElasticPress 4.3.1, at includes/classes/Feature/Search/Search.php

680 lines 20.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Search feature
4 *
5 * @since 1.9
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Feature\Search;
10
11 use ElasticPress\Feature as Feature;
12 use ElasticPress\Indexables as Indexables;
13 use ElasticPress\Utils as Utils;
14
15 /**
16 * Search feature class
17 */
18 class Search extends Feature {
19 /**
20 * Synonyms Class (Sub Feature)
21 *
22 * @var Synonyms
23 */
24 public $synonyms;
25
26 /**
27 * Weighting Class (Sub Feature)
28 *
29 * @var Weighting
30 */
31 public $weighting;
32
33 /**
34 * Highlighting tags
35 *
36 * @var array
37 */
38 public static $default_highlight_tags = [
39 'mark',
40 'span',
41 'strong',
42 'em',
43 'i',
44 ];
45
46 /**
47 * Initialize feature setting it's config
48 *
49 * @since 3.0
50 */
51 public function __construct() {
52 $this->slug = 'search';
53
54 $this->title = esc_html__( 'Post Search', 'elasticpress' );
55
56 $this->summary = __( 'Instantly find the content you’re looking for. The first time.', 'elasticpress' );
57
58 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#post-search', 'elasticpress' );
59
60 $this->requires_install_reindex = false;
61
62 $this->default_settings = [
63 'decaying_enabled' => '1',
64 'synonyms_editor_mode' => 'simple',
65 'highlight_enabled' => '0',
66 'highlight_excerpt' => '0',
67 'highlight_tag' => 'mark',
68 ];
69
70 $this->available_during_installation = true;
71
72 parent::__construct();
73 }
74
75 /**
76 * We need to delay search setup up since it will fire after protected content and protected
77 * content filters into the search setup
78 *
79 * @since 2.2
80 */
81 public function setup() {
82 add_action( 'init', [ $this, 'search_setup' ] );
83 add_filter( 'ep_sanitize_feature_settings', [ $this, 'sanitize_highlighting_settings' ] );
84
85 // Set up weighting sub-module
86 $this->weighting = new Weighting();
87 $this->weighting->setup();
88
89 $this->synonyms = new Synonyms();
90 $this->synonyms->setup();
91 }
92
93 /**
94 * Setup feature on each page load
95 *
96 * @since 3.0
97 */
98 public function search_setup() {
99 add_filter( 'ep_elasticpress_enabled', [ $this, 'integrate_search_queries' ], 10, 2 );
100 add_filter( 'ep_formatted_args', [ $this, 'weight_recent' ], 11, 2 );
101 add_filter( 'ep_query_post_type', [ $this, 'filter_query_post_type_for_search' ], 10, 2 );
102
103 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
104 add_filter( 'ep_formatted_args', [ $this, 'add_search_highlight_tags' ], 10, 2 );
105 add_filter( 'ep_highlighting_tag', [ $this, 'get_highlighting_tag' ] );
106 add_action( 'ep_highlighting_pre_add_highlight', [ $this, 'allow_excerpt_html' ] );
107 }
108
109
110 /**
111 * Enqueue styles for highlighting.
112 */
113 public function enqueue_scripts() {
114 $settings = $this->get_settings();
115
116 if ( ! $settings ) {
117 $settings = [];
118 }
119
120 $settings = wp_parse_args( $settings, $this->default_settings );
121
122 if ( true !== $settings['highlight_enabled'] ) {
123 return;
124 }
125
126 wp_enqueue_style(
127 'searchterm-highlighting',
128 EP_URL . 'dist/css/highlighting-styles.min.css',
129 Utils\get_asset_info( 'highlighting-styles', 'dependencies' ),
130 Utils\get_asset_info( 'highlighting-styles', 'version' )
131 );
132 }
133
134 /**
135 * Set default fields to highlight, and outputs
136 * the tags on the front end.
137 *
138 * @param array $formatted_args ep_formatted_args array
139 * @param array $args WP_Query args
140 * @return array $formatted_args formatted args with search highlight tags
141 */
142 public function add_search_highlight_tags( $formatted_args, $args ) {
143
144 /**
145 * Fires before the highlighting clause is added to the Elasticsearch query
146 *
147 * @since 3.5.1
148 * @hook ep_highlighting_pre_add_highlight
149 * @param {array} $formatted_args ep_formatted_args array
150 * @param {string} $args WP_Query args
151 */
152 do_action( 'ep_highlighting_pre_add_highlight', $formatted_args, $args );
153
154 // get current config
155 $settings = $this->get_settings();
156
157 if ( ! $settings ) {
158 $settings = [];
159 }
160
161 $settings = wp_parse_args( $settings, $this->default_settings );
162
163 if ( true !== $settings['highlight_enabled'] ) {
164 return $formatted_args;
165 }
166
167 if ( empty( $args['s'] ) ) {
168 return $formatted_args;
169 }
170
171 /**
172 * Filter whether to add the `highlight` clause in the query or not.
173 *
174 * @since 3.5.6
175 * @hook ep_highlight_should_add_clause
176 * @param {bool} $add_highlight_clause True means the clause should be added.
177 * @param {array} $formatted_args ep_formatted_args array
178 * @param {array} $args WP query args
179 * @return {bool} New $add_highlight_clause value
180 */
181 $add_highlight_clause = apply_filters(
182 'ep_highlight_should_add_clause',
183 Utils\is_integrated_request( 'highlighting', [ 'public' ] ),
184 $formatted_args,
185 $args
186 );
187
188 if ( ! $add_highlight_clause ) {
189 return $formatted_args;
190 }
191
192 /**
193 * Filter the fields that should be highlighted.
194 *
195 * @since 3.5.1
196 * @hook ep_highlighting_fields
197 * @param {array} $fields Highlighting fields
198 * @param {array} $formatted_args array
199 * @param {array} $args WP_Query args
200 * @return {array} New Highlighting fields
201 */
202 $fields_to_highlight = apply_filters(
203 'ep_highlighting_fields',
204 [ 'post_title', 'post_content' ],
205 $formatted_args,
206 $args
207 );
208
209 // define the tag to use
210 $current_tag = $settings['highlight_tag'];
211
212 /**
213 * Filter the tag that wraps the search highlighted term
214 *
215 * @since 3.5
216 * @hook ep_highlighting_tag
217 * @param {string} $current_tag Highlighting tag
218 * @return {string} New highlighting tag
219 */
220 $highlight_tag = apply_filters( 'ep_highlighting_tag', $current_tag );
221
222 /**
223 * Filter class applied to search highlight tags
224 *
225 * @since 3.5
226 * @hook ep_highlighting_class
227 * @param {string} $class Highlighting class
228 * @return {string} New highlighting class
229 */
230 $highlight_class = apply_filters( 'ep_highlighting_class', 'ep-highlight' );
231
232 // tags
233 $opening_tag = '<' . $highlight_tag . " class='" . $highlight_class . "'>";
234 $closing_tag = '</' . $highlight_tag . '>';
235
236 foreach ( $fields_to_highlight as $field ) {
237 $formatted_args['highlight']['fields'][ $field ] = [
238 'pre_tags' => [ $opening_tag ],
239 'post_tags' => [ $closing_tag ],
240 'type' => 'plain',
241 'number_of_fragments' => 0,
242 ];
243 }
244
245 return $formatted_args;
246 }
247
248 /**
249 * Called by ep_highlighting_pre_add_highlight action.
250 *
251 * Replaces the default excerpt with the custom excerpt, allowing
252 * for the selected tag to be displayed in it.
253 */
254 public function allow_excerpt_html() {
255 if ( ! Utils\is_integrated_request( 'highlighting', [ 'public' ] ) ) {
256 return;
257 }
258
259 if ( empty( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
260 return;
261 }
262
263 $settings = $this->get_settings();
264
265 if ( ! $settings ) {
266 $settings = [];
267 }
268
269 $settings = wp_parse_args( $settings, $this->default_settings );
270
271 if ( ! empty( $settings['highlight_excerpt'] ) && true === $settings['highlight_excerpt'] ) {
272 remove_filter( 'get_the_excerpt', 'wp_trim_excerpt' );
273 add_filter( 'get_the_excerpt', [ $this, 'ep_highlight_excerpt' ] );
274 add_filter( 'ep_highlighting_fields', [ $this, 'ep_highlight_add_excerpt_field' ] );
275 }
276 }
277
278 /**
279 * Called by allow_excerpt_html
280 * logic for the excerpt filter allowing the currently selected tag.
281 *
282 * @param string $text - excerpt string
283 * @return string $text - the new excerpt
284 */
285 public function ep_highlight_excerpt( $text ) {
286
287 $settings = $this->get_settings();
288
289 if ( ! $settings ) {
290 $settings = [];
291 }
292
293 $settings = wp_parse_args( $settings, $this->default_settings );
294
295 // reproduces wp_trim_excerpt filter, preserving the excerpt_more and excerpt_length filters
296 if ( '' === $text ) {
297 $text = get_the_content( '' );
298 $text = apply_filters( 'the_content', $text );
299 $text = str_replace( '\]\]\>', ']]&gt;', $text );
300 $text = strip_tags( $text, '<' . esc_html( $settings['highlight_tag'] ) . '>' );
301
302 // use the defined length, if already applied...
303 $excerpt_length = apply_filters( 'excerpt_length', 55 );
304
305 // use defined excerpt_more filter if it is used
306 $excerpt_more = apply_filters( 'excerpt_more', $text );
307
308 $excerpt_more = $excerpt_more !== $text ? $excerpt_more : '[&hellip;]';
309
310 $words = explode( ' ', $text, $excerpt_length + 1 );
311 if ( count( $words ) > $excerpt_length ) {
312 array_pop( $words );
313 array_push( $words, $excerpt_more );
314 $text = implode( ' ', $words );
315 }
316 }
317
318 return $text;
319 }
320
321 /**
322 * Add `post_content` to the list of fields to highlight.
323 *
324 * @since 3.5.1
325 * @param array $fields_to_highlight The list of fields to highlight.
326 * @return array
327 */
328 public function ep_highlight_add_excerpt_field( $fields_to_highlight ) {
329 $fields_to_highlight[] = 'post_excerpt';
330 return $fields_to_highlight;
331 }
332
333 /**
334 * Helper filter to check if the tag is allowed.
335 *
336 * @param string $tag - html tag
337 * @return string
338 */
339 public function get_highlighting_tag( $tag ) {
340 if ( ! in_array( $tag, self::$default_highlight_tags, true ) ) {
341 $tag = 'mark';
342 }
343
344 return $tag;
345 }
346
347 /**
348 * Sanitizes our highlighting settings.
349 *
350 * @param array $settings Array of current settings
351 * @return mixed
352 */
353 public function sanitize_highlighting_settings( $settings ) {
354 if ( ! empty( $settings['search']['highlight_excerpt'] ) ) {
355 $settings['search']['highlight_excerpt'] = (bool) $settings['search']['highlight_excerpt'];
356 }
357
358 if ( ! empty( $settings['search']['highlight_enabled'] ) ) {
359 $settings['search']['highlight_enabled'] = (bool) $settings['search']['highlight_enabled'];
360 }
361
362 return $settings;
363 }
364
365 /**
366 * Returns searchable post types for the current site
367 *
368 * @since 1.9
369 * @return mixed|void
370 */
371 public function get_searchable_post_types() {
372 $post_types = get_post_types( array( 'exclude_from_search' => false ) );
373
374 /**
375 * Don't search attachments by default
376 *
377 * @since 3.0
378 */
379 unset( $post_types['attachment'] );
380
381 /**
382 * Filter searchable post types
383 *
384 * @hook ep_searchable_post_types
385 * @param {array} $post_types Post types
386 * @return {array} New post types
387 */
388 return apply_filters( 'ep_searchable_post_types', $post_types );
389 }
390
391 /**
392 * Make sure we don't search for "any" on a search query
393 *
394 * @param string $post_type Post type
395 * @param WP_Query $query WP Query
396 * @return string|array
397 */
398 public function filter_query_post_type_for_search( $post_type, $query ) {
399 if ( 'any' === $post_type && $query->is_search() ) {
400 $searchable_post_types = $this->get_searchable_post_types();
401
402 // If we have no searchable post types, there's no point going any further
403 if ( empty( $searchable_post_types ) ) {
404
405 // Have to return something or it improperly calculates the found_posts
406 return false;
407 }
408
409 // Conform the post types array to an acceptable format for ES
410 $post_types = [];
411
412 foreach ( $searchable_post_types as $type ) {
413 $post_types[] = $type;
414 }
415
416 // These are now the only post types we will search
417 $post_type = $post_types;
418 }
419
420 return $post_type;
421 }
422
423 /**
424 * Returns true/false if decaying is/isn't enabled
425 *
426 * @return bool
427 */
428 public function is_decaying_enabled() {
429 $settings = $this->get_settings();
430
431 $settings = wp_parse_args(
432 $settings,
433 [
434 'decaying_enabled' => true,
435 ]
436 );
437
438 return (bool) $settings['decaying_enabled'];
439 }
440
441 /**
442 * Weight more recent content in searches
443 *
444 * @param array $formatted_args Formatted ES args
445 * @param array $args WP_Query args
446 * @since 2.1
447 * @return array
448 */
449 public function weight_recent( $formatted_args, $args ) {
450 if ( empty( $args['s'] ) ) {
451 return $formatted_args;
452 }
453 if ( ! $this->is_decaying_enabled() ) {
454 return $formatted_args;
455 }
456 /**
457 * Filter search date weighting scale
458 *
459 * @hook epwr_decay_function
460 * @param {string} $decay_function Current decay function
461 * @param {array} $formatted_args Formatted Elasticsearch arguments
462 * @param {array} $args WP_Query arguments
463 * @return {string} New decay function
464 */
465 $decay_function = apply_filters( 'epwr_decay_function', 'exp', $formatted_args, $args );
466 /**
467 * Filter search date weighting field
468 *
469 * @hook epwr_decay_field
470 * @param {string} $field Current decay field
471 * @param {array} $formatted_args Formatted Elasticsearch arguments
472 * @param {array} $args WP_Query arguments
473 * @return {string} New decay field
474 * @since 4.3.0
475 */
476 $field = apply_filters( 'epwr_decay_field', 'post_date_gmt', $formatted_args, $args );
477 $date_score = array(
478 'function_score' => array(
479 'query' => $formatted_args['query'],
480 'functions' => array(
481 array(
482 $decay_function => array(
483 $field => array(
484 /**
485 * Filter search date weighting scale
486 *
487 * @hook epwr_scale
488 * @param {string} $scale Current scale
489 * @param {array} $formatted_args Formatted Elasticsearch arguments
490 * @param {array} $args WP_Query arguments
491 * @return {string} New scale
492 */
493 'scale' => apply_filters( 'epwr_scale', '14d', $formatted_args, $args ),
494 /**
495 * Filter search date weighting decay
496 *
497 * @hook epwr_decay
498 * @param {float} $decay Current decay
499 * @param {array} $formatted_args Formatted Elasticsearch arguments
500 * @param {array} $args WP_Query arguments
501 * @return {float} New decay
502 */
503 'decay' => apply_filters( 'epwr_decay', 0.25, $formatted_args, $args ),
504 /**
505 * Filter search date weighting offset
506 *
507 * @hook epwr_offset
508 * @param {string} $offset Current offset
509 * @param {array} $formatted_args Formatted Elasticsearch arguments
510 * @param {array} $args WP_Query arguments
511 * @return {string} New offset
512 */
513 'offset' => apply_filters( 'epwr_offset', '7d', $formatted_args, $args ),
514 ),
515 ),
516 ),
517 array(
518 /**
519 * Filter search date weight
520 *
521 * @since 3.5.6
522 * @hook epwr_weight
523 * @param {float} $weight Current weight
524 * @param {array} $formatted_args Formatted Elasticsearch arguments
525 * @param {array} $args WP_Query arguments
526 * @return {float} New weight
527 */
528 'weight' => apply_filters( 'epwr_weight', 0.001, $formatted_args, $args ),
529 ),
530 ),
531 /**
532 * Filter search date weighting score mode
533 *
534 * @hook epwr_score_mode
535 * @param {string} $score_mode Current score mode
536 * @param {array} $formatted_args Formatted Elasticsearch arguments
537 * @param {array} $args WP_Query arguments
538 * @return {string} New score mode
539 */
540 'score_mode' => apply_filters( 'epwr_score_mode', 'sum', $formatted_args, $args ),
541 /**
542 * Filter search date weighting boost mode
543 *
544 * @hook epwr_boost_mode
545 * @param {string} $boost_mode Current boost mode
546 * @param {array} $formatted_args Formatted Elasticsearch arguments
547 * @param {array} $args WP_Query arguments
548 * @return {string} New boost mode
549 */
550 'boost_mode' => apply_filters( 'epwr_boost_mode', 'multiply', $formatted_args, $args ),
551 ),
552 );
553
554 $formatted_args['query'] = $date_score;
555
556 return $formatted_args;
557 }
558
559 /**
560 * Output feature box long text
561 *
562 * @since 3.0
563 */
564 public function output_feature_box_long() {
565 ?>
566 <p><?php esc_html_e( 'Overcome higher-end performance and functional limits posed by the traditional WordPress structured (SQL) database to deliver superior keyword search, instantly. ElasticPress indexes custom fields, tags, and other metadata to improve search results. Fuzzy matching accounts for misspellings and verb tenses.', 'elasticpress' ); ?></p>
567
568 <?php
569 }
570
571 /**
572 * Enable integration on search queries
573 *
574 * @param bool $enabled Original enabled value
575 * @param WP_Query $query WP Query
576 * @since 2.1
577 * @return bool
578 */
579 public function integrate_search_queries( $enabled, $query ) {
580 if ( ! Utils\is_integrated_request( $this->slug ) ) {
581 return false;
582 }
583
584 if ( ! is_a( $query, 'WP_Query' ) ) {
585 return $enabled;
586 }
587
588 if ( isset( $query->query_vars['ep_integrate'] ) && ! filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ) ) {
589 return false;
590 }
591
592 if ( method_exists( $query, 'is_search' ) && $query->is_search() && ! empty( $query->query_vars['s'] ) ) {
593 $enabled = true;
594
595 /**
596 * WordPress have to be version 4.6 or newer to have "fields" support
597 * since it requires the "posts_pre_query" filter.
598 *
599 * @see WP_Query::get_posts
600 */
601 $fields = $query->get( 'fields' );
602 if ( ! version_compare( get_bloginfo( 'version' ), '4.6', '>=' ) && ! empty( $fields ) ) {
603 $enabled = false;
604 }
605 }
606
607 /**
608 * Filter whether to enable integration on search queries or not.
609 *
610 * @hook ep_integrate_search_queries
611 * @since 4.2.0
612 * @param {bool} $enabled Original enabled value
613 * @param {WP_Query} $query WP_Query
614 * @return {bool} New $enabled value
615 */
616 return apply_filters( 'ep_integrate_search_queries', $enabled, $query );
617 }
618
619 /**
620 * Display decaying settings on dashboard.
621 *
622 * @since 2.4
623 */
624 public function output_feature_box_settings() {
625 $settings = $this->get_settings();
626
627 if ( ! $settings ) {
628 $settings = [];
629 }
630
631 $settings = wp_parse_args( $settings, $this->default_settings );
632
633 ?>
634 <div class="field">
635 <div class="field-name status"><?php esc_html_e( 'Weight results by date', 'elasticpress' ); ?></div>
636 <div class="input-wrap">
637 <label><input name="settings[decaying_enabled]" type="radio" <?php checked( (bool) $settings['decaying_enabled'] ); ?> value="1"><?php esc_html_e( 'Enabled', 'elasticpress' ); ?></label><br>
638 <label><input name="settings[decaying_enabled]" type="radio" <?php checked( ! (bool) $settings['decaying_enabled'] ); ?> value="0"><?php esc_html_e( 'Disabled', 'elasticpress' ); ?></label>
639 </div>
640 </div>
641 <div class="field">
642 <div class="field-name status"><?php esc_html_e( 'Highlighting status', 'elasticpress' ); ?></div>
643 <div class="input-wrap">
644 <label><input name="settings[highlight_enabled]" type="radio" <?php checked( (bool) $settings['highlight_enabled'] ); ?> value="1"><?php esc_html_e( 'Enabled', 'elasticpress' ); ?></label><br>
645 <label><input name="settings[highlight_enabled]" type="radio" <?php checked( ! (bool) $settings['highlight_enabled'] ); ?> value="0"><?php esc_html_e( 'Disabled', 'elasticpress' ); ?></label>
646 <p class="field-description"><?php esc_html_e( 'Wrap search terms in HTML tags in results for custom styling. The wrapping HTML tag comes with the "ep-highlight" class for easy styling.' ); ?></p>
647 </div>
648 </div>
649 <div class="field">
650 <label for="highlight-tag" class="field-name status"><?php echo esc_html_e( 'Highlight tag ', 'elasticpress' ); ?></label>
651 <div class="input-wrap">
652 <select id="highlight-tag" name="settings[highlight_tag]">
653 <?php
654 foreach ( self::$default_highlight_tags as $option ) :
655 echo '<option value="' . esc_attr( $option ) . '" ' . selected( $option, $settings['highlight_tag'] ) . '>' . esc_html( $option ) . '</option>';
656 endforeach;
657 ?>
658 </select>
659 </div>
660 </div>
661
662 <div class="field">
663 <div class="field-name status"><?php esc_html_e( 'Excerpt highlighting', 'elasticpress' ); ?></div>
664 <div class="input-wrap">
665 <label><input name="settings[highlight_excerpt]" type="radio" <?php checked( (bool) $settings['highlight_excerpt'] ); ?> value="1"><?php esc_html_e( 'Enabled', 'elasticpress' ); ?></label><br>
666 <label><input name="settings[highlight_excerpt]" type="radio" <?php checked( ! (bool) $settings['highlight_excerpt'] ); ?> value="0"><?php esc_html_e( 'Disabled', 'elasticpress' ); ?></label>
667 <p class="field-description"><?php esc_html_e( 'By default, WordPress strips HTML from content excerpts. Enable when using the_excerpt() to display search results. ', 'elasticpress' ); ?></p>
668 </div>
669 </div>
670
671 <?php if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) : ?>
672 <br class="clear">
673 <p><a href="<?php echo esc_url( admin_url( 'admin.php?page=elasticpress-weighting' ) ); ?>"><?php esc_html_e( 'Advanced fields and weighting settings', 'elasticpress' ); ?></a></p>
674 <p><a href="<?php echo esc_url( admin_url( 'admin.php?page=elasticpress-synonyms' ) ); ?>"><?php esc_html_e( 'Add synonyms to your post searches', 'elasticpress' ); ?></a></p>
675 <?php endif; ?>
676
677 <?php
678 }
679 }
680