PluginProbe
ElasticPress / 4.2.0
ElasticPress v4.2.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 / Search / Search.php

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

667 lines 19.8 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 ( is_admin() ) {
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 if ( $this->is_decaying_enabled() ) {
452 /**
453 * Filter search date weighting scale
454 *
455 * @hook epwr_decay_function
456 * @param {string} $decay_function Current decay function
457 * @param {array} $formatted_args Formatted Elasticsearch arguments
458 * @param {array} $args WP_Query arguments
459 * @return {string} New decay function
460 */
461 $decay_function = apply_filters( 'epwr_decay_function', 'exp', $formatted_args, $args );
462 $date_score = array(
463 'function_score' => array(
464 'query' => $formatted_args['query'],
465 'functions' => array(
466 array(
467 $decay_function => array(
468 'post_date_gmt' => array(
469 /**
470 * Filter search date weighting scale
471 *
472 * @hook epwr_scale
473 * @param {string} $scale Current scale
474 * @param {array} $formatted_args Formatted Elasticsearch arguments
475 * @param {array} $args WP_Query arguments
476 * @return {string} New scale
477 */
478 'scale' => apply_filters( 'epwr_scale', '14d', $formatted_args, $args ),
479 /**
480 * Filter search date weighting decay
481 *
482 * @hook epwr_decay
483 * @param {float} $decay Current decay
484 * @param {array} $formatted_args Formatted Elasticsearch arguments
485 * @param {array} $args WP_Query arguments
486 * @return {float} New decay
487 */
488 'decay' => apply_filters( 'epwr_decay', 0.25, $formatted_args, $args ),
489 /**
490 * Filter search date weighting offset
491 *
492 * @hook epwr_offset
493 * @param {string} $offset Current offset
494 * @param {array} $formatted_args Formatted Elasticsearch arguments
495 * @param {array} $args WP_Query arguments
496 * @return {string} New offset
497 */
498 'offset' => apply_filters( 'epwr_offset', '7d', $formatted_args, $args ),
499 ),
500 ),
501 ),
502 array(
503 /**
504 * Filter search date weight
505 *
506 * @since 3.5.6
507 * @hook epwr_weight
508 * @param {float} $weight Current weight
509 * @param {array} $formatted_args Formatted Elasticsearch arguments
510 * @param {array} $args WP_Query arguments
511 * @return {float} New weight
512 */
513 'weight' => apply_filters( 'epwr_weight', 0.001, $formatted_args, $args ),
514 ),
515 ),
516 /**
517 * Filter search date weighting score mode
518 *
519 * @hook epwr_score_mode
520 * @param {string} $score_mode Current score mode
521 * @param {array} $formatted_args Formatted Elasticsearch arguments
522 * @param {array} $args WP_Query arguments
523 * @return {string} New score mode
524 */
525 'score_mode' => apply_filters( 'epwr_score_mode', 'sum', $formatted_args, $args ),
526 /**
527 * Filter search date weighting boost mode
528 *
529 * @hook epwr_boost_mode
530 * @param {string} $boost_mode Current boost mode
531 * @param {array} $formatted_args Formatted Elasticsearch arguments
532 * @param {array} $args WP_Query arguments
533 * @return {string} New boost mode
534 */
535 'boost_mode' => apply_filters( 'epwr_boost_mode', 'multiply', $formatted_args, $args ),
536 ),
537 );
538
539 $formatted_args['query'] = $date_score;
540 }
541 }
542
543 return $formatted_args;
544 }
545
546 /**
547 * Output feature box long text
548 *
549 * @since 3.0
550 */
551 public function output_feature_box_long() {
552 ?>
553 <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>
554
555 <?php
556 }
557
558 /**
559 * Enable integration on search queries
560 *
561 * @param bool $enabled Original enabled value
562 * @param WP_Query $query WP Query
563 * @since 2.1
564 * @return bool
565 */
566 public function integrate_search_queries( $enabled, $query ) {
567 if ( ! Utils\is_integrated_request( $this->slug ) ) {
568 return false;
569 }
570
571 if ( ! is_a( $query, 'WP_Query' ) ) {
572 return $enabled;
573 }
574
575 if ( isset( $query->query_vars['ep_integrate'] ) && ! filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ) ) {
576 return false;
577 }
578
579 if ( method_exists( $query, 'is_search' ) && $query->is_search() && ! empty( $query->query_vars['s'] ) ) {
580 $enabled = true;
581
582 /**
583 * WordPress have to be version 4.6 or newer to have "fields" support
584 * since it requires the "posts_pre_query" filter.
585 *
586 * @see WP_Query::get_posts
587 */
588 $fields = $query->get( 'fields' );
589 if ( ! version_compare( get_bloginfo( 'version' ), '4.6', '>=' ) && ! empty( $fields ) ) {
590 $enabled = false;
591 }
592 }
593
594 /**
595 * Filter whether to enable integration on search queries or not.
596 *
597 * @hook ep_integrate_search_queries
598 * @since 4.2.0
599 * @param {bool} $enabled Original enabled value
600 * @param {WP_Query} $query WP_Query
601 * @return {bool} New $enabled value
602 */
603 return apply_filters( 'ep_integrate_search_queries', $enabled, $query );
604 }
605
606 /**
607 * Display decaying settings on dashboard.
608 *
609 * @since 2.4
610 */
611 public function output_feature_box_settings() {
612 $settings = $this->get_settings();
613
614 if ( ! $settings ) {
615 $settings = [];
616 }
617
618 $settings = wp_parse_args( $settings, $this->default_settings );
619
620 ?>
621 <div class="field">
622 <div class="field-name status"><?php esc_html_e( 'Weight results by date', 'elasticpress' ); ?></div>
623 <div class="input-wrap">
624 <label><input name="settings[decaying_enabled]" type="radio" <?php checked( (bool) $settings['decaying_enabled'] ); ?> value="1"><?php esc_html_e( 'Enabled', 'elasticpress' ); ?></label><br>
625 <label><input name="settings[decaying_enabled]" type="radio" <?php checked( ! (bool) $settings['decaying_enabled'] ); ?> value="0"><?php esc_html_e( 'Disabled', 'elasticpress' ); ?></label>
626 </div>
627 </div>
628 <div class="field">
629 <div class="field-name status"><?php esc_html_e( 'Highlighting status', 'elasticpress' ); ?></div>
630 <div class="input-wrap">
631 <label><input name="settings[highlight_enabled]" type="radio" <?php checked( (bool) $settings['highlight_enabled'] ); ?> value="1"><?php esc_html_e( 'Enabled', 'elasticpress' ); ?></label><br>
632 <label><input name="settings[highlight_enabled]" type="radio" <?php checked( ! (bool) $settings['highlight_enabled'] ); ?> value="0"><?php esc_html_e( 'Disabled', 'elasticpress' ); ?></label>
633 <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>
634 </div>
635 </div>
636 <div class="field">
637 <label for="highlight-tag" class="field-name status"><?php echo esc_html_e( 'Highlight tag ', 'elasticpress' ); ?></label>
638 <div class="input-wrap">
639 <select id="highlight-tag" name="settings[highlight_tag]">
640 <?php
641 foreach ( self::$default_highlight_tags as $option ) :
642 echo '<option value="' . esc_attr( $option ) . '" ' . selected( $option, $settings['highlight_tag'] ) . '>' . esc_html( $option ) . '</option>';
643 endforeach;
644 ?>
645 </select>
646 </div>
647 </div>
648
649 <div class="field">
650 <div class="field-name status"><?php esc_html_e( 'Excerpt highlighting', 'elasticpress' ); ?></div>
651 <div class="input-wrap">
652 <label><input name="settings[highlight_excerpt]" type="radio" <?php checked( (bool) $settings['highlight_excerpt'] ); ?> value="1"><?php esc_html_e( 'Enabled', 'elasticpress' ); ?></label><br>
653 <label><input name="settings[highlight_excerpt]" type="radio" <?php checked( ! (bool) $settings['highlight_excerpt'] ); ?> value="0"><?php esc_html_e( 'Disabled', 'elasticpress' ); ?></label>
654 <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>
655 </div>
656 </div>
657
658 <?php if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) : ?>
659 <br class="clear">
660 <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>
661 <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>
662 <?php endif; ?>
663
664 <?php
665 }
666 }
667