How to Combine AND / OR Logic in Complex Searches

January 19, 2026
How to Combine AND / OR Logic in Complex Searches

Complex WordPress searches often fail for one reason: mixing AND and OR
logic across different dimensions (keywords, meta fields, taxonomies, post types) is not straightforward in
WP_Query.

If you try to jam everything into one query, you usually end up with:

  • Unexpected zero results
  • Wrong “OR” behavior (only one condition works)
  • Slow meta queries and duplicated joins
  • Pagination issues and unstable ordering

This article shows practical, code-first patterns to combine AND/OR logic safely,
with predictable results and real-world performance considerations.

Understand the Core Limitation

WP_Query supports:

  • Keyword search (s)
  • meta_query with nested relation
  • tax_query with nested relation

But it does not provide a native way to group conditions like:

(keyword OR meta OR taxonomy) AND (flag1) AND (flag2)

Keyword search is its own subsystem and doesn’t neatly nest into meta/tax relations.
That’s why the most reliable approach is often: compute IDs in stages.

Pattern 1: Build OR IDs, Then Apply AND Filters (Most Reliable)

This approach is predictable and debuggable:

  • Run separate queries for each OR branch (keyword / meta / taxonomy)
  • Merge IDs (OR result)
  • Apply AND filters using array_intersect
  • Feed IDs into the main query using post__in

Example: (Keyword OR Meta OR Taxonomy) AND (is_featured) AND (has_pdf)

<?php
function wpct_get_ids_by_keyword( string $keyword ): array {
  if ( $keyword === '' ) return array();

  $q = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => -1,
    'fields'         => 'ids',
    's'              => $keyword,
  ) );

  return array_map( 'absint', $q->posts );
}

function wpct_get_ids_by_meta_or( array $values ): array {
  if ( empty( $values ) ) return array();

  $meta_query = array( 'relation' => 'OR' );
  foreach ( $values as $v ) {
    $meta_query[] = array(
      'key'     => 'dt_field',
      'value'   => (string) $v,
      'compare' => 'LIKE',
    );
  }

  $q = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => -1,
    'fields'         => 'ids',
    'meta_query'     => $meta_query,
  ) );

  return array_map( 'absint', $q->posts );
}

function wpct_get_ids_by_tax( array $term_ids ): array {
  if ( empty( $term_ids ) ) return array();

  $q = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => -1,
    'fields'         => 'ids',
    'tax_query'      => array(
      array(
        'taxonomy' => 'category',
        'field'    => 'term_id',
        'terms'    => array_map( 'intval', $term_ids ),
        'operator' => 'IN',
      ),
    ),
  ) );

  return array_map( 'absint', $q->posts );
}

function wpct_get_ids_by_flag( string $meta_key, $meta_value ): array {
  $q = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => -1,
    'fields'         => 'ids',
    'meta_query'     => array(
      array(
        'key'     => $meta_key,
        'value'   => $meta_value,
        'compare' => '=',
      ),
    ),
  ) );
  return array_map( 'absint', $q->posts );
}

Combine logic:

<?php
$keyword   = isset( $_GET['s'] ) ? sanitize_text_field( $_GET['s'] ) : '';
$dt_values = isset( $_GET['dt_field'] ) && is_array( $_GET['dt_field'] ) ? array_map( 'sanitize_text_field', $_GET['dt_field'] ) : array();
$cats      = isset( $_GET['category'] ) && is_array( $_GET['category'] ) ? array_map( 'intval', $_GET['category'] ) : array();

$or_ids = array();

// OR branches
$or_ids = array_merge( $or_ids, wpct_get_ids_by_keyword( $keyword ) );
$or_ids = array_merge( $or_ids, wpct_get_ids_by_meta_or( $dt_values ) );
$or_ids = array_merge( $or_ids, wpct_get_ids_by_tax( $cats ) );

$or_ids = array_values( array_unique( array_map( 'absint', $or_ids ) ) );

// AND filters (optional)
$featured_ids = isset( $_GET['is_featured'] ) && $_GET['is_featured'] === '1'
  ? wpct_get_ids_by_flag( 'is_featured', '1' )
  : null;

