WordPress

Your Search Box Can’t Find Your Own Products: A WordPress Search Results Guide

13 min read

Every WordPress site ships with a search box, and almost nobody ever opens it on purpose. It sits in the header or the sidebar, inherited from the theme, doing whatever the theme’s default template says it should do. Then a visitor who’s already decided to buy from you types in your flagship product name, gets “Nothing Found,” and leaves.

The frustrating part is that WordPress core’s search isn’t broken. It’s just narrow in ways that aren’t documented anywhere the average site owner will trip over them. It reads three database columns and no others. It ranks results with a hard-coded relevance ladder that quietly gives up once your query gets long. And since WordPress 5.7 it tells search engines not to index the results page at all, which is correct behavior that occasionally surprises people who were hoping those pages would rank.

This guide covers what core’s search actually does under the hood, why your on-site results look thin, how to widen them without breaking anything, and what Google wants you to do with internal search URLs. Everything here was checked against WordPress core source, not against a plugin’s marketing page.


What WordPress Search Actually Searches

When someone submits your search form, WordPress builds a WP_Query with an s parameter, splits the phrase into terms, and assembles a pile of SQL LIKE comparisons. The important detail is what it compares those terms against.

Three columns. That’s the whole list.

Core defines its default search columns as post_title, post_excerpt, and post_content. Nothing else. Not custom fields, not taxonomy terms, not attachment metadata, not the SKU your store keeps in a meta key, not the author’s name. If a word only exists in a field WordPress doesn’t consider part of the post, the search behaves as though the word doesn’t exist on your site.

Since WordPress 6.2 there’s a post_search_columns filter that lets you change which of those columns get searched. It’s genuinely useful, but read the source carefully before you plan around it: core runs your filtered list through array_intersect() against the same three defaults, so the filter can narrow the search but cannot widen it. Return ['post_title', 'my_custom_field'] and you get a title-only search. Return only a column core doesn’t support and the intersection comes back empty, at which point core quietly restores all three defaults rather than searching nothing. Adding a fourth column takes a different tool entirely, which we’ll get to.

The last cleanup we did on a client’s search was exactly this failure. Their whole catalog had been built in a page builder, with the product names living in a custom field the theme rendered separately from the post body. Search for the product by name and WordPress returned nothing, because the string genuinely wasn’t in any of the three columns it reads. Nobody had broken anything. The site had simply never been able to find its own products.

WHY IT MATTERS

A visitor who uses your search box has already told you they want something specific. They’re further down the funnel than someone browsing your nav. Handing that person an empty results page is a much more expensive miss than a slightly slow homepage, and it’s the one failure your analytics dashboard is least likely to surface unless you go looking for it.

Which post types are even eligible

Every post type carries an exclude_from_search setting. If you don’t specify it when calling register_post_type(), core derives it from the public argument: a public post type is included in search, a non-public one is excluded. That default is sensible, but it means a custom post type registered by a plugin author who set public to false and then bolted on their own front-end templates will be invisible to your search box, and the setting responsible is one you never typed.

Worth checking on any site where a plugin owns a chunk of your content. Testimonials, team members, portfolio items, and event listings are the usual suspects.

How Core Decides What Ranks First

WordPress doesn’t just return matches in date order and call it a day, though that’s what a lot of people assume. There’s a relevance ladder built into WP_Query, and it’s worth understanding because its edges explain most of the weird result orderings people report.

The six-rung ladder

For a multi-word query, core builds a SQL CASE statement that buckets every matching post into one of six tiers, then sorts by tier. From best to worst:

  • The full phrase appears in the post title
  • Every search word appears in the title, in any order
  • Any search word appears in the title
  • The full phrase appears in the excerpt
  • The full phrase appears in the post content
  • Everything else that matched at all

Titles dominate, which is why a post whose title happens to contain a common word can outrank a post that’s genuinely about the topic. It also means your excerpt is doing quiet double duty: it’s a ranking field, not just the blurb on your archive page.

The ladder gives up on long queries

Core includes a comment in the source calling this a sanity limit, and it’s exactly that. Once a query has seven or more terms, WordPress skips the “all words in title” and “any word in title” tiers entirely, on the reasoning that few searches run that long and most titles won’t contain seven specific words anyway. At ten or more terms, or when a query is nothing but short words and stopwords, core stops splitting it at all and matches the whole string as a single phrase.

That’s a defensible engineering trade-off for a general-purpose CMS. It’s also why a visitor who pastes a full sentence from your PDF into the search box tends to get nothing back.

QUICK REFERENCE

