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:
- The orders list does not run
WP_Query. It runsOrdersTableQueryagainstwp_wc_ordersandwp_wc_orders_meta. None of the legacyposts_*filters fire. Anything you registered onposts_searchis dead code. woocommerce_shop_order_search_fieldsis 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.- The billing email lives in two places. HPOS writes the email to a top-level
billing_emailcolumn onwp_wc_ordersand also mirrors it towp_wc_orders_metafor compatibility. If you query the meta table for_billing_email, you might match nothing because HPOS usesbilling_email(no underscore) as the meta key. Or you query the column on a billing-anonymised order and it isnullbecause 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.