$pdf_ids = isset( $_GET['has_pdf'] ) && $_GET['has_pdf'] === '1'
  ? wpct_get_ids_by_flag( 'has_pdf', '1' )
  : null;

// Apply AND over OR result
$final_ids = $or_ids;

if ( $featured_ids !== null ) {
  $final_ids = array_values( array_intersect( $final_ids, $featured_ids ) );
}
if ( $pdf_ids !== null ) {
  $final_ids = array_values( array_intersect( $final_ids, $pdf_ids ) );
}

Finally, run the visible query:

<?php
$paged = max( 1, get_query_var( 'paged' ) );

$query = new WP_Query( array(
  'post_type'      => 'post',
  'post__in'       => ! empty( $final_ids ) ? $final_ids : array( 0 ),
  'orderby'        => 'post__in',
  'posts_per_page' => 20,
  'paged'          => $paged,
) );
?>

This gives you:

  • True OR across dimensions
  • AND constraints applied cleanly
  • Stable result set that pagination can work with

Important: Don’t Return Everything When No Conditions Are Set

If you want “no filters means show nothing”, handle it explicitly:

<?php
$has_any_or = ( $keyword !== '' ) || ! empty( $dt_values ) || ! empty( $cats );
$has_any_and = ( isset( $_GET['is_featured'] ) && $_GET['is_featured'] === '1' )
  || ( isset( $_GET['has_pdf'] ) && $_GET['has_pdf'] === '1' );

if ( ! $has_any_or && ! $has_any_and ) {
  $final_ids = array(); // show nothing / show message
}

Pattern 2: Nested meta_query and tax_query (Works Only Within Each System)

If your “AND/OR” is only inside meta queries or only inside tax queries,
you can use nested relations.

Meta Query Example: (A OR B) AND (C)

<?php
$args = array(
  'post_type'  => 'post',
  'meta_query' => array(
    'relation' => 'AND',
    array(
      'relation' => 'OR',
      array(
        'key'     => 'color',
        'value'   => 'blue',
        'compare' => '=',
      ),
      array(
        'key'     => 'color',
        'value'   => 'green',
        'compare' => '=',
      ),
    ),
    array(
      'key'     => 'in_stock',
      'value'   => '1',
      'compare' => '=',
    ),
  ),
);

This is fine because meta_query supports nested relations.
But it still doesn’t solve keyword + taxonomy grouping reliably.

Pattern 3: Custom SQL Filters (Powerful, But High Risk)

You can combine keyword search with meta and taxonomy by filtering SQL:

  • posts_where
  • posts_search
  • posts_join
  • posts_clauses

This is valid in expert-only cases, but it has costs:

  • Harder to maintain across WP updates
  • Easy to break pagination/count queries
  • Can introduce duplicates without careful GROUP BY
  • Performance can degrade unexpectedly

If you need grouping logic across systems, the ID-merging approach is usually safer.

Performance Notes (What to Watch)

  • Use 'fields' => 'ids' for intermediate queries
  • Use posts_per_page => -1 only for ID collection (not rendering)
  • Cache expensive ID lists if filters are stable (transients or object cache)
  • Avoid meta LIKE when possible (taxonomies scale better)

Also note:

  • Large post__in arrays can become heavy
  • If you routinely return thousands of IDs, consider a custom index table

Debugging Complex Searches

When results look wrong:

  • Log each ID set (keyword IDs, meta IDs, tax IDs)
  • Compare merged set size before applying AND filters
  • Inspect SQL with $query->request
  • Use Query Monitor to confirm executed queries

Summary

  • WordPress can’t naturally group keyword/meta/tax logic into one nested AND/OR tree
  • The most reliable approach is: build OR IDs, then apply AND via intersections
  • Use nested relations only within meta_query or tax_query
  • Custom SQL filters are powerful but fragile
  • For large-scale directory/search builds, consider a custom index table

If you need truly complex boolean logic, treat WordPress as a rendering layer and build your search logic
as a controlled pipeline: compute IDs → apply filters → render posts normally.

Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.