Использование meta query ('meta_query') вместе с поисковым запросом ('s')
Пытаюсь создать поиск, который ищет не только по стандартным полям (заголовок, содержимое и т.д.), но также и по определенному произвольному полю.
Мой текущий запрос:
$args = array(
'post_type' => 'post',
's' => $query,
'meta_query' => array(
array(
'key' => 'speel',
'value' => $query,
'compare' => 'LIKE'
)
)
);
$search = new WP_Query( $args )
...
Этот запрос возвращает записи, которые соответствуют И поисковому запросу, И meta запросу, но я также хотел бы, чтобы он возвращал записи, которые соответствуют любому из них.
Есть идеи?

Я потратил часы на поиск решения этой проблемы. Слияние массивов - не выход, особенно когда запросы сложные и вам нужно иметь возможность добавлять к мета-запросам в будущем. Решение, которое поразительно просто, заключается в изменении параметра 's' на такой, который позволяет искать как по заголовкам, так и по мета-полям.
add_action( 'pre_get_posts', function( $q )
{
if( $title = $q->get( '_meta_or_title' ) )
{
add_filter( 'get_meta_sql', function( $sql ) use ( $title )
{
global $wpdb;
// Выполняем только один раз:
static $nr = 0;
if( 0 != $nr++ ) return $sql;
// Модифицируем WHERE условие
$sql['where'] = sprintf(
" AND ( %s OR %s ) ",
$wpdb->prepare( "{$wpdb->posts}.post_title like '%%%s%%'", $title),
mb_substr( $sql['where'], 5, mb_strlen( $sql['where'] ) )
);
return $sql;
});
}
});
Использование:
$meta_query = array();
$args = array();
$search_string = "test";
$meta_query[] = array(
'key' => 'staff_name',
'value' => $search_string,
'compare' => 'LIKE'
);
$meta_query[] = array(
'key' => 'staff_email',
'value' => $search_string,
'compare' => 'LIKE'
);
// Если мета-запросов больше одного, объединяем их через OR
if(count($meta_query) > 1) {
$meta_query['relation'] = 'OR';
}
// Формируем запрос
$args['post_type'] = "staff";
$args['_meta_or_title'] = $search_string; // больше не используем 's'
$args['meta_query'] = $meta_query;
$the_query = new WP_Query($args);

Фантастика, это отлично сработало для меня. Отличное решение! Спасибо!

Это запрос, а не поиск. Если у вас есть плагины, которые работают на поиск, это не сработает. Я ошибаюсь?

@marek.m вам потребуется настроить плагин (возможно, используя фильтр, если он доступен), чтобы добавить meta query.

Не работает, если я добавляю параметр tax_query вместе с

Много кода можно сократить, используя модифицированную версию этого ответа.
$q1 = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => -1,
's' => $query
));
$q2 = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => 'speel',
'value' => $query,
'compare' => 'LIKE'
)
)
));
$result = new WP_Query();
$result->posts = array_unique( array_merge( $q1->posts, $q2->posts ), SORT_REGULAR );
$result->post_count = count( $result->posts );

Я немного оптимизировал ответ от @Stabir Kira
function wp78649_extend_search( $query ) {
$search_term = filter_input( INPUT_GET, 's', FILTER_SANITIZE_NUMBER_INT) ?: 0;
if (
$query->is_search
&& !is_admin()
&& $query->is_main_query()
&& //ваше дополнительное условие
) {
$query->set('meta_query', [
[
'key' => 'meta_key',
'value' => $search_term,
'compare' => '='
]
]);
add_filter( 'get_meta_sql', function( $sql )
{
global $wpdb;
static $nr = 0;
if( 0 != $nr++ ) return $sql;
$sql['where'] = mb_eregi_replace( '^ AND', ' OR', $sql['where']);
return $sql;
});
}
return $query;
}
add_action( 'pre_get_posts', 'wp78649_extend_search');
Теперь вы можете осуществлять поиск по (заголовку, содержимому, excerpt) или (мета полю) или (обоим вариантам).

У меня была такая же проблема, для моего нового сайта я просто добавил новое мета-поле "title":
functions.php
add_action('save_post', 'title_to_meta');
function title_to_meta($post_id)
{
update_post_meta($post_id, 'title', get_the_title($post_id));
}
А затем... просто добавил что-то вроде этого:
$sub = array('relation' => 'OR');
$sub[] = array(
'key' => 'tags',
'value' => $_POST['q'],
'compare' => 'LIKE',
);
$sub[] = array(
'key' => 'description',
'value' => $_POST['q'],
'compare' => 'LIKE',
);
$sub[] = array(
'key' => 'title',
'value' => $_POST['q'],
'compare' => 'LIKE',
);
$params['meta_query'] = $sub;
Что вы думаете об этом решении?

