Использование мета-запроса (‘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 )
...

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

Есть идеи?

Я искал решение этой проблемы несколько часов. Объединение массивов — не лучший вариант, особенно когда запросы сложные и нужно будет добавлять мета-запросы в будущем. Простое и красивое решение заключается в том, чтобы изменить ‘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()

wordpress выполнит эту функцию

введите описание изображения здесь

Вдохновившись этой функцией, я написал следующий код для поиска в метаданных:

   /**
     * Включить поиск в постмета и постах в одном запросе
     *
     * @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');

Как это работает:

  1. Проверка запроса: В функции custom_search_query мы проверяем, является ли запрос поисковым и не осуществляется ли он в административной панели WordPress.

  2. Запросы: Мы создаём два отдельных запроса с помощью WP_Query. Первый запрос ищет в заголовках и содержимом, второй — в пользовательском мета-поле, используя meta_query.

  3. Объединение результатов: Мы объединяем результаты, удаляя дубликаты с помощью функции array_unique.

  4. Установка постов в главный запрос: Если посты были найдены, мы устанавливаем их в текущий запрос WordPress. Если же посты не найдены, устанавливаем post__in в пустой массив, чтобы не возвращать результаты.

Заключение

Данный подход позволяет эффективно искать как в стандартных полях постов, так и в пользовательских мета-полях, обеспечивая при этом простоту кода и развитие функционала в будущем. Вы можете легко добавлять дополнительные поля мета или менять логику поиска, согласно вашим нуждам.

Оцените материал
Добавить комментарий

Капча загружается...