<?php
/**
* /lib/phrases.php
*
* @package Relevanssi
* @author Mikko Saari
* @license https://wordpress.org/about/gpl/ GNU General Public License
* @see https://www.relevanssi.com/
*/
/**
* Extracts phrases from the search query.
*
* Finds all phrases wrapped in quotes (curly or straight) from the search
* query.
*
* @param string $query The search query.
*
* @return array An array of phrases (strings).
*/
function relevanssi_extract_phrases( string $query ) {
// iOS uses “” or „“ as the default quotes, so Relevanssi needs to
// understand those as well.
$normalized_query = str_replace( array( '”', '“', '„' ), '"', $query );
$pos = relevanssi_stripos( $normalized_query, '"' );
$phrases = array();
while ( false !== $pos ) {
if ( $pos + 2 > relevanssi_strlen( $normalized_query ) ) {
$pos = false;
continue;
}
$start = relevanssi_stripos( $normalized_query, '"', $pos );
$end = false;
if ( false !== $start ) {
$end = relevanssi_stripos( $normalized_query, '"', $start + 2 );
}
if ( false === $end ) {
// Just one " in the query.
$pos = $end;
continue;
}
$phrase = relevanssi_substr(
$normalized_query,
$start + 1,
$end - $start - 1
);
$phrase = trim( $phrase );
// Do not count single-word phrases as phrases.
if ( relevanssi_is_multiple_words( $phrase ) ) {
$phrases[] = $phrase;
}
$pos = $end + 1;
}
return $phrases;
}
/**
* Generates the MySQL code for restricting the search to phrase hits.
*
* This function uses relevanssi_extract_phrases() to figure out the phrases in
* the search query, then generates MySQL queries to restrict the search to the
* posts containing those phrases in the title, content, taxonomy terms or meta
* fields.
*
* @global array $relevanssi_variables The global Relevanssi variables.
*
* @param string $search_query The search query.
* @param string $operator The search operator (AND or OR).
*
* @return string $queries If not phrase hits are found, an empty string;
* otherwise MySQL queries to restrict the search.
*/
function relevanssi_recognize_phrases( $search_query, $operator = 'AND' ) {
global $relevanssi_variables;
$phrases = relevanssi_extract_phrases( $search_query );
$all_queries = array();
if ( 0 === count( $phrases ) ) {
return $all_queries;
}
/**
* Filters the custom fields for phrase matching.
*
* If you don't want the phrase matching to target custom fields, you can
* have this filter hook return an empty array.
*
* @param array $custom_fields An array of custom field names.
*/
$custom_fields = apply_filters( 'relevanssi_phrase_custom_fields', relevanssi_get_custom_fields() );
/**
* Filters the taxonomies for phrase matching.
*
* If you don't want the phrase matching to target taxonomies, you can have
* this filter hook return an empty array.
*
* @param array $taxonomies An array of taxonomy names.
*/
$taxonomies = apply_filters( 'relevanssi_phrase_taxonomies', get_option( 'relevanssi_index_taxonomies_list', array() ) );
$excerpts = get_option( 'relevanssi_index_excerpt', 'off' );
$phrase_queries = array();
$queries = array();
if (
isset( $relevanssi_variables['phrase_targets'] ) &&
is_array( $relevanssi_variables['phrase_targets'] )
) {
$non_targeted_phrases = array();
foreach ( $phrases as $phrase ) {
if (
isset( $relevanssi_variables['phrase_targets'][ $phrase ] ) &&
function_exists( 'relevanssi_targeted_phrases' )
) {
$queries = relevanssi_targeted_phrases( $phrase );
} else {
$non_targeted_phrases[] = $phrase;
}
}
$phrases = $non_targeted_phrases;
}
$queries = array_merge(
$queries,
relevanssi_generate_phrase_queries(
$phrases,
$taxonomies,
$custom_fields,
$excerpts
)
);
$phrase_queries = array();
foreach ( $queries as $phrase => $p_queries ) {
$pq_array = array();
foreach ( $p_queries as $query ) {
$pq_array[] = "relevanssi.{$query['target']} IN {$query['query']}";
}
$p_queries = implode( ' OR ', $pq_array );
$all_queries[] = "($p_queries)";
$phrase_queries[ $phrase ] = $p_queries;
}
$operator = strtoupper( $operator );
if ( 'AND' !== $operator && 'OR' !== $operator ) {
$operator = 'AND';
}
if ( ! empty( $all_queries ) ) {
$all_queries = ' AND ( ' . implode( ' ' . $operator . ' ', $all_queries ) . ' ) ';
}
return array(
'and' => $all_queries,
'or' => $phrase_queries,
);
}
/**
* Generates the phrase queries from phrases.
*
* Takes in phrases and a bunch of parameters and generates the MySQL queries
* that restrict the main search query to only posts that have the phrase.
*
* @param array $phrases A list of phrases to handle.
* @param array $taxonomies An array of taxonomy names to use.
* @param array|string $custom_fields A list of custom field names to use,
* "visible", or "all".
* @param string $excerpts If 'on', include excerpts.
*
* @global object $wpdb The WordPress database interface.
*
* @return array An array of queries sorted by phrase.
*/
function relevanssi_generate_phrase_queries(
array $phrases,
array $taxonomies,
$custom_fields,
string $excerpts
): array {
global $wpdb;
$status = relevanssi_valid_status_array();
// Add "inherit" to the list of allowed statuses to include attachments.
if ( ! strstr( $status, 'inherit' ) ) {
$status .= ",'inherit'";
}
$phrase_queries = array();
foreach ( $phrases as $phrase ) {
$queries = array();
$phrase = $wpdb->esc_like( $phrase );
$phrase = str_replace( array( '‘', '’', "'", '"', '”', '“', '“', '„', '´' ), '_', $phrase );
$title_phrase = $phrase;
$phrase = htmlspecialchars( $phrase );
/**
* Filters each phrase before it's passed through esc_sql() and used in
* the MySQL query. You can use this filter hook to for example run
* htmlentities() on the phrase in case your database needs that.
*
* @param string $phrase The phrase after quotes are replaced with a
* MySQL wild card and the phrase has been passed through esc_like() and
* htmlspecialchars().
*/
$phrase = esc_sql( apply_filters( 'relevanssi_phrase', $phrase ) );
$excerpt = '';
if ( 'on' === $excerpts ) {
$excerpt = "OR post_excerpt LIKE '%$phrase%'";
}
$query = "(SELECT ID FROM $wpdb->posts
WHERE (post_content LIKE '%$phrase%'
OR post_title LIKE '%$title_phrase%' $excerpt)
AND post_status IN ($status))";
$queries[] = array(
'query' => $query,
'target' => 'doc',
);
if ( ! empty( $taxonomies ) ) {
$taxonomies_escaped = implode( "','", array_map( 'esc_sql', $taxonomies ) );
$taxonomies_sql = "AND s.taxonomy IN ('$taxonomies_escaped')";
$query = "(SELECT ID FROM
$wpdb->posts as p,
$wpdb->term_relationships as r,
$wpdb->term_taxonomy as s, $wpdb->terms as t
WHERE r.term_taxonomy_id = s.term_taxonomy_id
AND s.term_id = t.term_id AND p.ID = r.object_id
$taxonomies_sql
AND t.name LIKE '%$phrase%' AND p.post_status IN ($status))";
$queries[] = array(
'query' => $query,
'target' => 'doc',
);
}
if ( ! empty( $custom_fields ) ) {
$keys = '';
if ( is_array( $custom_fields ) ) {
if ( ! in_array( '_relevanssi_pdf_content', $custom_fields, true ) ) {
array_push( $custom_fields, '_relevanssi_pdf_content' );
}
if ( strpos( implode( ' ', $custom_fields ), '%' ) ) {
// ACF repeater fields involved.
$custom_fields_regexp = str_replace( '%', '.+', implode( '|', $custom_fields ) );
$keys = "AND m.meta_key REGEXP ('$custom_fields_regexp')";
} else {
$custom_fields_escaped = implode(
"','",
array_map(
'esc_sql',
$custom_fields
)
);
$keys = "AND m.meta_key IN ('$custom_fields_escaped')";
}
}
if ( 'visible' === $custom_fields ) {
$keys = "AND (m.meta_key NOT LIKE '\_%' OR m.meta_key = '_relevanssi_pdf_content')";
}
$query = "(SELECT ID
FROM $wpdb->posts AS p, $wpdb->postmeta AS m
WHERE p.ID = m.post_id
$keys
AND m.meta_value LIKE '%$phrase%'
AND p.post_status IN ($status))";
$queries[] = array(
'query' => $query,
'target' => 'doc',
);
}
/**
* Filters the phrase queries.
*
* Relevanssi Premium uses this filter hook to add Premium-specific
* phrase queries.
*
* @param array $queries The MySQL queries for phrase matching.
* @param string $phrase The current phrase.
* @param string $status A string containing post statuses.
*
* @return array An array of phrase queries, where each query is an
* array that has the actual MySQL query in 'query' and the target
* column ('doc' or 'item') in the Relevanssi index table in 'target'.
*/
$queries = apply_filters( 'relevanssi_phrase_queries', $queries, $phrase, $status );
$phrase_queries[ $phrase ] = $queries;
}
return $phrase_queries;
}
|