Most WordPress search results pages are blunt instruments. They match query strings against post content and titles, then dump everything into a single chronological list. For a site with a carefully built editorial taxonomy—categories, tags, custom taxonomies, and curated relationships—that default behavior is a quiet disaster. It ignores the structural decisions editors made and treats every piece of content as an undifferentiated blob. The result is a search experience that feels disconnected from the rest of the site, and worse, it trains users to stop trusting the navigation you built.
I’m Jonas Venn, and I spend my days inside WordPress systems engineering. I see this pattern repeat across news sites, documentation portals, and content-heavy membership platforms. The fix isn’t a plugin that promises AI-powered everything. It’s a deliberate rebuild of the search query, the template, and the way results are presented. This article walks through that rebuild step by step, with code you can drop into a child theme or a custom plugin.
Why Default WordPress Search Undermines Editorial Structure
WordPress ships with a search mechanism that runs a simple WP_Query looking at post_title and post_content. It doesn’t consider taxonomy terms, custom fields, or the weight of different content types. If you run a site where a “Guide” custom post type should always rank above a “News” post for certain queries, the default search won’t know that. If you have a hierarchical category structure where “Tutorials > Intermediate” is more relevant than a loosely tagged post, the default search ignores that hierarchy entirely.
This creates a mismatch. Editors spend hours building taxonomies that group, relate, and prioritize content. The search page then bypasses all of that work. Users who click a category link get a beautifully filtered view; users who type the same concept into the search bar get a noisy, flat list. That inconsistency erodes confidence in the site’s information architecture.
Mapping the Search Query to Your Taxonomy Model
Before writing any code, you need to define what a “good” result looks like for your specific content model. This isn’t a one-size-fits-all problem. On a documentation site, a result from the “Reference” custom post type might be more valuable than a blog post. On a recipe site, a result tagged with the exact ingredient the user typed should surface above a post that merely mentions it in passing.
Start by listing your content types and taxonomies. For each, decide:
- Which post types should be searched by default?
- Which taxonomies should be matched against the search query?
- Should taxonomy term archives themselves appear as results?
- What weighting or sorting logic makes sense for your editorial priorities?
Once you have that map, you can modify the main search query using the pre_get_posts action. This hook lets you alter the query before it runs, so you can add post types, set meta queries, and adjust ordering without touching the template.
add_action( 'pre_get_posts', function( $query ) {
if ( ! is_admin() && $query->is_main_query() && $query->is_search() ) {
$query->set( 'post_type', array( 'post', 'guide', 'reference' ) );
$query->set( 'posts_per_page', 20 );
// We'll add taxonomy-aware ordering later
}
});
This snippet expands the search to include custom post types. But it’s still just a content match. To make the search taxonomy-aware, you need to go deeper.
Injecting Taxonomy Term Matching into the Search Logic
WordPress doesn’t natively search taxonomy terms. If a user types “intermediate,” the default search looks for that word in post titles and content. It won’t find posts assigned to the “Intermediate” category unless the word appears in the post body. To fix this, you need to intercept the search query and add a taxonomy condition.
One approach is to use the posts_search filter to modify the SQL WHERE clause, but that’s fragile and hard to maintain. A cleaner method is to run a two-pass search: first, find taxonomy terms that match the query; second, include posts assigned to those terms in the results, weighted appropriately.
Here’s a practical implementation. We’ll hook into pre_get_posts and build a tax query if the search string matches any existing term names:
add_action( 'pre_get_posts', function( $query ) {
if ( ! is_admin() && $query->is_main_query() && $query->is_search() ) {
$search_term = $query->get( 's' );
if ( empty( $search_term ) ) {
return;
}
// Find matching terms across specified taxonomies
$matching_terms = get_terms( array(
'taxonomy' => array( 'category', 'post_tag', 'guide_topic' ),
'name__like' => $search_term,
'fields' => 'ids',
'hide_empty' => false,
) );
if ( ! empty( $matching_terms ) && ! is_wp_error( $matching_terms ) ) {
$tax_query = array(
'relation' => 'OR',
array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $matching_terms,
),
array(
'taxonomy' => 'post_tag',
'field' => 'term_id',
'terms' => $matching_terms,
),
array(
'taxonomy' => 'guide_topic',
'field' => 'term_id',
'terms' => $matching_terms,
),
);
$query->set( 'tax_query', $tax_query );
}
}
});
This code checks if the search string matches any term name in your chosen taxonomies. If it does, it adds a tax_query that pulls in posts assigned to those terms. The OR relation means a post can match via content or via taxonomy assignment. That’s a start, but it still doesn’t prioritize taxonomy-matched posts over content-matched ones. For that, we need to adjust the ordering.
Weighting Results by Editorial Relevance
Default WordPress search orders by date, or by relevance if you’re using MySQL’s fulltext search. Neither respects your editorial taxonomy. To change that, you can use the posts_orderby filter to inject custom sorting logic, or you can run a secondary query that merges and reorders results based on your own scoring system.
I prefer a scoring approach because it gives you full control. Here’s the idea: run the main query to get content-matched posts, then separately query for taxonomy-matched posts. Assign a relevance score to each post based on where the match occurred—title match scores highest, then taxonomy term match, then content match. Merge the results, deduplicate, and sort by score.
This requires overriding the default search template. Create a search.php file in your theme (or better, a child theme) and build the custom query there:
$search_term = get_search_query();
$scored_posts = array();
// Query 1: Title matches (highest score)
$title_query = new WP_Query( array(
'post_type' => array( 'post', 'guide', 'reference' ),
'posts_per_page' => -1,
's' => $search_term,
'search_columns' => array( 'post_title' ),
) );
foreach ( $title_query->posts as $post ) {
$scored_posts[ $post->ID ] = array( 'post' => $post, 'score' => 3 );
}
// Query 2: Taxonomy term matches (medium score)
$matching_terms = get_terms( array(
'taxonomy' => array( 'category', 'post_tag', 'guide_topic' ),
'name__like' => $search_term,
'fields' => 'ids',
'hide_empty' => false,
) );
if ( ! empty( $matching_terms ) && ! is_wp_error( $matching_terms ) ) {
$tax_query = new WP_Query( array(
'post_type' => array( 'post', 'guide', 'reference' ),
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $matching_terms,
),
),
) );
foreach ( $tax_query->posts as $post ) {
if ( ! isset( $scored_posts[ $post->ID ] ) ) {
$scored_posts[ $post->ID ] = array( 'post' => $post, 'score' => 2 );
} else {
$scored_posts[ $post->ID ]['score'] += 2;
}
}
}
// Query 3: Content matches (lowest score)
$content_query = new WP_Query( array(
'post_type' => array( 'post', 'guide', 'reference' ),
'posts_per_page' => -1,
's' => $search_term,
) );
foreach ( $content_query->posts as $post ) {
if ( ! isset( $scored_posts[ $post->ID ] ) ) {
$scored_posts[ $post->ID ] = array( 'post' => $post, 'score' => 1 );
}
}
// Sort by score descending
usort( $scored_posts, function( $a, $b ) {
return $b['score'] - $a['score'];
});
This gives you a $scored_posts array sorted by editorial relevance. A post whose title contains the search term gets a score of 3. A post assigned to a matching taxonomy term gets 2. A post that only matches in content gets 1. If a post matches in multiple ways, the scores add up, naturally boosting the most relevant content to the top.
You can extend this scoring model to include custom fields, post type priority, or even recency. The key is that you define the weights based on your editorial structure, not an opaque algorithm.
Designing the Results Template to Reflect Taxonomy Context
Even with smart ordering, a search results page can still look like a disconnected list. The template needs to show why a result is relevant. That means displaying the taxonomy terms that triggered the match, the post type label, and breadcrumb-style context.
For each result, output the post type as a small label (“Guide,” “Reference,” “Article”) and list the matching taxonomy terms. If a post matched because it’s in the “Intermediate” category, show that category with a link. This reinforces the site’s information architecture and gives users a path to browse related content.
Here’s a loop snippet for the scored results:
if ( ! empty( $scored_posts ) ) :
foreach ( $scored_posts as $item ) :
$post = $item['post'];
setup_postdata( $post ); ?>
No results found for your search. Try a different term or browse our categories.
Notice the fallback message. When a search truly returns nothing, don’t just show a dead end. Link to the category archive or a sitemap-style page. If you have a post that explains common search pitfalls, link to that too. For example, if your site has a known issue with empty archives after a migration, you might point users to a resource like What to Fix First When a New WordPress Site Says Nothing Found. That turns a failed search into a helpful navigation moment.

