Log WooCommerce search queries and the products they return

Published Updated 4 min read

WooCommerce's Searches report shows the terms customers typed on your shop and search pages. It is useful for spotting high-demand queries and comparing volume. It does not show what those searches returned: which products appeared, how many results there were, or whether any of them matched what the customer wanted.

That is where bad results hide. A customer types 'nose ring', sees five ear cuffs and leaves, and the only trace in your analytics is one more tally next to 'nose ring'. From term counts alone you cannot tell whether nobody stocks nose rings, whether you stock them under another name, or whether search is pulling the wrong items.

This plugin adds a log to the WooCommerce admin. Every product search records the term, the total number of results and the names of the first ten products shown. Nothing leaves your server, no JavaScript runs in the browser, and a Clear Log button stops the table growing forever.

What WooCommerce and GA4 record, and what they miss

WooCommerce Analytics keeps search terms under the Searches tab, filterable by date and exportable as a CSV sorted by count. It shows what is popular, how volume moves over time, and which terms spike with outside traffic. For the broad picture of demand it is adequate.

GA4 adds user-level detail when site search tracking is set up: device, acquisition source, session properties. Neither shows which products appeared in a given result set. They record that a search happened, not what the customer found.

SearchWP and Relevanssi replace the search engine and come with their own reporting, including zero-result queries and sometimes conversion rates. This log is for stores on native WooCommerce search that want to see result content without buying a search upgrade.

Wrong results do not show up as zero results. Someone types 'linen shirt', gets ten cotton blouses and leaves, and to your analytics that looks like a successful search. Seeing product names beside the term is the only way to tell the difference without running every query yourself.

Zero-result searches matter too, for different reasons: either the customer wanted something you do not carry, or you carry it under a name that does not match their words. One calls for a new product, the other for renaming an existing one with the words customers use.

Each row has a timestamp, so you can see whether a failing term is a one-off or a habit. Twice with bad results is minor; forty times is lost revenue.

Two hooks and a table

When WooCommerce starts building a product search, woocommerce_product_query passes the WP_Query object and the plugin keeps the search term. When WordPress assembles the results, the posts_results filter lets it record the term and what came back before returning the array untouched. That covers the shop page, category archives and the main WordPress search page when WooCommerce has it return products, which is the default.

Each row stores the time, the term as typed, the total result count and the first ten product titles, comma-separated. Ten keeps rows readable, and the count shows when there were more. The table is created on activation with dbDelta, which also handles schema changes cleanly.

WooCommerce > Search Log shows the 500 most recent searches, newest first, and Clear Log truncates the table in one step. Each search adds one database write and no front-end reads, which is negligible for most stores; on a store with thousands of searches a day, clear the log now and then.

Clicks, AJAX search and repeated terms

Clicks are not captured. You see what appeared, not what the customer chose, and tying searches to purchases needs session tracking; SearchWP and Relevanssi both link queries to downstream sales.

Autosuggest dropdowns and search-as-you-type usually fire their own requests and never pass through woocommerce_product_query. If WooCommerce Product Search, Doofinder or any plugin replaces the search backend entirely, the log records nothing.

Nothing is aggregated: 200 searches for 'silver bracelet' are 200 rows. A follow-up message can add a summary tab grouping terms by frequency with their latest date and average result count, a CSV export, and the referring URL with whether the searcher was logged in.

If native search is simply wrong for your catalogue, the log will show it on almost every query and then stop being useful, because the fix is a better search engine, not a naming change. Stores with several hundred products tend to hit the limits of exact-string matching quickly. Relevanssi (with a free tier) and SearchWP (paid) rebuild the index with stemming, fuzzy matching and relevance scoring.

If you need analytics as well as better search, buy them together: SearchWP and Relevanssi Pro both include zero-result reports and frequent-query dashboards. A log on top of a search engine you plan to replace is maintenance with no lasting value.

What you get

Included

  • 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

Not included

  • 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 the things on the right. This build covers the part most sites use.

Questions

What does the log show for a search with no results?

An empty product list and a count of 0. Those rows are the quickest way to find failing searches: scan the admin table, or query the wc_search_log table directly in your database client.

Build it in Steem

The button opens the builder with this guide's prompt filled in, so you can change anything before it runs. Nothing is built, and nothing is charged, until you send it.

Build this pluginAbout 55 credits. The free plan includes enough for one.
Read the prompt (331 words)

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.