PluginProbe
ElasticPress / 5.0.2
ElasticPress v5.0.2
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 / DidYouMean / DidYouMean.php

DidYouMean.php in ElasticPress 5.0.2, at includes/classes/Feature/DidYouMean/DidYouMean.php

444 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Did You Mean feature.
4 *
5 * @since 4.6.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Feature\DidYouMean;
10
11 use ElasticPress\{Elasticsearch, Feature, FeatureRequirementsStatus, Features };
12
13 /**
14 * Did You Mean feature class.
15 */
16 class DidYouMean extends Feature {
17
18 /**
19 * Initialize feature, setting it's config.
20 */
21 public function __construct() {
22 $this->slug = 'did-you-mean';
23
24 $this->title = esc_html__( 'Did You Mean', 'elasticpress' );
25
26 $this->summary = '<p>' . __( '"Did You Mean" search feature provides alternative suggestions for misspelled or ambiguous search queries, enhancing search accuracy and user experience. To display suggestions in your theme, please follow <a href="https://elasticpress.zendesk.com/hc/en-us/articles/16673223107085-Did-You-Mean">this tutorial</a>.', 'elasticpress' ) . '</p>';
27
28 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/16673223107085-Did-You-Mean', 'elasticpress' );
29
30 $this->requires_install_reindex = true;
31
32 $this->available_during_installation = true;
33
34 $this->default_settings = [
35 'search_behavior' => '0',
36 ];
37
38 $this->requires_feature = 'search';
39
40 parent::__construct();
41 }
42
43 /**
44 * Setup search functionality.
45 *
46 * @return void
47 */
48 public function setup() {
49 add_filter( 'ep_post_mapping', [ $this, 'add_mapping' ] );
50 add_filter( 'ep_post_formatted_args', [ $this, 'add_query_args' ], 10, 3 );
51 add_filter( 'ep_integrate_search_queries', [ $this, 'set_ep_suggestion' ], 10, 2 );
52 add_action( 'template_redirect', [ $this, 'automatically_redirect_user' ] );
53 add_action( 'ep_suggestions', [ $this, 'the_output' ] );
54 }
55
56 /**
57 * Output feature box long.
58 *
59 * @return void
60 */
61 public function output_feature_box_long() {
62 ?>
63 <p>
64 <?php
65 echo wp_kses_post(
66 sprintf(
67 /* translators: Tutorial URL */
68 __( '"Did You Mean" search feature provides alternative suggestions for misspelled or ambiguous search queries, enhancing search accuracy and user experience. To display suggestions in your theme, please follow <a href="%s">this tutorial</a>.', 'elasticpress' ),
69 'https://elasticpress.zendesk.com/hc/en-us/articles/16673223107085-Did-You-Mean'
70 )
71 );
72 ?>
73 </p>
74 <?php
75 }
76
77 /**
78 * Add mapping.
79 *
80 * @param array $mapping Post mapping.
81 */
82 public function add_mapping( $mapping ) : array {
83 // Shingle token filter.
84 $mapping['settings']['analysis']['filter']['shingle_filter'] = [
85 'type' => 'shingle',
86 'min_shingle_size' => 2,
87 'max_shingle_size' => 3,
88 ];
89
90 // Custom analyzer.
91 $mapping['settings']['analysis']['analyzer']['trigram'] = [
92 'type' => 'custom',
93 'tokenizer' => 'standard',
94 'filter' => [
95 'lowercase',
96 'shingle_filter',
97 ],
98 ];
99
100 if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
101 $mapping['mappings']['post']['properties']['post_content']['fields'] = [
102 'shingle' => [
103 'type' => 'text',
104 'analyzer' => 'trigram',
105 ],
106 ];
107 } else {
108 $mapping['mappings']['properties']['post_content']['fields'] = [
109 'shingle' => [
110 'type' => 'text',
111 'analyzer' => 'trigram',
112 ],
113 ];
114 }
115
116 return $mapping;
117 }
118
119 /**
120 * Return the suggested search term.
121 *
122 * @param WP_Query $query WP_Query object
123 * @return string|false
124 */
125 public function get_suggestion( $query = null ) {
126 global $wp_query;
127
128 $settings = $this->get_settings();
129 if ( empty( $settings['active'] ) ) {
130 return false;
131 }
132
133 if ( ! $query && $wp_query->is_main_query() && $wp_query->is_search() ) {
134 $query = $wp_query;
135 }
136
137 if ( ! is_a( $query, '\WP_Query' ) ) {
138 return false;
139 }
140
141 $term = $this->get_suggested_term( $query );
142 if ( empty( $term ) ) {
143 return false;
144 }
145
146 $html = sprintf( '<span class="ep-spell-suggestion">%s: <a href="%s">%s</a>?</span>', esc_html__( 'Did you mean', 'elasticpress' ), get_search_link( $term ), $term );
147
148 $html .= $this->get_alternatives_terms( $query );
149 $terms = $query->suggested_terms['options'] ?? [];
150
151 /**
152 * Filter the did you mean suggested HTML.
153 *
154 * @since 4.6.0
155 * @hook ep_suggestion_html
156 * @param {string} $html The HTML output.
157 * @param {array} $terms All suggested terms.
158 * @param {WP_Query} $query The WP_Query object.
159 * @return {string} New HTML output
160 */
161 return apply_filters( 'ep_suggestion_html', $html, $terms, $query );
162 }
163
164 /**
165 * If needed set the `suggest` to ES query clause.
166 *
167 * @param array $formatted_args Formatted Elasticsearch query.
168 * @param array $args WP_Query arguments
169 * @param array $wp_query WP_Query object
170 */
171 public function add_query_args( $formatted_args, $args, $wp_query ) : array {
172 $search_analyzer = [
173 'phrase' => [
174 'field' => 'post_content.shingle',
175 'max_errors' => 2,
176 'direct_generator' => [
177 [
178 'field' => 'post_content.shingle',
179 ],
180 ],
181 ],
182 ];
183
184 /**
185 * Filter the search analyzer use for the did you mean feature.
186 *
187 * @since 4.6.0
188 * @hook ep_search_suggestion_analyzer
189 * @param {array} $search_analyzer Search analyzer
190 * @param {array} $formatted_args Formatted Elasticsearch query
191 * @param {array} $args WP_Query arguments
192 * @param {WP_Query} $wp_query WP_Query object
193 * @return {array} New search analyzer
194 */
195 $search_analyzer = apply_filters( 'ep_search_suggestion_analyzer', $search_analyzer, $formatted_args, $args, $wp_query );
196
197 if ( ! empty( $args['s'] ) ) {
198 $formatted_args['suggest'] = array(
199 'text' => $args['s'],
200 'ep_suggestion' => $search_analyzer,
201 );
202 }
203
204 return $formatted_args;
205 }
206
207 /**
208 * Set the ep_suggestion flag to true if the query is a search query.
209 *
210 * @param bool $enabled Whether to enable the search queries integration.
211 * @param WP_Query $query The WP_Query object.
212 */
213 public function set_ep_suggestion( $enabled, $query ) : bool {
214 if ( $query->is_search() && ! empty( $query->query_vars['s'] ) ) {
215 $query->set( 'ep_suggestion', true );
216 }
217
218 return $enabled;
219 }
220
221 /**
222 * Returns requirements status of feature
223 *
224 * Requires the search feature to be activated
225 */
226 public function requirements_status() : FeatureRequirementsStatus {
227 return new FeatureRequirementsStatus( 1 );
228 }
229
230 /**
231 * Display feature settings.
232 *
233 * @return void
234 */
235 public function output_feature_box_settings() {
236 $settings = $this->get_settings();
237 ?>
238 <div class="field">
239 <div class="field-name status"><?php esc_html_e( 'Search behavior when no result is found', 'elasticpress' ); ?></div>
240 <div class="input-wrap">
241 <label><input name="settings[search_behavior]" type="radio" <?php checked( ! (bool) $settings['search_behavior'] ); ?> value="0"><?php esc_html_e( 'Display the top suggestion', 'elasticpress' ); ?></label><br>
242 <label><input name="settings[search_behavior]" type="radio" <?php checked( $settings['search_behavior'], 'list' ); ?> value="list"><?php esc_html_e( 'Display all the suggestions', 'elasticpress' ); ?></label><br>
243 <label><input name="settings[search_behavior]" type="radio" <?php checked( $settings['search_behavior'], 'redirect' ); ?> value="redirect"><?php esc_html_e( 'Automatically redirect the user to the top suggestion', 'elasticpress' ); ?></label><br>
244 </div>
245 </div>
246 <?php
247 }
248
249 /**
250 * Returns the list of other suggestions
251 *
252 * @param WP_Query $query WP_Query object
253 * @return string|false
254 */
255 protected function get_alternatives_terms( $query ) {
256 global $wp_query;
257
258 if ( ! $query && $wp_query->is_main_query() && $wp_query->is_search() ) {
259 $query = $wp_query;
260 }
261
262 if ( ! is_a( $query, '\WP_Query' ) ) {
263 return false;
264 }
265
266 $settings = $this->get_settings();
267
268 // If there are posts, we don't need to show the list of suggestions.
269 if ( 'list' !== $settings['search_behavior'] || $query->found_posts ) {
270 return false;
271 }
272
273 $options = $query->suggested_terms['options'] ?? [];
274 array_shift( $options );
275
276 if ( empty( $options ) ) {
277 return '';
278 }
279
280 $html = '<div class="ep-spell-suggestions">';
281 $html .= esc_html__( 'Other suggestions:', 'elasticpress' );
282 $html .= '<ul class="ep-suggestions-list">';
283 foreach ( $options as $option ) {
284 $html .= sprintf( '<li><a href="%s">%s</a></li>', get_search_link( $option['text'] ), $option['text'] );
285 }
286 $html .= '</ul>';
287 $html .= '</div>';
288
289 return $html;
290 }
291
292 /**
293 * Returns the top suggested term
294 *
295 * @param WP_Query $query WP_Query object
296 * @return string|bool
297 */
298 public function get_suggested_term( $query ) {
299 $options = $query->suggested_terms['options'] ?? [];
300 return ! empty( $options ) ? $options[0]['text'] : false;
301 }
302
303 /**
304 * Redirect user to suggested search term if no results found and search_behavior is set to redirect.
305 *
306 * @return void
307 */
308 public function automatically_redirect_user() {
309 global $wp_query;
310
311 if ( ! $wp_query->is_main_query() || ! $wp_query->is_search() ) {
312 return;
313 }
314
315 if ( $wp_query->found_posts ) {
316 return;
317 }
318
319 $settings = $this->get_settings();
320 if ( 'redirect' !== $settings['search_behavior'] ) {
321 return;
322 }
323
324 $term = $this->get_suggested_term( $wp_query );
325 if ( empty( $term ) ) {
326 return;
327 }
328
329 $url = get_search_link( $term );
330 $url = add_query_arg(
331 [
332 'ep_suggestion_original_term' => $wp_query->query_vars['s'],
333 ],
334 $url
335 );
336
337 wp_safe_redirect( $url );
338 exit;
339 }
340
341 /**
342 * Return a message to the user when the original search term has no results and the user is redirected to the suggested term.
343 *
344 * @param WP_Query $query WP_Query object
345 *
346 * @return string|void
347 */
348 public function get_original_search_term( $query = null ) {
349 global $wp_query;
350
351 $settings = $this->get_settings();
352 if ( empty( $settings['active'] ) ) {
353 return false;
354 }
355
356 if ( ! $query && $wp_query->is_main_query() && $wp_query->is_search() ) {
357 $query = $wp_query;
358 }
359
360 if ( ! is_a( $query, '\WP_Query' ) ) {
361 return;
362 }
363
364 if ( ! isset( $_GET['ep_suggestion_original_term'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
365 return;
366 }
367
368 $settings = $this->get_settings();
369 if ( 'redirect' !== $settings['search_behavior'] ) {
370 return;
371 }
372
373 $original_term = sanitize_text_field( wp_unslash( $_GET['ep_suggestion_original_term'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
374
375 $html = sprintf(
376 '<div class="ep-original-search-term-message">
377 <span class="result">%s</span><strong>%s</strong>
378 <br/>
379 <span class="no-result">%s</span><strong>%s</strong>
380 </div>',
381 esc_html__( 'Showing results for: ', 'elasticpress' ),
382 esc_html( $query->query_vars['s'] ),
383 esc_html__( 'No results for: ', 'elasticpress' ),
384 esc_html( $original_term )
385 );
386
387 /**
388 * Filter the HTML output for the original search term.
389 *
390 * @since 4.6.0
391 * @hook ep_suggestion_original_search_term_html
392 * @param {string} $html HTML output
393 * @param {string} $search_term Suggested search term
394 * @param {string} $original_term Original search term
395 * @param {WP_Query} $query WP_Query object
396 * @return {string} New HTML output
397 */
398 return apply_filters( 'ep_suggestion_original_search_term_html', $html, $query->query_vars['s'], $original_term, $query );
399 }
400
401 /**
402 * Returns the suggestion
403 *
404 * @param WP_Query $query WP_Query object
405 * @return void
406 */
407 public function the_output( $query = null ) {
408 $html = $this->get_original_search_term( $query );
409 $html .= $this->get_suggestion( $query );
410
411 echo wp_kses_post( $html );
412 }
413
414 /**
415 * Set the `settings_schema` attribute
416 *
417 * @since 5.0.0
418 */
419 protected function set_settings_schema() {
420 $this->settings_schema = [
421 [
422 'default' => '0',
423 'key' => 'search_behavior',
424 'label' => __( 'Search behavior when no result is found', 'elasticpress' ),
425 'options' => [
426 [
427 'label' => __( 'Display the top suggestion', 'elasticpress' ),
428 'value' => '0',
429 ],
430 [
431 'label' => __( 'Display all the suggestions', 'elasticpress' ),
432 'value' => 'list',
433 ],
434 [
435 'label' => __( 'Automatically redirect the user to the top suggestion', 'elasticpress' ),
436 'value' => 'redirect',
437 ],
438 ],
439 'type' => 'radio',
440 ],
441 ];
442 }
443 }
444