Handling Faceted Search Without Breaking the Query
If your site uses faceted search—allowing users to filter by post type, category, or tag—you need to integrate those facets with your custom scoring. The danger is that a faceted filter (e.g., “Show only Guides”) will override your carefully weighted query and revert to default date ordering.
The solution is to apply facets as additional query arguments on top of your scored results, not as a replacement query. If you’re using WP_Query directly in the template, you can merge facet parameters into each of your three scoring queries. If you’re using a plugin like FacetWP, you’ll need to hook into its indexing process to preserve your custom relevance scores.
For a custom faceted setup, extend the earlier code to accept query variables from the URL:
$post_type_filter = isset( $_GET['post_type'] ) ? sanitize_text_field( $_GET['post_type'] ) : '';
$category_filter = isset( $_GET['category'] ) ? intval( $_GET['category'] ) : 0;
$base_args = array(
'posts_per_page' => -1,
);
if ( $post_type_filter ) {
$base_args['post_type'] = $post_type_filter;
} else {
$base_args['post_type'] = array( 'post', 'guide', 'reference' );
}
if ( $category_filter ) {
$base_args['cat'] = $category_filter;
}
// Use $base_args in each of the three scoring queries
This keeps the scoring logic intact while respecting the user’s chosen filters. The facets become a lens on top of your editorial relevance model, not a replacement for it.
Caching Considerations for High-Traffic Search Pages
Running three separate WP_Query instances on every search request can get expensive. On a site with thousands of posts, you’ll want to cache the results. WordPress transients are a good fit here. Generate a cache key based on the search term and any active filters, store the scored results array, and serve from cache when possible.
$cache_key = 'search_results_' . md5( $search_term . $post_type_filter . $category_filter );
$scored_posts = get_transient( $cache_key );
if ( false === $scored_posts ) {
// Run the three queries and build $scored_posts
set_transient( $cache_key, $scored_posts, HOUR_IN_SECONDS );
}
Invalidate the cache when new content is published or when taxonomy terms are updated. Hook into save_post and edited_term to delete relevant transients. This keeps the search fresh without hammering the database on every request.