Это на самом деле неплохо, но требует либо повторного сохранения всех существующих записей, либо начала использования до того, как вы начнёте добавлять контент.

@Berend, вы могли бы написать функцию, которая получает все записи, проходит по ним в цикле и обновляет post_meta для каждой. Включите её в пользовательский шаблон или функцию, запустите один раз, а затем удалите.

Как предложил Ник Перкинс, мне пришлось объединить два запроса следующим образом:
$q1 = get_posts(array(
'fields' => 'ids', // Получаем только ID постов
'post_type' => 'post', // Тип записи - пост
's' => $query // Поиск по строке запроса
));
$q2 = get_posts(array(
'fields' => 'ids', // Получаем только ID постов
'post_type' => 'post', // Тип записи - пост
'meta_query' => array( // Запрос по метаполям
array(
'key' => 'speel', // Ключ метаполя
'value' => $query, // Значение для поиска
'compare' => 'LIKE' // Оператор сравнения
)
)
));
// Объединяем массивы ID и удаляем дубликаты
$unique = array_unique( array_merge( $q1, $q2 ) );
// Получаем полные данные постов по списку ID
$posts = get_posts(array(
'post_type' => 'posts', // Тип записи
'post__in' => $unique, // Фильтр по массиву ID
'post_status' => 'publish', // Только опубликованные
'posts_per_page' => -1 // Все подходящие посты
));
if( $posts ) : foreach( $posts as $post ) :
setup_postdata($post);
// теперь можно использовать стандартные функции цикла, например the_title() и другие
endforeach; endif;

Это до сих пор невозможно без слияния в 2016? Я редактирую поисковый запрос через pre_get_posts, так что это действительно не вариант...

Это своего рода хак, но он работает. Вам нужно добавить фильтр posts_clauses. Эта функция-фильтр проверяет, есть ли любое из слов запроса в пользовательском поле "speel", оставляя остальную часть запроса без изменений.
function custom_search_where($pieces) {
// фильтр для вашего запроса
if (is_search() && !is_admin()) {
global $wpdb;
$keywords = explode(' ', get_query_var('s'));
$query = "";
foreach ($keywords as $word) {
// пропускаем возможные наречия и числа
if (is_numeric($word) || strlen($word) <= 2)
continue;
$query .= "((mypm1.meta_key = 'speel')";
$query .= " AND (mypm1.meta_value LIKE '%{$word}%')) OR ";
}
if (!empty($query)) {
// добавляем к условию WHERE
$pieces['where'] = str_replace("(((wp_posts.post_title LIKE '%", "( {$query} ((wp_posts.post_title LIKE '%", $pieces['where']);
$pieces['join'] = $pieces['join'] . " INNER JOIN {$wpdb->postmeta} AS mypm1 ON ({$wpdb->posts}.ID = mypm1.post_id)";
}
}
return ($pieces);
}
add_filter('posts_clauses', 'custom_search_where', 20, 1);

Вот еще один способ: изменить запрос с помощью фильтра 'posts_where_request'. Все останется по умолчанию, за исключением замены ('s' И 'meta_query') на ('s' ИЛИ 'meta_query').
AND ( ((posts.post_title LIKE 'Lily') OR (posts.post_excerpt LIKE 'Lily') OR (posts.post_content LIKE 'Lily')) )
AND ( ( postmeta.meta_key = 'author' AND postmeta.meta_value LIKE 'Lily' ) )
=>
AND (
( ( postmeta.meta_key = 'author' AND postmeta.meta_value LIKE 'Lily' ) )
OR
((posts.post_title LIKE 'Lily') OR (posts.post_excerpt LIKE 'Lily') OR (posts.post_content LIKE 'Lily'))
)
Вот код:
function edit_request_wp_query( $where ) {
global $wpdb;
if ( strpos($where, $wpdb->postmeta.'.meta_key') && strpos($where, $wpdb->posts.'.post_title') ) {
$string = $where;
$index_meta = index_argument_in_request($string, $wpdb->postmeta.'.meta_key', $wpdb->postmeta.'.meta_value');
$meta_query = substr($string, $index_meta['start'], $index_meta['end']-$index_meta['start']);
$string = str_replace( $meta_query, '', $string );
$meta_query = ltrim($meta_query, 'AND').' OR ';
$index_s = index_argument_in_request($string, $wpdb->posts.'.post_title');
$insert_to = strpos($string, '(', $index_s['start'])+1;
$string = substr_replace($string, $meta_query, $insert_to, 0);
$where = $string;
}
return $where;
}
add_filter('posts_where_request', 'edit_request_wp_query');
function index_argument_in_request($string, $key_start, $key_end = '') {
if (!$key_end) $key_end = $key_start;
$index_key_start = strpos($string, $key_start);
$string_before = substr($string, 0, $index_key_start);
$index_start = strrpos($string_before, 'AND');
$last_index_key = strrpos($string, $key_end);
$index_end = strpos($string, 'AND', $last_index_key);
return ['start' => $index_start, 'end' => $index_end];
}

