PluginProbe
ElasticPress / 5.3.4
ElasticPress v5.3.4
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.3.4, at includes/classes/Feature/DidYouMean/DidYouMean.php

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