Searched by default: post title, post excerpt, post content.
Not searched: custom fields, taxonomy terms, comments, author names, attachment metadata.
Filter to narrow columns: post_search_columns (WordPress 6.2+, cannot add new columns).
Filter to rewrite the SQL: posts_search.
Filter to change query args: pre_get_posts.
Template file: search.php, with the form in searchform.php.

Fixing the Common Failures

You don’t need a search plugin for most of this. Three or four small changes cover the majority of what people complain about.

Give every post an excerpt

Since the excerpt sits on its own rung of the relevance ladder and is one of only three searched columns, an empty excerpt is a wasted ranking field on every post you publish. Most sites leave it blank because the theme auto-generates a trimmed version for archives, and the auto-generated one is derived at display time rather than stored, so it does nothing for search. Writing a real excerpt is unglamorous and it measurably improves on-site results.

Restrict search to the post types that matter

On a site with a dozen registered post types, a search for a common word can return a mess of reusable blocks, form entries, and page-builder templates alongside the two blog posts the visitor wanted. Hook pre_get_posts, confirm you’re on the main front-end search query, and set post_type to the handful you actually want people to find. It’s five lines and it does more for perceived quality than any relevance tuning.

The guard matters more than the filter. Check is_admin(), is_search(), and is_main_query() before touching anything, or you’ll change the results of every secondary query on the site, including ones running in the dashboard.

Handle the empty result properly

A “Nothing Found” page with no next step is the most common search failure and the easiest to fix. Your search.php template should, at minimum, echo back what the person searched for, keep the search form on the page so they can refine without hitting the back button, and offer somewhere to go — recent posts, your main categories, a contact link. Some of the best converting search pages we’ve built end with a plain sentence offering to just answer the question directly.

Customize the form, not just the results

get_search_form() looks for a searchform.php file in your child theme first, then the parent, before falling back to core’s markup. That’s the clean place to add a placeholder, a hidden post_type field, or the ARIA labelling that core’s default form doesn’t include. If you can’t add a template file, the get_search_form filter lets you rewrite the markup from a plugin instead.

When you actually do need a search plugin

Widening the search past those three columns means either writing raw SQL through the posts_search filter or handing the job to something built for it. Custom fields, taxonomy terms, typo tolerance, weighted relevance, and stemming all fall on the far side of that line. If a meaningful share of your revenue moves through the search box, a dedicated index is worth the cost. If search is a convenience feature on a fifty-page brochure site, tune the excerpts and move on.

Search Results Pages and SEO

This is where a lot of sites accumulate damage quietly, because internal search URLs multiply without anyone creating them.

Core already noindexes your results, and has since 5.7

WordPress 5.7 introduced a wp_robots filter system and registered wp_robots_noindex_search() against it by default. On any search results page, core emits a robots meta tag telling compliant crawlers not to index it. You don’t have to configure this and most SEO plugins leave it alone.

People occasionally want the opposite, reasoning that a results page for a valuable term would be a useful landing page. It’s a bad trade. Those pages are generated from a template with no unique writing on them, and anyone with a query string can create them in unlimited variety. Google’s spam policies don’t name internal search results pages specifically, but they do describe doorway abuse as creating substantially similar pages that are closer to search results than a clearly defined, browseable hierarchy. Internal search URLs sit uncomfortably close to that description, which is why they’re so commonly disallowed. If a query is valuable enough to rank for, the right answer is a real page about it.

Google would rather you blocked the crawl than the index

There’s a distinction here that trips up a lot of otherwise careful SEO work. A noindex tag keeps a URL out of the index, but Google still has to fetch the page to discover the tag. Its crawl budget documentation is explicit about this: don’t use noindex to save crawl budget, because Google will still request the page and then drop it once it sees the directive, wasting the crawl either way. For URLs you don’t want crawled at all, robots.txt is the tool.

Be careful about the reason you give yourself, though. Google scopes that crawl budget guidance explicitly to very large or very rapidly changing sites, and opens it by telling everyone else they don’t need to read it. It also warns that blocking URLs won’t hand the freed budget to your other pages unless you were already hitting your crawl capacity limit. On an ordinary small site, a Disallow rule matching your search URL pattern isn’t buying you crawl budget. It’s keeping low-value URLs out of the index and saving your own server the work, which is exactly the rationale Google gives on its faceted navigation page, where it endorses disallowing filter query strings because there’s often no good reason to allow crawling of filtered items.

One order-of-operations trap: don’t stack a robots.txt block on top of a page you also need deindexed. Google’s own indexing documentation is explicit that a blocked crawler never sees the noindex rule, so the page can still surface in results if something links to it. Deindex first, block second, and give it time in between.

