PluginProbe
ElasticPress / 4.7.2
ElasticPress v4.7.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 4.7.2, at includes/classes/Feature/DidYouMean/DidYouMean.php

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