found_posts; if ( $total_results > $n ) { return null; } $suggestion = relevanssi_simple_generate_suggestion( $query ); $result = null; if ( $suggestion ) { $url = trailingslashit( get_bloginfo( 'url' ) ); $url = esc_attr( add_query_arg( array( 's' => rawurlencode( $suggestion ) ), $url ) ); /** * Filters the 'Did you mean' suggestion URL. * * @param string $url The URL for the suggested search query. * @param string $query The search query. * @param string $suggestion The suggestion. */ $url = apply_filters( 'relevanssi_didyoumean_url', $url, $query, $suggestion ); // Escape the suggestion to avoid XSS attacks. $suggestion = htmlspecialchars( $suggestion ); /** * Filters the complete 'Did you mean' suggestion. * * @param string The suggestion HTML code. */ $result = apply_filters( 'relevanssi_didyoumean_suggestion', "$pre$suggestion$post" ); } return $result; } /** * Generates the 'Did you mean' suggestions. Can be used to correct any queries. * * Uses the Relevanssi search logs as source material for corrections. If there * are no logged search queries, can't do anything. * * @global object $wpdb The WordPress database interface. * @global array $relevanssi_variables The Relevanssi global variables, used * for table names. * * @param string $query The query to correct. * * @return string Corrected query, empty if nothing found. */ function relevanssi_simple_generate_suggestion( $query ) { global $wpdb, $relevanssi_variables; /** * The minimum limit of occurrances to include a word. * * To save resources, only words with more than this many occurrances are * fed for the spelling corrector. If there are problems with the spelling * corrector, increasing this value may fix those problems. * * @param int $number The number of occurrances must be more than this * value, default 2. */ $count = apply_filters( 'relevanssi_get_words_having', 2 ); if ( ! is_numeric( $count ) ) { $count = 2; } $q = 'SELECT query, count(query) as c, AVG(hits) as a FROM ' . $relevanssi_variables['log_table'] . ' WHERE hits > ' . $count . ' GROUP BY query ORDER BY count(query) DESC'; /** * Filters the MySQL query used to fetch potential suggestions from the log. * * @param string $q MySQL query for fetching the suggestions. */ $q = apply_filters( 'relevanssi_didyoumean_query', $q ); $data = get_transient( 'relevanssi_didyoumean_query' ); if ( empty( $data ) ) { $data = $wpdb->get_results( $q ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared set_transient( 'relevanssi_didyoumean_query', $data, MONTH_IN_SECONDS ); } $query = htmlspecialchars_decode( $query, ENT_QUOTES ); $tokens = relevanssi_tokenize( $query, true, -1, 'search_query' ); $suggestions_made = false; $suggestion = ''; foreach ( $tokens as $token => $count ) { /** * Filters the tokens for Did you mean suggestions. * * You can use this filter hook to modify the tokens before Relevanssi * tries to come up with Did you mean suggestions for them. If you * return an empty string, the token will be skipped and no suggestion * will be made for the token. * * @param string $token An individual word from the search query. * * @return string The token. */ $token = apply_filters( 'relevanssi_didyoumean_token', trim( $token ) ); if ( ! $token ) { continue; } $closest = ''; $distance = -1; foreach ( $data as $row ) { if ( $row->c < 2 ) { break; } if ( $token === $row->query ) { $closest = ''; break; } elseif ( strlen( $token ) < 255 && strlen( $row->query ) < 255 ) { // The levenshtein() function has a max length of 255 // characters. The function uses strlen(), so we must use // too, instead of relevanssi_strlen(). $lev = levenshtein( $token, $row->query ); if ( $lev < 3 && ( $lev < $distance || $distance < 0 ) ) { if ( $row->a > 0 ) { $distance = $lev; $closest = $row->query; if ( $lev < 2 ) { break; // get the first with distance of 1 and go. } } } } } if ( ! empty( $closest ) ) { $query = str_ireplace( $token, $closest, $query, $replacement_count ); if ( $replacement_count > 0 ) { $suggestions_made = true; } } } if ( $suggestions_made ) { $suggestion = $query; } return $suggestion; }