WooCommerce HPOS Order Search by Email Not Working Fix

WooCommerce HPOS order search by customer email returning empty? The legacy postmeta query is bypassed. Add a custom filter against the orders table.
WooCommerceWordPressHPOS
May 15, 20265 min read955 words

The Problem

I ran into this on a client project this week. A logistics shop had migrated to High-Performance Order Storage (HPOS) months ago and their support team finally noticed that searching for an order by the customer's email address in wp-admin > WooCommerce > Orders returned zero results. The same email was clearly visible on the order detail page. Searching by order ID worked. Searching by name worked. Email was empty every time.

If you flipped the HPOS switch, the orders list filter is silently broken on the search-by-email path, and you cannot reproduce it on a fresh install — your custom plugin or a one-line snippet from years ago is filtering the legacy postmeta query that no longer runs. The Orders Table is a different schema and the old hooks miss it entirely.

Why It Happens

Pre-HPOS, the orders list in admin ran a WP_Query against the shop_order post type. The search box hooked into posts_search and joined postmeta on _billing_email. Most extensions and snippets that "made order search better" (Yoast WooCommerce SEO add-ons, WPML order helpers, custom B2B plugins) registered filters on posts_join, posts_where, or woocommerce_shop_order_search_fields.

Three things break together once HPOS is the source of truth:

  1. The orders list does not run WP_Query. It runs OrdersTableQuery against wp_wc_orders and wp_wc_orders_meta. None of the legacy posts_* filters fire. Anything you registered on posts_search is dead code.
  2. woocommerce_shop_order_search_fields is HPOS-aware but ignored when something earlier returns results. If a third-party plugin short-circuited the legacy filter with a non-empty array, HPOS skips its native email search and trusts the plugin's intent. You get an empty result set instead of a fallback.
  3. The billing email lives in two places. HPOS writes the email to a top-level billing_email column on wp_wc_orders and also mirrors it to wp_wc_orders_meta for compatibility. If you query the meta table for _billing_email, you might match nothing because HPOS uses billing_email (no underscore) as the meta key. Or you query the column on a billing-anonymised order and it is null because the customer was a guest.

The HPOS developer reference confirms the column-vs-meta split. The fix is to register an HPOS-specific search filter that targets the orders table directly.

The Fix

You need to do two things: remove or guard any legacy posts_search filters that target _billing_email, and add a new filter on woocommerce_orders_table_search_orders_meta_query that joins the orders table on the email column with a LIKE clause.

Step 1: Find and disable the legacy filter. Grep your custom plugins and the active theme's functions.php for these strings:

grep -rn "posts_search\|posts_join.*_billing_email\|woocommerce_shop_order_search_fields" \
  wp-content/plugins/ wp-content/themes/[active-theme]/

Anything you find that wraps _billing_email needs to either be deleted (if it was a workaround for an HPOS-era version of WooCommerce) or wrapped in a compatibility check so it only runs when HPOS is off:

<?php
use Automattic\WooCommerce\Utilities\OrderUtil;

if ( ! OrderUtil::custom_orders_table_usage_is_enabled() ) {
    add_filter( 'woocommerce_shop_order_search_fields', 'acme_legacy_search_fields' );
}

If you skip this step, the legacy filter still returns [] on HPOS sites and short-circuits the native search.

Step 2: Register an HPOS-aware search filter. Drop this in a small mu-plugin so it loads early:

<?php
add_filter(
    'woocommerce_orders_table_search_orders_meta_query',
    function ( $args, $query ) {
        global $wpdb;
        $term = isset( $query->get_query_vars()['s'] )
            ? trim( (string) $query->get_query_vars()['s'] )
            : '';

        if ( $term === '' || strpos( $term, '@' ) === false ) {
            return $args;
        }

        $orders_table = $wpdb->prefix . 'wc_orders';
        $like         = '%' . $wpdb->esc_like( $term ) . '%';

        $matching = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT id FROM {$orders_table}
                 WHERE billing_email LIKE %s
                 LIMIT 500",
                $like
            )
        );

        if ( ! empty( $matching ) ) {
            $args['order_id_in'] = array_map( 'intval', $matching );
        }

        return $args;
    },
    10,
    2
);

The hook name is woocommerce_orders_table_search_orders_meta_query, not woocommerce_orders_table_search_query. The latter is for column-only searches and does not give you the assembled meta join. The order_id_in key is the documented way to inject pre-filtered IDs without rewriting the SQL by hand.

The strpos( $term, '@' ) guard prevents this filter from firing on every search. Only requests that look like an email run the extra query. Without it you triple every order list page load.

Step 3: Verify with the WP-CLI HPOS tool. Run a search from the command line so you can see the SQL:

wp wc shell
> $query = new \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableQuery( [
>     's' => 'jane@acme.test',
>     'limit' => 10,
> ] );
> var_dump( $query->orders );

If the return is a populated array of order IDs, the filter is wired. If it is empty, double-check that $term reaches your callback (error_log) and that the billing_email column actually has the value (SELECT billing_email FROM wp_wc_orders WHERE id = X).

The Lesson

HPOS is a schema migration, not a flag. Anything in your codebase that touched the legacy posts_* filters or _billing_email postmeta needs to be re-implemented against wp_wc_orders and the OrdersTableQuery filters. The native WooCommerce search works on a clean install. The bugs come from old extension code that quietly disables it.

If your store migrated to HPOS and the admin list, custom reports, or background jobs have stopped finding orders the way they used to, that is the kind of audit I run. See my services. For another HPOS-era pattern I covered, see WooCommerce HPOS custom queries fix.

Back to blogStart a project