Вопрос или проблема
Пытаюсь создать поиск, который не только ищет по стандартным полям (название, содержание и т.д.), но и по конкретному пользовательскому полю.
Мой текущий запрос:
$args = array(
'post_type' => 'post',
's' => $query,
'meta_query' => array(
array(
'key' => 'speel',
'value' => $query,
'compare' => 'LIKE'
)
)
);
$search = new WP_Query( $args )
...
Этот запрос возвращает записи, которые соответствуют как поисковому запросу, так и мета-запросу, но я также хочу, чтобы он возвращал записи, которые соответствуют хотя бы одному из них.
Есть идеи?
Я искал решение этой проблемы несколько часов. Объединение массивов — не лучший вариант, особенно когда запросы сложные и нужно будет добавлять мета-запросы в будущем. Простое и красивое решение заключается в том, чтобы изменить ‘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)
Много кода можно сократить, используя модифицированную версию этого ответа.
$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');
Теперь вы можете искать по (названию, содержимому, отрывку) или (мета-полю) или (оба).
У меня была такая же проблема, для моего нового сайта я просто добавил новую мету “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;
Что вы думаете об этом обходном решении?
Согласно предложению Ника Перкинса, мне пришлось объединить два запроса так:
$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, $q2 ) );
$posts = get_posts(array(
'post_type' => 'posts',
'post__in' => $unique,
'post_status' => 'publish',
'posts_per_page' => -1
));
if( $posts ) : foreach( $posts as $post ) :
setup_postdata($post);
// теперь используйте стандартные функции цикла, такие как the_title() и т.д.
endforeach; endif;
Ну, это своего рода хак, но он работает. Вам нужно добавить фильтр 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);
Я не смог найти решение для поиска нескольких ключевых слов, которые могут быть смешаны как в заголовке записи, так и в описании И/ИЛИ одном или нескольких метах, поэтому я добавил свое собственное решение в функцию поиска.
Все, что вам нужно сделать, это добавить следующий код в function.php, и каждый раз, когда вы используете аргумент ‘s’ в стандартной функции WP_Query() и хотите, чтобы он искал в одном или нескольких мета-полях, вы просто добавляете аргумент 's_meta_keys'
, который является массивом мета-ключей, по которым вы хотите искать:
/************************************************************************\
|** **|
|** Позволить функции поиска WP_Query() искать по нескольким ключевым словам **|
|** в метах в дополнение к post_title и post_content **|
|** **|
|** Автор: rAthus @ Arkanite **|
|** Создано: 2020-08-18 **|
|** Обновлено: 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(); // инициируем массив идентификаторов совпадающих записей per searched keyword
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; // собираем идентификаторы совпадающих записей
$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; // собираем идентификаторы совпадающих записей
$merged = array_merge($ids_posts,$ids_metas); // объединяем идентификаторы заголовков, содержимого и мета, полученные из обоих запросов
$unique = array_unique($merged); // убираем дубликаты
if (!$unique)
$unique = array(0); // если результата нет, добавляем "0" идентификатор, иначе все записи будут возвращены
$ids[] = $unique; // добавляем массив совпадающих идентификаторов в основной массив
}
}
if (count($ids)>1)
$intersected = call_user_func_array('array_intersect',$ids); // если несколько ключевых слов, оставляем только идентификаторы, которые найдены во всех массивах совпадений ключевых слов
else
$intersected = $ids[0]; // в противном случае оставляем массив идентификаторов совпадений
$unique = array_unique($intersected); // убираем дубликаты
if (!$unique)
$unique = array(0); // если результата нет, добавляем "0" идентификатор, иначе все записи будут возвращены
unset($query->query_vars['s']); // удаляем обычный поисковый запрос
$query->set('post__in',$unique); // добавляем фильтр по идентификатору записи вместо этого
}
}
Пример использования:
$search= "kewords to 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);
Этот пример будет искать ключевые слова “kewords to search” в заголовках записей, описаниях и мета-ключах ‘short_desc’ и ‘tags’.
Ключевые слова могут находиться в одном или нескольких полях, в любом порядке, он вернет любую запись, которая содержит все ключевые слова в любом из назначенных полей.
Вы можете, очевидно, заставить поиск работать в списке мета-ключей, которые вы добавляете в функцию, и избавиться от лишних аргументов, если хотите, чтобы ВСЕ поисковые запросы включали эти мета-ключи 🙂
Надеюсь, это поможет кому-то, кто сталкивается с той же проблемой, с которой столкнулся я!
Я нашел чистое решение в самом ядре WordPress. Разработчики WordPress уже сталкивались с этой проблемой, когда искали в аттачментах мета _wp_attached_file
, и они решили эту проблему в следующей функции:
_filter_query_attachment_filenames()
Вдохновившись этой функцией, я написал следующий код для поиска в метаданных:
/**
* Включить поиск в постмета и постах в одном запросе
*
* @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. Если результаты имеются в других полях, но не в этом, вы ничего не получите. Никто этого не хочет.
Нужен левый джоин. Следующее создаст его.
$meta_query_args = array(
'relation' => 'OR',
array(
'key' => 'speel',
'value' => $search_term,
'compare' => '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']
на ваше значение поиска
Вот еще один способ: просто измените запрос с помощью фильтра ‘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];
}
для меня отлично работает следующий код:
$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’, вам нужно установить массив идентификаторов, а $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; // заполняем массив идентификатором записи
}
$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));
// Если записи не найдены, убедитесь, что запрос не выберет все записи.
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
));
Ответ или решение
Для реализации поиска в WordPress, который будет искать не только в стандартных полях (таких как заголовок и содержание поста), но и в определённом пользовательском поле (мета-поле), можно воспользоваться различными подходами. Ниже представлен один из самых эффективных и простых способов реализации данного функционала.
Решение задачи
Для того чтобы сделать так, чтобы поиск работал по как заголовкам и содержимому, так и по пользовательским полям, нам нужно создать два отдельных запроса, а затем объединить их результаты.
Вот пример кода, который осуществляет данную задачу:
function custom_search_query($query) {
// Проверяем, является ли это основной поисковой запрос и не в админке
if ($query->is_search() && !is_admin()) {
global $wpdb;
// Получаем поисковый запрос
$search_term = $query->get('s');
// Первый запрос - поиск по заголовку и содержимому
$args1 = array(
'post_type' => 'post',
's' => $search_term,
'posts_per_page' => -1
);
$q1 = new WP_Query($args1); // Выполняем первый запрос
// Второй запрос - поиск по мета-полю
$args2 = array(
'post_type' => 'post',
'meta_query' => array(
array(
'key' => 'speel', // Название мета-поля
'value' => $search_term,
'compare' => 'LIKE'
),
),
'posts_per_page' => -1
);
$q2 = new WP_Query($args2); // Выполняем второй запрос
// Объединяем результаты обоих запросов
$combined_posts = array_unique(array_merge($q1->posts, $q2->posts), SORT_REGULAR);
// Проверяем на наличие постов
if ($combined_posts) {
// Извлекаем уникальные ID постов
$post_ids = wp_list_pluck($combined_posts, 'ID');
// Устанавливаем новые параметры для основного запроса
$query->set('post__in', $post_ids);
$query->set('posts_per_page', -1); // Устанавливаем количество выводимых постов
} else {
// Если нет никаких постов, добавляем -1, чтобы ничего не возвращалось
$query->set('post__in', array(-1));
}
}
return $query; // Возвращаем изменённый запрос
}
add_action('pre_get_posts', 'custom_search_query');
Как это работает:
-
Проверка запроса: В функции
custom_search_query
мы проверяем, является ли запрос поисковым и не осуществляется ли он в административной панели WordPress. -
Запросы: Мы создаём два отдельных запроса с помощью
WP_Query
. Первый запрос ищет в заголовках и содержимом, второй — в пользовательском мета-поле, используяmeta_query
. -
Объединение результатов: Мы объединяем результаты, удаляя дубликаты с помощью функции
array_unique
. -
Установка постов в главный запрос: Если посты были найдены, мы устанавливаем их в текущий запрос WordPress. Если же посты не найдены, устанавливаем
post__in
в пустой массив, чтобы не возвращать результаты.
Заключение
Данный подход позволяет эффективно искать как в стандартных полях постов, так и в пользовательских мета-полях, обеспечивая при этом простоту кода и развитие функционала в будущем. Вы можете легко добавлять дополнительные поля мета или менять логику поиска, согласно вашим нуждам.