%PDF- %PDF-
Direktori : /proc/1857783/root/var/www/cwg/wp-content/plugins/searchwp/includes/Logic/ |
Current File : //proc/1857783/root/var/www/cwg/wp-content/plugins/searchwp/includes/Logic/PartialMatches.php |
<?php /** * SearchWP partial matches logic. * * @package SearchWP * @author Jon Christopher */ namespace SearchWP\Logic; use SearchWP\Mod; use SearchWP\Query; use SearchWP\Engine; use SearchWP\Tokens; use SearchWP\Settings; use SearchWP\Logic\FuzzyMatches; /** * Class PartialMatches is responsible for finding partial matches for search strings. * * @since 4.0 */ class PartialMatches { /** * Index * * @since 4.0 * @var Index */ private $index; /** * Query * * @since 4.0 * @var Query */ private $query; /** * Tokens * * @since 4.0 * @var Tokens */ private $tokens; /** * Exact token matches * * @since 4.0 * @var array */ private $exact_matches = []; /** * Whether to force partial matches. * * @since 4.0 * @var false */ private $force = false; /** * Partial constructor. * * @since 4.0 */ function __construct() { add_filter( 'searchwp\query\tokens', [ $this, 'find' ], 5, 2 ); } /** * Finds partial matches for tokens. * * @since 4.0 * @param array $tokens Incoming tokens. * @param Query $query The Query being run. * @return array Tokens with partial matches. */ public function find( array $tokens, Query $query ) { if ( ! apply_filters( 'searchwp\query\partial_matches', Settings::get_single( 'partial_matches', 'boolean' ), [ 'tokens' => $tokens, 'query' => $query, ] ) ) { return $tokens; } $engine = $query->get_args()['engine'] instanceof Engine ? $query->get_args()['engine'] : new Engine( $query->get_args()['engine'] ); // We want to ensure we're working with the original search string, not anything filtered. $this->index = \SearchWP::$index; $this->query = $query; $this->tokens = new Tokens( $this->query->get_keywords() ); $keywords_tokens = array_unique( array_merge( $tokens, $this->tokens->get() ) ); $this->exact_matches = $this->index->has_tokens( $keywords_tokens, array_keys( $engine->get_sources() ), $query->get_args()['site'] ); // At this point, stemming has not yet taken place so we need to consider that when defining keywords tokens. if ( $this->query->use_stems ) { $stemmer = new \SearchWP\Stemmer(); $keywords_tokens = array_unique( array_map( function( $token ) use ( $stemmer ) { return $stemmer->stem( $token ); }, $keywords_tokens ) ); } $keywords_tokens = apply_filters( 'searchwp\query\partial_matches\keywords', $keywords_tokens, [ 'tokens' => $this->tokens, 'query' => $this->query, 'exact_matches' => $this->exact_matches, ] ); $values = []; $excluded = '1=1'; $this->force = apply_filters( 'searchwp\query\partial_matches\force', $this->force, [ 'tokens' => $this->tokens, 'query' => $this->query, 'exact_matches' => $this->exact_matches, ] ); // If the search string contains only exact matches, we can bail out. if ( apply_filters( 'searchwp\query\partial_matches\strict', ! $this->force ) ) { if ( empty( array_diff( array_values( $tokens ), array_values( $this->exact_matches ) ) ) ) { return $tokens; } } if ( ! $this->force ) { // Remove exact matches from consideration. $keywords_tokens = array_diff( $keywords_tokens, $this->exact_matches ); // Exclude exact matches from being returned. if ( ! empty( $this->exact_matches ) ) { $excluded = 'token NOT IN (' . implode( ',', array_fill( 0, count( $this->exact_matches ), '%s' ) ) . ')'; $values = $this->exact_matches; } } // If there are no tokens to work with, bail out. if ( empty( $keywords_tokens ) ) { return $tokens; } // Find partial match tokens. $values_before = $values; $partials = $this->prepare_tokens( $keywords_tokens ); $values = array_merge( $values, $partials ); $partial_tokens = $this->get_partial_tokens( $partials, $excluded, $values ); // If a wildcard after found no results, should we adapt by adding a wildcard before? $adaptive = apply_filters( 'searchwp\query\partial_matches\adaptive', true, [ 'tokens' => $this->tokens, 'query' => $this->query, 'partials' => $partial_tokens, ] ); // If no partials were found with no wildcard before, add a wildcard before and try again if we're adapting. if ( ( $this->force && $adaptive ) || ( empty( $partial_tokens ) && $adaptive ) ) { $partials = $this->prepare_tokens( $keywords_tokens, true ); $values = array_merge( $values_before, $partials ); // Need to undo earlier merge. $partial_tokens = $this->get_partial_tokens( $partials, $excluded, $values ); } $partial_args = [ 'keyword_tokens' => $keywords_tokens, 'exact_matches' => $this->exact_matches, 'query' => $query, 'partial' => $this, ]; // We also want to give exact matches a buoy. add_filter( 'searchwp\query\mods', [ $this, 'exact_match_buoy' ], 5, 2 ); // If we found partial matches (and aren't forcing fuzzy matches despite that) return them. if ( ! empty( $partial_tokens ) && ! apply_filters( 'searchwp\query\partial_matches\fuzzy\force', false, [ 'tokens' => $this->tokens, 'query' => $this->query, 'partials' => $partial_tokens, ] ) ) { return array_unique( array_merge( $this->get_original_tokens( $tokens, $partial_tokens ), $partial_tokens, $this->exact_matches ) ); } // There were no partial matches so return the original tokens. if ( ! apply_filters( 'searchwp\query\partial_matches\fuzzy', true ) ) { return $tokens; } // Integrate fuzzy matching. $fuzzy = new FuzzyMatches( $partial_tokens, $partial_args ); // Give priority to "Did you mean?" as it will essentially short circut fuzzy match finding. if ( apply_filters( 'searchwp\query\partial_matches\did_you_mean', true, [ 'tokens' => $this->tokens, 'query' => $this->query, ] ) ) { // Remove the exact match buoy because we're making an automatic suggestion. remove_filter( 'searchwp\query\mods', [ $this, 'exact_match_buoy' ], 5 ); return $fuzzy->did_you_mean(); } else { // Passive fuzzy matches. add_filter( 'searchwp\query\partial_matches\tokens', [ $fuzzy, 'find' ], 9, 2 ); } // Allow for additional token finding logic. $partial_tokens = apply_filters( 'searchwp\query\partial_matches\tokens', $partial_tokens, [ 'args' => $partial_args, 'tokens' => $this->tokens, 'query' => $this->query, ] ); return array_merge( $tokens, $partial_tokens, $this->exact_matches ); } /** * Control whether the invalid original tokens are returned. Check to see if each * token is included in the found partial matches and if it's not it is an invalid * token and will be removed unless the developer wants otherwise with the hook. * * @since 4.1.5 * @param mixed $tokens * @param mixed $partial_tokens * @return void */ private function get_original_tokens( $tokens, $partial_tokens ) { if ( apply_filters( 'searchwp\query\partial_matches\remove_invalid_tokens', true ) ) { $tokens = array_filter( array_map( function( $token ) use ( $partial_tokens ) { return in_array( $token, $partial_tokens, true ) ? $token : false; }, $tokens ) ); } return $tokens; } /** * Retrieves tokens for the submitted partial strings. * * @since 4.0 * @param array $partials Strings that need partial matches. * @param string $excluded Strings to exclude (e.g. exact matches). * @param array $values Values to prepare. * @return array Tokens for partial matches of submitted $partials strings. */ private function get_partial_tokens( array $partials, string $excluded, array $values ) { global $wpdb; $col = $this->query->use_stems ? 'stem' : 'token'; return $wpdb->get_col( $wpdb->prepare( "SELECT token FROM {$this->index->get_tables()['tokens']->table_name} WHERE {$excluded} {$this->get_boundaries_sql()} AND (" . implode( ' OR ', array_fill( 0, count( $partials ), "{$col} LIKE %s" ) ) . ")", $values ) ); } /** * Generates SQL clause to limit the length boundaries of tokens to find. * * @since 4.0 * @return string The gereated SQL clause. */ private function get_boundaries_sql() { global $wpdb; $min_length = absint( apply_filters( 'searchwp\query\partial_matches\minimum_length', $this->tokens->get_minimum_length(), [ 'tokens' => $this->tokens, 'query' => $this->query, ] ) ); $max_length = absint( apply_filters( 'searchwp\query\partial_matches\maximum_length', 0, [ 'tokens' => $this->tokens, 'query' => $this->query, ] ) ); $boundaries = []; // Site limiter. if ( 'all' !== $this->query->get_args()['site'] ) { $boundaries[] = $wpdb->prepare( "id IN ( SELECT token FROM {$this->index->get_tables()['index']->table_name} WHERE site IN (" . implode( ', ', array_fill( 0, count( $this->query->get_args()['site'] ), '%d' ) ) . ') )', $this->query->get_args()['site'] ); } if ( $min_length ) { $boundaries[] = "CHAR_LENGTH(token) >= {$min_length}"; } if ( $max_length ) { $boundaries[] = "CHAR_LENGTH(token) <= {$max_length}"; } return empty( $boundaries ) ? '' : ' AND (' . implode( ' AND ', $boundaries ) . ')'; } /** * Prepares the tokens. * * @since 4.0 * @param array $tokens Tokens to prepare. * @return array Prepared tokens. */ private function prepare_tokens( array $tokens, $force_wildcards = false ) { global $wpdb; return array_map( function( $token ) use ( $wpdb, $force_wildcards ) { $wildcard_before = $this->use_wildcard_before() || $force_wildcards ? '%' : ''; $wildcard_after = apply_filters( 'searchwp\query\partial_matches\wildcard_after', true, [ 'tokens' => $this->tokens, 'query' => $this->query, ] ) || $force_wildcards ? '%' : ''; return $wildcard_before . $wpdb->esc_like( $token ) . $wildcard_after; }, $tokens ); } /** * Whether to use a wildcard before tokens. * * @since 4.0 * @return boolean */ public function use_wildcard_before() { return apply_filters( 'searchwp\query\partial_matches\wildcard_before', false, [ 'tokens' => $this->tokens, 'query' => $this->query, ] ); } /** * Adds a buoy to ensure exact matches rank first. * * @since 4.0 * @param array $mods Incoming Mods. * @param array $args Query arguments. * @return array Mods with our buoy. */ public function exact_match_buoy( array $mods, Query $query ) { // If there are no exact matches bail out else we'll have an Error. if ( empty( $this->exact_matches ) || ! apply_filters( 'searchwp\query\partial_matches\buoy', true, [ 'tokens' => $this->tokens, 'query' => $this->query, ] ) ) { return $mods; } $alias = $this->index->get_alias(); $index_table = $this->index->get_tables()['index']->table_name; $mod = new Mod(); $mod->column_as( "( SELECT SUM({$index_table}.occurrences) FROM {$index_table} WHERE {$index_table}.id = {$alias}.id AND {$index_table}.site = {$alias}.site AND {$index_table}.source = {$alias}.source AND {$index_table}.token IN(" . implode( ',', array_map( 'absint', array_keys( $this->exact_matches ) ) ) . ") GROUP BY {$index_table}.id )", 'searchwp_exacts' ); $mod->order_by( "searchwp_exacts+0", 'DESC', 5 ); $mods[] = $mod; return $mods; } /** * Getter for Query. * * @since 4.0 * @return SearchWP\Query */ public function get_query() { return $this->query; } /** * Getter for Tokens. * * @since 4.0 * @return SearchWP\Tokens */ public function get_tokens() { return $this->tokens; } }