Log WooCommerce search queries and the products they return: a plugin you can build today
· 5 min read
WooCommerce ships with a Searches report that shows which terms customers typed on your shop and search pages. It is useful for spotting high-demand queries and comparing volume across terms. It does not show what those searches returned: which products appeared, how many results a query found, or whether any of them were relevant to what the customer asked for.
That gap is where bad search results hide. A customer who types 'nose ring' and sees five ear cuffs will leave without buying, and the only trace in your analytics is one more tally next to 'nose ring'. You cannot tell from term counts whether the problem is that nobody carries nose rings, that you carry them under a different product name, or that your search results are pulling completely wrong items.
The plugin here adds a log table to your WooCommerce admin. Every time a customer runs a product search, it records the term, the total number of results, and the names of the first ten products that appeared. Nothing is sent off-site, no JavaScript runs in the browser, and a one-click Clear Log button keeps the table from growing indefinitely. The log turns a blind spot into a list of queries you can act on.
What one build gives you
- Log of every product search: term searched, result count, and names of the first ten products returned
- Admin table at WooCommerce > Search Log showing the 500 most recent searches
- Database table created on activation with dbDelta so no manual setup is required
- Clear Log button to truncate the table and keep it manageable
What it does not do
- Click-through tracking: which result the customer selected after seeing the list
- Conversion attribution: whether the search led to a purchase
- AJAX search suggestions, search-as-you-type dropdowns, or third-party search plugins that bypass the native WooCommerce product query
- Grouped summary by term or frequency ranking in the initial build
- CSV export from the admin table without the follow-up prompt
SearchWP does these. This covers the part most sites use.
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
Build a WordPress plugin called WC Search Log. Plugin Name: WC Search Log. Version: 1.0.0. Description: Log WooCommerce product searches and the products returned, visible under WooCommerce > Search Log. Wrap all code in a class WC_Search_Log with a public init() method and a private string property $pending_term initialised to empty string. Instantiate the class and call init() on plugins_loaded. Database setup: Register register_activation_hook(__FILE__, ['WC_Search_Log', 'create_table']). In the static method create_table: global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table = $wpdb->prefix . 'wc_search_log'; $sql = "CREATE TABLE $table ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, searched_at DATETIME NOT NULL, search_term VARCHAR(200) NOT NULL, result_count INT NOT NULL DEFAULT 0, top_products TEXT NOT NULL DEFAULT '', PRIMARY KEY (id) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); Search capture: In init(), add_action('woocommerce_product_query', [$this, 'on_product_query']). In on_product_query(WP_Query $q): if (!empty($q->get('s'))) { $this->pending_term = sanitize_text_field($q->get('s')); } In init(), add_filter('posts_results', [$this, 'on_posts_results'], 10, 2). In on_posts_results($posts, $query): if (empty($this->pending_term)) { return $posts; } $term = $this->pending_term; $this->pending_term = ''; $count = count($posts); $top = array_slice($posts, 0, 10); $names = implode(', ', array_map(function($p) { return $p->post_title; }, $top)); global $wpdb; $wpdb->insert($wpdb->prefix . 'wc_search_log', ['searched_at' => current_time('mysql'), 'search_term' => $term, 'result_count' => $count, 'top_products' => $names], ['%s', '%s', '%d', '%s']); return $posts; Admin page: In init(), add_action('admin_menu', [$this, 'add_admin_page']). In add_admin_page(): add_submenu_page('woocommerce', 'Search Log', 'Search Log', 'manage_woocommerce', 'wc-search-log', [$this, 'render_admin_page']). In render_admin_page(): first check if (isset($_POST['wc_search_log_clear']) && wp_verify_nonce($_POST['_wpnonce'] ?? '', 'wc_search_log_clear')) and if true: global $wpdb; $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wc_search_log"); wp_safe_redirect(admin_url('admin.php?page=wc-search-log&cleared=1')); exit; Then output: <div class="wrap"><h1>Search Log</h1>. If isset($_GET['cleared']): output <div class="updated notice"><p>Log cleared.</p></div>. Output <form method="post">, wp_nonce_field('wc_search_log_clear'), <button type="submit" name="wc_search_log_clear" class="button">Clear Log</button>, </form>. Then global $wpdb; $rows = $wpdb->get_results("SELECT searched_at, search_term, result_count, top_products FROM {$wpdb->prefix}wc_search_log ORDER BY searched_at DESC LIMIT 500"); if (empty($rows)) echo '<p>No searches logged yet.</p>'; else output a <table class="wp-list-table widefat fixed striped"> with thead columns Date, Search term, Results, Products shown, and a tbody row per result: td searched_at, td esc_html(search_term), td result_count, td esc_html(top_products). Close table and div. No settings page, no stylesheet, no script.
What the built-in search reporting covers
WooCommerce Analytics groups search data under the Searches tab. You can filter by date range and export a CSV of terms sorted by usage count. This tells you what is popular, how search volume changes across dates, and which terms appear in bursts that correlate with external traffic spikes. For a broad picture of search demand it is adequate.
GA4 extends this with user-level data when site search tracking is configured. You can segment by device, acquisition source, and session properties. Neither WooCommerce Analytics nor GA4 shows you what products appeared in any given result set. They record that the query happened, not what the customer found.
SearchWP and Relevanssi, both search replacement plugins, include their own reporting panels. If you are already running one of those, their dashboards show zero-result queries and sometimes conversion rates. The log plugin here is for stores running native WooCommerce search that want to see result content without buying a search upgrade.
Why seeing the results matters
A search that returns the wrong products is not a zero-result problem and will not surface as one. A customer who types 'linen shirt' and receives ten cotton blouses sees a full results page and leaves without buying. That looks identical to a successful search in your analytics. Seeing the product names alongside the term is the only way to tell the difference without manually running every query yourself.
Zero-result searches are equally useful. When result_count is 0, you know the customer looked for something you do not carry, or that you carry under a name that does not match how they described it. Both are worth different actions: adding a product, or renaming an existing one to include the words customers use. The log surfaces these without any manual audit.
Frequency matters too. A term that appears twice with bad results is a minor issue. The same term appearing forty times is a revenue problem. The log table shows the date and time of each search, so you can see whether a failing term is recurring and how often.
How the plugin captures searches
The plugin adds two hooks. When WooCommerce begins building a product search query, it fires the woocommerce_product_query action and passes the WP_Query object. The plugin reads the search term from that object and stores it temporarily. When the query finishes and WordPress assembles the result set, the plugin intercepts the posts via the posts_results filter, records the term and results, and returns the array unchanged.
Each log row stores four things: the time the search happened, the search term as entered, the total result count, and a comma-separated list of the first ten product titles that appeared. The list is truncated at ten to keep the database row readable; the total count shows whether there were more. The table is created on plugin activation using WordPress's dbDelta function, which handles schema updates cleanly if the table already exists.
The admin page sits under WooCommerce > Search Log and shows the 500 most recent searches, newest first. The Clear Log button truncates the table in one step. There is no pagination, date filter, or export in the initial build. The follow-up prompt in the final section adds a grouped summary view and CSV export.
What the log does not cover
Click-through is not captured. The log shows which products appeared but not which one the customer clicked. Connecting search results to purchase conversions requires session tracking, which this plugin does not do. For that level of attribution, SearchWP and Relevanssi both include analytics that link queries to downstream purchases.
AJAX-powered search suggestions and search-as-you-type dropdowns are not captured. These typically issue their own queries via fetch or XMLHttpRequest and do not go through the woocommerce_product_query hook the plugin uses. If your store runs WooCommerce Product Search, Doofinder, or any plugin that replaces the search backend, this log will capture nothing.
The log does not aggregate. Each search is its own row. If 'silver bracelet' was searched 200 times, you will see 200 rows. Spotting patterns requires scrolling or exporting and filtering manually. The follow-up prompt adds a summary view that groups by term and shows the most common queries at a glance.
When to use a dedicated search plugin instead
If native WooCommerce search is not returning relevant results for your catalog size, the log will show you exactly that and then stop being useful: every query will show bad results, and the fix is not a naming change but a better search engine. Stores with several hundred products typically hit the limits of exact-string matching quickly. Relevanssi (free tier available) and SearchWP (paid) both rebuild the search index with stemming, fuzzy matching, and relevance scoring.
If you need search analytics alongside a search upgrade, buying the full solution is more efficient. Both SearchWP and Relevanssi Pro include zero-result reporting and high-frequency query dashboards. Installing a log on top of a search engine you are planning to replace adds maintenance with no long-term value.
Paste the prompt below into Steem to build the plugin. To extend the log with a summary view grouped by term and a CSV export, the follow-up is: 'Add a Summary tab to WC Search Log that shows each unique search term alongside the total number of times it was searched, the most recent date, and the average result count. Order by frequency descending. Add a CSV export button that downloads the full raw log as a file with columns Date, Term, Results, Products shown.'
Questions
- Does it capture searches on the main WordPress search page, or only on the WooCommerce shop page?
Both. The plugin hooks into woocommerce_product_query, which fires whenever WooCommerce builds a product result set. That includes the shop page, category archives, and the main WordPress search results page when WooCommerce has configured it to return products, which is the default for WooCommerce stores.
- What appears in the Products shown column when a search returns zero results?
The column will be empty and the result count will be 0. Rows where result_count is 0 are the quickest way to identify failing searches. You can spot them by scanning the admin table or querying the wc_search_log table directly in your database client.
- Will logging every search slow the store down?
The plugin adds one database write per search. For most stores this is negligible. There is no read query on the front end and no caching overhead. If your store handles thousands of searches per day, clear the log periodically to keep the table small and admin queries fast.
- Does it work if I have an AJAX search plugin installed?
No. Third-party search plugins that power autosuggest dropdowns or search-as-you-type typically run their own query backend and bypass the woocommerce_product_query hook. The log will capture standard page-load searches but not AJAX-driven ones. If you replace native WooCommerce search entirely, the log will capture nothing.
- Can I see which page or URL the search came from?
Not in the initial build. The follow-up prompt in the guide extends the log with the referring URL and a note on whether the searcher was a logged-in user or a guest. Those two fields let you distinguish searches from the shop page versus the site search bar and separate customer behaviour from admin testing.
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
Build a WordPress plugin called WC Search Log. Plugin Name: WC Search Log. Version: 1.0.0. Description: Log WooCommerce product searches and the products returned, visible under WooCommerce > Search Log. Wrap all code in a class WC_Search_Log with a public init() method and a private string property $pending_term initialised to empty string. Instantiate the class and call init() on plugins_loaded. Database setup: Register register_activation_hook(__FILE__, ['WC_Search_Log', 'create_table']). In the static method create_table: global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table = $wpdb->prefix . 'wc_search_log'; $sql = "CREATE TABLE $table ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, searched_at DATETIME NOT NULL, search_term VARCHAR(200) NOT NULL, result_count INT NOT NULL DEFAULT 0, top_products TEXT NOT NULL DEFAULT '', PRIMARY KEY (id) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); Search capture: In init(), add_action('woocommerce_product_query', [$this, 'on_product_query']). In on_product_query(WP_Query $q): if (!empty($q->get('s'))) { $this->pending_term = sanitize_text_field($q->get('s')); } In init(), add_filter('posts_results', [$this, 'on_posts_results'], 10, 2). In on_posts_results($posts, $query): if (empty($this->pending_term)) { return $posts; } $term = $this->pending_term; $this->pending_term = ''; $count = count($posts); $top = array_slice($posts, 0, 10); $names = implode(', ', array_map(function($p) { return $p->post_title; }, $top)); global $wpdb; $wpdb->insert($wpdb->prefix . 'wc_search_log', ['searched_at' => current_time('mysql'), 'search_term' => $term, 'result_count' => $count, 'top_products' => $names], ['%s', '%s', '%d', '%s']); return $posts; Admin page: In init(), add_action('admin_menu', [$this, 'add_admin_page']). In add_admin_page(): add_submenu_page('woocommerce', 'Search Log', 'Search Log', 'manage_woocommerce', 'wc-search-log', [$this, 'render_admin_page']). In render_admin_page(): first check if (isset($_POST['wc_search_log_clear']) && wp_verify_nonce($_POST['_wpnonce'] ?? '', 'wc_search_log_clear')) and if true: global $wpdb; $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}wc_search_log"); wp_safe_redirect(admin_url('admin.php?page=wc-search-log&cleared=1')); exit; Then output: <div class="wrap"><h1>Search Log</h1>. If isset($_GET['cleared']): output <div class="updated notice"><p>Log cleared.</p></div>. Output <form method="post">, wp_nonce_field('wc_search_log_clear'), <button type="submit" name="wc_search_log_clear" class="button">Clear Log</button>, </form>. Then global $wpdb; $rows = $wpdb->get_results("SELECT searched_at, search_term, result_count, top_products FROM {$wpdb->prefix}wc_search_log ORDER BY searched_at DESC LIMIT 500"); if (empty($rows)) echo '<p>No searches logged yet.</p>'; else output a <table class="wp-list-table widefat fixed striped"> with thead columns Date, Search term, Results, Products shown, and a tbody row per result: td searched_at, td esc_html(search_term), td result_count, td esc_html(top_products). Close table and div. No settings page, no stylesheet, no script.
Steem