Testing Your Taxonomy-Aware Search
You can’t trust that your scoring logic works until you test it against real editorial content. Create a set of test posts with known titles, taxonomy assignments, and body content. Then run searches that should match in specific ways:
- A search for a term that appears only in a post title should return that post first.
- A search for a category name should return posts in that category, even if the word isn’t in the post content.
- A search that matches both a title and a taxonomy term should rank higher than a content-only match.
- Applying a post type filter should narrow results without breaking the scoring order.
Use WP_DEBUG and query logging during development to see exactly what SQL is being generated. If you’re on a local environment, the Query Monitor plugin is invaluable for spotting slow queries or unexpected tax query joins.
Extending the Model to Custom Taxonomies and Post Types
The code examples above use standard categories and tags, but the same pattern works for any registered taxonomy. If you run a documentation site with a “Software Version” taxonomy, you can include it in the term-matching step. If you have a “Priority” custom field that editors use to mark important content, you can factor that into the scoring.
The important principle is that your search should reflect the same organizational logic that editors use when they structure the site. If a human editor would say “this guide is more relevant than that news post for this query,” your search algorithm should agree. That means sitting down with your editorial team, understanding their taxonomy decisions, and encoding those decisions into the scoring weights.

FAQ
Will this custom search work with the block editor’s site editor?
Yes, but you’ll need to use a child theme or a custom plugin to house the PHP code. The block editor’s full-site editing mode doesn’t support arbitrary PHP in templates, so a classic search.php template in a child theme is the most reliable approach. If you’re committed to FSE, you can build a custom block that runs the scored query and renders results, but that adds complexity.
How do I handle misspellings or partial matches in taxonomy terms?
The name__like parameter in get_terms() does a basic SQL LIKE match, which won’t catch misspellings. For fuzzy matching, you could use the get_terms filter to modify the SQL, or pre-process the search term with similar_text() against your term list. A simpler approach is to use WordPress’s built-in soundex or metaphone functions, but those are limited. For most sites, exact and partial matching covers the majority of user queries.
Does this approach affect the WordPress admin search?
No, the code examples use ! is_admin() checks to ensure they only run on the front end. The admin search will continue to use the default WordPress behavior. If you want to apply similar logic to the admin, you can remove that condition, but be cautious—admin search screens often have their own filtering needs.
Can I use this with a third-party search plugin like Relevanssi?
Yes, but you’ll need to adapt the approach. Plugins like Relevanssi already index taxonomy terms and allow custom weighting. Instead of building your own scoring queries, you’d configure the plugin’s settings to match your editorial priorities. The template design principles still apply: show taxonomy context and post type labels in the results.
Building a search results page that respects your editorial taxonomy isn’t about adding more features. It’s about removing the disconnect between how you organize content and how users find it. The code in this article gives you a foundation. The real work is in defining what relevance means for your specific content model, and then sticking to that definition across every search interaction on your site.