Redirects behave differently on search pages

One core quirk worth knowing if you’re debugging odd URL behavior: redirect_canonical(), the function responsible for WordPress quietly fixing malformed URLs, explicitly skips search pages. Whatever URL shape your search produces is the URL that stays in the address bar, trailing slash inconsistencies and all. If you’re comparing search URLs in a log file and the forms don’t match, that’s why.

Empty and no-result queries

Google’s guidance on faceted navigation is a good model for any filtered listing: when a combination genuinely has no results, return a 404 rather than redirecting to a generic error page, so both users and crawlers get an honest signal at the URL they requested. WordPress search doesn’t do that natively — a zero-result search returns a 200 with your search.php template — and for a human-facing search box, a helpful 200 page is arguably the better experience. The lesson to carry over is narrower: don’t let empty and near-empty result URLs become indexable, crawlable inventory. Core’s built-in noindex handles most of that for you.

Auditing What People Actually Search For

Everything above is mechanics. The higher-leverage move is finding out what your visitors type in, because that list is the least filtered feedback your site will ever give you.

Internal site search terms are pure demand signal. Nobody types into a search box to be polite. If forty people a month search your site for a service you stopped offering, or for a spec sheet you keep as a PDF nobody links to, you’ve learned something that no keyword tool would have told you — those people are already on your site and already interested.

Analytics platforms can capture the query string parameter as a site-search dimension; on WordPress that’s s by default. You can also log searches yourself with a small pre_get_posts hook that records the term and the result count. The result count is the part most setups skip, and it’s the most valuable column: sorting by “searched often, returned nothing” gives you a prioritized content backlog assembled entirely by your own audience.

Once you have that list, the fixes usually sort into three piles. Terms with no matching content become new pages. Terms that match content the search couldn’t see become an excerpt fix or a widened index. Terms that are just synonyms for something you do have become a redirect, an alias, or a sentence added to the page so the word exists somewhere searchable.

THE 20-MINUTE AUDIT

Open your own site and search for your five most important terms: your top service, your flagship product, the thing people call you about, your own brand name, and a phrase from your most-read post. Note which ones return nothing, which return the wrong thing first, and which return a wall of undifferentiated results. That short list is your whole to-do list, and it takes about as long as reading this article.

Frequently Asked Questions

Core search compares your query against three database columns only: the post title, the post excerpt, and the post content. Text that a theme or page builder renders from a custom field, a taxonomy term, or a shortcode is visible to the reader but is not stored in any of those three columns, so WordPress cannot find it. Widening the search past those columns requires either the posts_search filter or a dedicated search plugin.

No. The post_search_columns filter was added in WordPress 6.2 and lets you choose which of the supported columns are searched, but core then intersects your returned list with the three defaults. Any column you add that is not post_title, post_excerpt, or post_content is discarded. The filter is for narrowing a search, for example to titles only, not for extending it.

Generally no, and WordPress already handles it for you. Since version 5.7, core registers a function called wp_robots_noindex_search that adds a noindex directive on search results pages by default. Those pages carry no unique content, can be generated in unlimited variety from a query string, and are the kind of low-value inventory Google discourages. If a search term is valuable enough to rank for, build a real page about it instead.

They do different jobs. A noindex directive keeps a URL out of the index but Google still fetches the page to read the tag, which is why Google’s crawl budget documentation advises against using noindex as a crawl-saving measure. A robots.txt disallow rule prevents the crawl in the first place. Do not stack both on a page you need removed from the index, because a blocked crawler cannot read the noindex directive it needs to see. Remove it from the index first, then block the crawl.

For a multi-word query, core builds a SQL CASE statement that sorts matches into tiers: the full phrase in the title ranks highest, then all search words in the title, then any search word in the title, then the full phrase in the excerpt, then the full phrase in the content, and finally everything else that matched. Titles dominate the ranking, and the excerpt is a genuine ranking field rather than just archive decoration.

Core applies deliberate limits to long queries. Once a search reaches seven or more terms, it stops applying the title-matching relevance tiers, on the assumption that few titles contain that many specific words. At ten or more terms, or when a query consists only of short words and stopwords, WordPress stops splitting the query and matches the entire string as one phrase, which rarely appears verbatim in any post. Pasting a full sentence into a WordPress search box therefore tends to return nothing.

Run the twenty-minute audit above on your own site this week. If your search box can’t find your best page, neither can the visitor who came looking for it.

Built by amplifi.studio — see also our guide to WordPress noindex and robots.txt.