Я не смог найти готового решения для поиска по нескольким ключевым словам, которые могут встречаться в заголовке записи, описании И/ИЛИ одном или нескольких мета-полях, поэтому создал собственное расширение функции поиска.
Всё, что вам нужно - добавить следующий код в файл function.php, и когда вы используете аргумент 's' в стандартной функции WP_Query() и хотите, чтобы поиск выполнялся также в одном или нескольких мета-полях, просто добавьте аргумент 's_meta_keys'
, который представляет собой массив ключей мета-полей, в которых нужно искать:
/************************************************************************\
|** **|
|** Позволяет функции поиска WP_Query() искать по нескольким ключевым **|
|** словам в мета-полях в дополнение к post_title и post_content **|
|** **|
|** By rAthus @ Arkanite **|
|** Created: 2020-08-18 **|
|** Updated: 2020-08-19 **|
|** **|
|** Просто используйте стандартный аргумент 's' и добавьте аргумент **|
|** 's_meta_keys' содержащий массив ключей мета-полей для поиска :) **|
|** **|
|** Пример : **|
|** **|
|** $args = array( **|
|** 'numberposts' => -1, **|
|** 'post_type' => 'post', **|
|** 's' => $MY_SEARCH_STRING, **|
|** 's_meta_keys' => array('META_KEY_1','META_KEY_2'); **|
|** 'orderby' => 'date', **|
|** 'order' => 'DESC', **|
|** ); **|
|** $posts = new WP_Query($args); **|
|** **|
\************************************************************************/
add_action('pre_get_posts', 'my_search_query'); // добавляем специальную функцию поиска к каждому запросу get_posts (включая WP_Query())
function my_search_query($query) {
if ($query->is_search() and $query->query_vars and $query->query_vars['s'] and $query->query_vars['s_meta_keys']) { // если используется поиск с аргументом 's' и добавлен аргумент 's_meta_keys'
global $wpdb;
$search = $query->query_vars['s']; // получаем строку поиска
$ids = array(); // инициализируем массив ID записей, соответствующих каждому ключевому слову
foreach (explode(' ',$search) as $term) { // разбиваем ключевые слова и ищем совпадения для каждого
$term = trim($term); // удаляем лишние пробелы
if (!empty($term)) { // проверяем, что ключевое слово не пустое
$query_posts = $wpdb->prepare("SELECT * FROM {$wpdb->posts} WHERE post_status='publish' AND ((post_title LIKE '%%%s%%') OR (post_content LIKE '%%%s%%'))", $term, $term); // ищем в заголовке и содержании как в стандартной функции
$ids_posts = [];
$results = $wpdb->get_results($query_posts);
if ($wpdb->last_error)
die($wpdb->last_error);
foreach ($results as $result)
$ids_posts[] = $result->ID; // собираем ID соответствующих записей
$query_meta = [];
foreach($query->query_vars['s_meta_keys'] as $meta_key) // формируем запрос для поиска в каждом указанном мета-поле
$query_meta[] = $wpdb->prepare("meta_key='%s' AND meta_value LIKE '%%%s%%'", $meta_key, $term);
$query_metas = $wpdb->prepare("SELECT * FROM {$wpdb->postmeta} WHERE ((".implode(') OR (',$query_meta)."))");
$ids_metas = [];
$results = $wpdb->get_results($query_metas);
if ($wpdb->last_error)
die($wpdb->last_error);
foreach ($results as $result)
$ids_metas[] = $result->post_id; // собираем ID соответствующих записей
$merged = array_merge($ids_posts,$ids_metas); // объединяем ID из результатов поиска в заголовках, содержании и мета-полях
$unique = array_unique($merged); // удаляем дубликаты
if (!$unique)
$unique = array(0); // если нет результатов, добавляем ID "0", иначе будут возвращены все записи
$ids[] = $unique; // добавляем массив ID в основной массив
}
}
if (count($ids)>1)
$intersected = call_user_func_array('array_intersect',$ids); // если несколько ключевых слов, оставляем только ID, которые есть во всех массивах результатов
else
$intersected = $ids[0]; // иначе оставляем единственный массив результатов
$unique = array_unique($intersected); // удаляем дубликаты
if (!$unique)
$unique = array(0); // если нет результатов, добавляем ID "0", иначе будут возвращены все записи
unset($query->query_vars['s']); // отключаем стандартный поиск
$query->set('post__in',$unique); // добавляем фильтр по ID записей
}
}
Пример использования:
$search= "ключевые слова для поиска";
$args = array(
'numberposts' => -1,
'post_type' => 'post',
's' => $search,
's_meta_keys' => array('short_desc','tags');
'orderby' => 'date',
'order' => 'DESC',
);
$posts = new WP_Query($args);
Этот пример будет искать ключевые слова "ключевые слова для поиска" в заголовках записей, описаниях и мета-полях 'short_desc' и 'tags'.
Ключевые слова могут быть найдены в одном или нескольких полях, в любом порядке, функция вернет любую запись, которая содержит все ключевые слова в любом из указанных полей.
Вы можете, конечно, жестко задать список мета-полей для поиска в функции и убрать дополнительный аргумент, если хотите, чтобы ВСЕ поисковые запросы включали эти мета-поля :)
Надеюсь, это поможет тем, кто столкнулся с той же проблемой, что и я!

