By default, WordPress handles its search functionality and custom field queries with strict 'AND' logic. This means that if you try to pass both a search parameter (s) and a meta_query to WP_Query, WordPress will only return posts that match both the search term in the content and the specific value in your custom fields.

For many developers, this is a major limitation. You might want to build a search feature where a user can find a post if the keyword exists in the post title, the post content, OR a specific custom field like an SKU, a staff member's name, or a book author.

In this guide, we will explore several ways to bridge this gap, ranging from simple query merging to advanced SQL filtering.

The Problem with Default WP_Query Logic

When you build a standard query like the one below, WordPress generates a SQL statement that joins the wp_posts and wp_postmeta tables.

$args = array(
  'post_type' => 'post',
  's' => $query,
  'meta_query' => array(
     array(
       'key' => 'speel',
       'value' => $query,
       'compare' => 'LIKE'
     )
   )
);

$search = new WP_Query( $args );

The resulting SQL uses an AND operator between the search clause and the meta clause. If the keyword 'WordPress' is in the title but the custom field 'speel' is empty, that post will be excluded from the results. To fix this, we need to tell WordPress to use OR logic instead.

Method 1: Merging Multiple Queries

If you are working on a smaller site or a simple implementation, the most straightforward approach is to run two separate queries and merge the results. This avoids complex SQL filtering and keeps your code readable.

In this method, we fetch the IDs from a standard search and the IDs from a meta query, merge them, and then run a final query to get the actual post objects.

// 1. Get IDs from a standard search
$q1 = get_posts(array(
    'fields' => 'ids',
    'post_type' => 'post',
    's' => $query
));

// 2. Get IDs from a meta query
$q2 = get_posts(array(
    'fields' => 'ids',
    'post_type' => 'post',
    'meta_query' => array(
        array(
           'key' => 'speel',
           'value' => $query,
           'compare' => 'LIKE'
        )
     )
));

// 3. Merge and unique the IDs
$unique_ids = array_unique( array_merge( $q1, $q2 ) );

// 4. Run the final query
$posts = get_posts(array(
    'post_type' => 'post',
    'post__in' => $unique_ids,
    'post_status' => 'publish',
    'posts_per_page' => -1
));

if( $posts ) : 
    foreach( $posts as $post ) : 
        setup_postdata($post);
        // Display your results here
    endforeach; 
    wp_reset_postdata();
endif;

Pros: Easy to understand and debug. Cons: Less efficient for large databases as it requires three database hits.

Method 2: Using the pre_get_posts and get_meta_sql Filters

For a more performant and "WordPress-native" solution, you can use the pre_get_posts action combined with the get_meta_sql filter. This allows you to modify the SQL query directly before it is executed, changing the relationship between the title/content search and the meta search to OR.

First, we define a custom query variable (e.g., _meta_or_title) to trigger our logic:

add_action( 'pre_get_posts', function( $q )
{
    // Check if our custom argument is set
    if( $search_term = $q->get( '_meta_or_title' ) )
    {
        add_filter( 'get_meta_sql', function( $sql ) use ( $search_term )
        {
            global $wpdb;

            // Only run this filter once per query
            static $nr = 0; 
            if( 0 != $nr++ ) return $sql;

            // Modify the WHERE clause to include post_title and post_content
            $sql['where'] = sprintf(
                " AND ( (%s OR %s) OR %s ) ",
                $wpdb->prepare( "{$wpdb->posts}.post_title LIKE '%%%s%%'", $search_term),
                $wpdb->prepare( "{$wpdb->posts}.post_content LIKE '%%%s%%'", $search_term),
                mb_substr( $sql['where'], 5, mb_strlen( $sql['where'] ) )
            );

            return $sql;
        });
    }
});

To use this, you simply pass the _meta_or_title argument instead of the standard s parameter:

$args = array(
    'post_type' => 'post',
    '_meta_or_title' => $search_string,
    'meta_query' => array(
        array(
            'key' => 'my_custom_field',
            'value' => $search_string,
            'compare' => 'LIKE'
        )
    )
);

$the_query = new WP_Query($args);

Method 3: The posts_clauses Filter Approach

If you need maximum control over the SQL, the posts_clauses filter is your best friend. This filter gives you access to every part of the SQL query, including the JOIN and WHERE statements. This is particularly useful if you want to search across multiple specific meta keys simultaneously.

add_filter( 'posts_clauses', function ( $clauses ) {
    global $wpdb;

    // Check if we are on a search page and not in the admin
    if ( ! is_admin() && is_search() ) {

        // Define the meta keys you want to include in the search
        $target_metas = ['sku_number', 'product_brand'];

        foreach ( $target_metas as $index => $meta_key ) {
            // Add a LEFT JOIN for each meta key
            $clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS custom_sql{$index} ON ( {$wpdb->posts}.ID = custom_sql{$index}.post_id AND custom_sql{$index}.meta_key = '{$meta_key}' )";

            // Inject the OR condition into the WHERE clause
            $clauses['where'] = preg_replace(
                "/\\({$wpdb->posts}.post_content (NOT LIKE|LIKE) (\'[^']+\\')\\)/",
                "$0 OR ( custom_sql{$index}.meta_value $1 $2 )",
                $clauses['where']
            );
        }
    }

    return $clauses;
}, 999 );

This approach mirrors how WordPress core handles attachment filename searches. By using LEFT JOIN, we ensure that posts are still returned even if they don't have the specific meta key present.

Handling Edge Cases: Multi-word Searches

When users search for multiple words (e.g., "Blue Widget"), WordPress often splits these into separate terms. If you are using custom SQL filters, you must ensure your logic accounts for these exploded keywords.

Using explode(' ', get_query_var('s')) and looping through the keywords to build your WHERE clause dynamically is a robust way to handle complex search strings while maintaining the OR relationship with your custom fields.

Frequently Asked Questions

Why doesn't WordPress support 'OR' between search and meta by default?

WordPress core prioritizes performance. Joining the postmeta table is expensive. By defaulting to AND, the engine can narrow down results faster. Implementing a global OR would require complex SQL that might slow down sites with millions of rows of metadata.

Will these methods affect my site's performance?

Query merging (Method 1) is the heaviest on performance. The posts_clauses (Method 3) is the most efficient because it executes a single, optimized SQL query. However, always ensure your meta keys are indexed if you have a very large database.

Can I use these methods with Custom Post Types?

Yes. All methods described above work with Custom Post Types. Simply ensure you specify the post_type in your $args array or check for it within your filter logic.

Wrapping Up

Combining a standard search with a meta query using OR logic is a common requirement for professional WordPress builds. While query merging is a quick fix for small projects, mastering filters like pre_get_posts and posts_clauses will allow you to build faster, more scalable search experiences.

When implementing these solutions, always remember to: 1. Sanitize your search inputs to prevent SQL injection. 2. Use wp_reset_postdata() after custom loops. 3. Test your search performance with a realistic amount of data.

By following these patterns, you can ensure your users find exactly what they are looking for, whether it's in the title, the content, or a hidden custom field.