PluginProbe
ElasticPress / 4.6.0
ElasticPress v4.6.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 / DidYouMean / DidYouMean.php

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

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