220 lines
6.5 KiB
PHP
220 lines
6.5 KiB
PHP
<?php
|
|
/**
|
|
* /lib/didyoumean.php
|
|
*
|
|
* @package Relevanssi
|
|
* @author Mikko Saari
|
|
* @license https://wordpress.org/about/gpl/ GNU General Public License
|
|
* @see https://www.relevanssi.com/
|
|
*/
|
|
|
|
/**
|
|
* Generates the Did you mean suggestions.
|
|
*
|
|
* A wrapper function that prints out the Did you mean suggestions. If Premium
|
|
* is available, will use relevanssi_premium_didyoumean(), otherwise the
|
|
* relevanssi_simple_didyoumean() is used.
|
|
*
|
|
* @param string $query The query.
|
|
* @param string $pre Printed out before the suggestion.
|
|
* @param string $post Printed out after the suggestion.
|
|
* @param int $n Maximum number of search results found for the
|
|
* suggestions to show up. Default 5.
|
|
* @param boolean $echoed If true, echo out. Default true.
|
|
*
|
|
* @return string|null The suggestion HTML element.
|
|
*/
|
|
function relevanssi_didyoumean( $query, $pre, $post, $n = 5, $echoed = true ) {
|
|
if ( function_exists( 'relevanssi_premium_didyoumean' ) ) {
|
|
$result = relevanssi_premium_didyoumean( $query, $pre, $post, $n );
|
|
} else {
|
|
$result = relevanssi_simple_didyoumean( $query, $pre, $post, $n );
|
|
}
|
|
|
|
if ( $echoed ) {
|
|
echo $result; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Generates the Did you mean suggestions HTML code.
|
|
*
|
|
* Uses relevanssi_simple_generate_suggestion() to come up with a suggestion,
|
|
* then wraps that up with HTML code.
|
|
*
|
|
* @global object $wpdb The WordPress database interface.
|
|
* @global array $relevanssi_variables The Relevanssi global variables.
|
|
* @global object $wp_query The WP_Query object.
|
|
*
|
|
* @param string $query The query.
|
|
* @param string $pre Printed out before the suggestion.
|
|
* @param string $post Printed out after the suggestion.
|
|
* @param int $n Maximum number of search results found for the
|
|
* suggestions to show up. Default 5.
|
|
*
|
|
* @return string|null The suggestion HTML code, null if nothing found.
|
|
*/
|
|
function relevanssi_simple_didyoumean( $query, $pre, $post, $n = 5 ) {
|
|
global $wp_query;
|
|
|
|
$total_results = $wp_query->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<a href='$url'>$suggestion</a>$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;
|
|
}
|