Я нашел чистое решение в ядре WordPress.
Разработчики WordPress уже сталкивались с этой проблемой при поиске в метаполях вложений _wp_attached_file
и исправили её в этой функции:
_filter_query_attachment_filenames()
Взяв идею из этой функции, я написал следующий код для поиска в метаданных:
/**
* Включает поиск в таблицах postmeta и posts в одном запросе
*
* @see _filter_query_attachment_filenames()
*/
add_filter( 'posts_clauses', function ( $clauses ) {
global $wpdb;
// Выполняется только один раз:
static $counter = 0;
if ( 0 != $counter ++ ) {
return $clauses;
}
foreach (
[
'my_custom_meta_1',
'my_custom_meta_2',
] as $index => $meta_key
) {
// Добавляем LEFT JOIN таблицы postmeta, чтобы не перезаписывать существующие JOIN
$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS my_sql{$index} ON ( {$wpdb->posts}.ID = my_sql{$index}.post_id AND my_sql{$index}.meta_key = '{$meta_key}' )";
$clauses['where'] = preg_replace(
"/\({$wpdb->posts}.post_content (NOT LIKE|LIKE) (\'[^']+\')\)/",
"$0 OR ( my_sql{$index}.meta_value $1 $2 )",
$clauses['where']
);
}
return $clauses;
}, 999 );

Все вышеперечисленные решения возвращают результаты только при наличии совпадения в мета-поле "speel". Если у вас есть результаты в других полях, но не в этом, вы ничего не получите. А это никому не нужно.
Необходим LEFT JOIN. Следующий код создаст его.
$meta_query_args = array(
'relation' => 'OR', // отношение между условиями - ИЛИ
array(
'key' => 'speel', // ключ мета-поля
'value' => $search_term, // искомый термин
'compare' => 'LIKE', // оператор сравнения - LIKE
),
array(
'key' => 'speel', // ключ мета-поля
'compare' => 'NOT EXISTS', // проверка на отсутствие поля
),
);
$query->set('meta_query', $meta_query_args); // устанавливаем параметры мета-запроса

Ответ от @satbir-kira работает отлично, но он выполняет поиск только по метаполям и заголовкам записей. Если вам нужно искать по метаполям, заголовкам и содержимому записи, вот модифицированная версия.
add_action( 'pre_get_posts', function( $q )
{
if( $title = $q->get( '_meta_or_title' ) )
{
add_filter( 'get_meta_sql', function( $sql ) use ( $title )
{
global $wpdb;
// Выполняется только один раз:
static $nr = 0;
if( 0 != $nr++ ) return $sql;
// Модифицированное условие WHERE
$sql['where'] = sprintf(
" AND ( (%s OR %s) OR %s ) ",
$wpdb->prepare( "{$wpdb->posts}.post_title like '%%%s%%'", $title),
$wpdb->prepare( "{$wpdb->posts}.post_content like '%%%s%%'", $title),
mb_substr( $sql['where'], 5, mb_strlen( $sql['where'] ) )
);
return $sql;
});
}
});
Пример использования:
$args['_meta_or_title'] = $get['search']; // больше не используем 's'
$args['meta_query'] = array(
'relation' => 'OR',
array(
'key' => '_ltc_org_name',
'value' => $get['search'],
'compare' => 'LIKE'
),
array(
'key' => '_ltc_org_school',
'value' => $get['search'],
'compare' => 'LIKE'
),
array(
'key' => '_ltc_district_address',
'value' => $get['search'],
'compare' => 'LIKE'
)
);
Замените $get['search']
на ваше значение для поиска

@SubhojitMukherjee Вы нашли решение, когда добавляете tax_query? У меня точно такая же проблема, как у вас :)

Окей, нашел решение! Спасибо Sully (https://wordpress.stackexchange.com/questions/403040/query-to-get-result-by-title-or-meta-along-with-tax-query-parameter) Вот код, который нужно обновить, чтобы он работал с tax_query:

add_filter( 'get_meta_sql', function( $sql, $queries, $type ) use ( $title ){ global $wpdb; static $nr = 0; if( 'post' !== $type || 0 != $nr++ ) return $sql; $sql['where'] = sprintf( " AND ( %s OR %s ) ", $wpdb->prepare( "{$wpdb->posts}.post_title like '%%%s%%'", $title), mb_substr( $sql['where'], 5, mb_strlen( $sql['where'] ) ) ); return $sql; }, 10, 3); }

Для меня отлично работает следующий код:
// Получаем поисковый запрос из параметра id
$search_word = $_GET['id'];
// Декодируем и обрезаем пробелы у поискового запроса
$data['words'] = trim(urldecode($search_word));
// Первый запрос: поиск по заголовку и контенту
$q1 = new WP_Query( array(
'post_type' => array('notas', 'productos'), // Типы записей: заметки и продукты
'posts_per_page' => -1, // Все записи без ограничения
's' => $search_word // Поисковый запрос
));
// Второй запрос: поиск по произвольным полям
$q2 = new WP_Query( array(
'post_type' => array('notas', 'productos'), // Типы записей: заметки и продукты
'posts_per_page' => -1, // Все записи без ограничения
'meta_query' => array(
'relation' => 'OR', // Логическое ИЛИ между условиями
array(
'key' => 'subtitulo', // Поле "подзаголовок"
'value' => $search_word, // Значение для поиска
'compare' => 'LIKE' // Поиск по частичному совпадению
),
array(
'key' => 'thumbnail_bajada', // Поле "описание миниатюры"
'value' => $search_word, // Значение для поиска
'compare' => 'LIKE' // Поиск по частичному совпадению
)
)
));
// Объединяем результаты двух запросов
$result = new WP_Query();
// Убираем дубликаты и сохраняем уникальные записи
$result->posts = array_unique( array_merge( $q1->posts, $q2->posts ), SORT_REGULAR );
// Обновляем счетчик записей
$result->post_count = count( $result->posts );

Это отличное решение, но нужно исправить один момент. При вызове 'post__in' необходимо передать массив ID, а $unique является массивом постов.
Пример:
$q1 = get_posts(array(
'fields' => 'ids',
'post_type' => 'post',
's' => $query
));
$q2 = get_posts(array(
'fields' => 'ids',
'post_type' => 'post',
'meta_query' => array(
array(
'key' => 'speel',
'value' => $query,
'compare' => 'LIKE'
)
)
));
$unique = array_unique( array_merge( $q1->posts, $q2->posts ) );
$array = array(); // здесь инициализируем массив
foreach($posts as $post)
{
$array[] = $post->ID; // заполняем массив ID постов
}
$posts = get_posts(array(
'post_type' => 'posts',
'post__in' => $array,
'post_status' => 'publish',
'posts_per_page' => -1
));

Я обнаружил, что ответ Асада Манзура сработал для меня. Если кому-то это нужно, моя версия требовала реализации параметра paged
:
$search_query = trim(esc_html( get_search_query() ));
$posts_per_page = $wp_query->query_vars['posts_per_page'];
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
$q1 = new WP_Query(array(
's' => $search_query,
'post_type' => array('page', 'post'),
'posts_per_page' => -1,
'fields' => 'ids'
));
$q2 = new WP_Query(array(
'fields' => 'ids',
'post_type' => array('page', 'post'),
'posts_per_page' => -1,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'custom_body',
'value' => $search_query,
'compare' => 'LIKE'
)
)
));
$unique = array_unique(array_merge($q1->posts, $q2->posts));
// Если записи не найдены, убедимся, что $query не выбирает все записи.
if (!$unique) {
$unique = array(-1);
}
$query = new WP_Query(array(
'post_type' => array('page', 'post'),
'post__in' => $unique,
'paged' => $paged,
'post_status' => 'publish',
'posts_per_page' => $posts_per_page
));
