How I Optimized Better Search Queries
Complex WordPress searches can spend more time joining related tables than finding matching posts. I saw this in Better Search when a query searched post meta, taxonomies, comments, and multiple terms at once.
The changes I made in Better Search 4.4.3 reduce unnecessary database work while keeping the existing relevance model intact.

Replacing row-multiplying joins
The older search path joined tables such as wp_postmeta, wp_term_relationships, and wp_comments whenever those fields were enabled. That creates one result row for every matching meta value, term, or comment. Better Search then has to group or deduplicate those rows before returning posts.
For a filter that only needs a yes-or-no answer, that work is unnecessary. The query now uses correlated EXISTS and NOT EXISTS subqueries in the FULLTEXT path:
EXISTS can stop after the first matching related row. It also avoids multiplying post rows, reducing grouping and duplicate work. This is why it can be faster on searches involving tables with many related rows. It doesn’t guarantee that every query will be faster. The result depends on the data, indexes, cache state, and the rest of the WordPress request.
The indexed title and content search still uses MATCH ... AGAINST. The change is limited to the supporting filters.
Fixing negative searches
Negative terms now travel through a separate path. For font -plugin, font remains in the positive FULLTEXT expression and plugin is applied as an exclusion across the enabled fields:
This also covers taxonomy terms, comments, authors, slugs, custom tables, and multisite search. Fuzzy LIKE fallback keeps the exclusions instead of replacing the complete WHERE clause.
Negative-only searches such as -plugin no longer leave an empty FULLTEXT expression that returns no results. There is no positive term to provide relevance in that case, so the custom-table path uses a zero score, and the query falls back to the normal result ordering.
The behavior change is intentional. In 4.4.2, searches such as -plugin and font -plugin could still return posts containing the excluded term. The current code removes those false matches rather than hiding valid results.
Smaller costs outside the main query
The search query was not the only source of avoidable work.
Live search now enforces the minimum character setting on the server, limits the request to 128 characters, caches responses per query, locale, and site, and sets no_found_rows because an autocomplete response does not need a total result count.
Heatmap counts are cached for 15 minutes by default. Both cache periods can be changed with bsearch_live_search_cache_time and bsearch_heatmap_cache_time.
The Pro spelling dictionary now rebuilds in ID-based batches instead of loading every published title into memory. It builds a replacement table first and swaps it into place only when the rebuild succeeds, so the existing dictionary remains available during the rebuild. Repeated saves use INSERT IGNORE, which prevents editing the same post from inflating word frequencies between scheduled rebuilds.
Custom-table result counts also skip relevance-score calculation when no relevance threshold is active. The previous count query could calculate MATCH() again over the full result set even though the score could not affect the count.
What I tested
The comparison was wider than a single plugin search. I tested the query paths that tend to behave differently:
- Vanilla FULLTEXT searches using title and content, such as
pluginandfont. - Searches that include post meta, taxonomy names, comments, authors, excerpts, and slugs.
- Positive and negative combinations, such as
plugin -sherlockandfont -plugin. - Negative-only searches, such as
-pluginandsherlock -plugin. - The Pro custom-table path, including the custom count query and searches across multiple
blog_idvalues. - Multisite network search, where Better Search builds a query across the configured sites.
- Fuzzy and LIKE fallback paths, where exclusions must not be lost when FULLTEXT returns no match.
For the performance comparison, I use GPT Luna in XHigh mode to first install 4.4.2, record a baseline, deploy the 4.4.3 candidate, and repeat the same searches. It used median timings rather than relying on one warm or cold run. Result IDs were also compared.
| Query | 4.4.2 median | Candidate median | Change | Results |
|---|---|---|---|---|
Vanilla: sherlock | 363 ms | 358 ms | 1.4% faster | Same |
Vanilla: plugin | 407 ms | 400 ms | 1.7% faster | Same |
Meta: plugin | 2,142 ms | 397 ms | 81.5% faster | Same |
Meta: horrid | 2,352 ms | 438 ms | 81.4% faster | Same |
Comments: plugin | 700 ms | 376 ms | 46.3% faster | Same |
Comments: horrid | 746 ms | 409 ms | 45.2% faster | Same |
Complex: sherlock plugin | 7,551 ms | 375 ms | 95.0% faster | Same |
Complex: horrid plugin | 7,547 ms | 377 ms | 95.0% faster | Same |
Custom tables: plugin | 302 ms | 290 ms | 4.0% faster | Same |
Network: plugin | 4,002 ms | 622 ms | 84.4% faster | Same |
Network: sherlock plugin | 5,626 ms | 1,014 ms | 82.0% faster | Same |
Network: horrid plugin | 5,291 ms | 925 ms | 82.5% faster | Same |
This shows where the optimization matters. Vanilla FULLTEXT searches were already using the indexed post fields, so they changed very little. Meta searches improved by more than 81 percent because the candidate avoids the row multiplication and grouping cost from joining wp_postmeta. Comment searches improved by roughly 45 percent for the same reason.
The largest gains appeared in complex searches that combine multiple fields. The custom-table result moved only 4 percent because that path already searches a dedicated index table. Network searches improved by more than 82 percent because the old work was repeated across the sites included in the network query.
Every benchmark returned the same result IDs. We checked the negative-search tests separately for correctness across native, custom-table, and network paths. They confirm that exclusions are preserved without changing the valid positive matches.
These are median query timings from the comparison run, not a promise that every site will see the same numbers. Results depend on table size, indexes, database version, cache state, enabled search fields, and the number of sites in a network.
The result is less unnecessary joining, safer negative search handling, lower memory use during dictionary rebuilds, and measurable improvements in some workloads. I would describe this as a query-efficiency and correctness update, not a promise that every search will be faster.
Keep on searching, but a lot faster this time!

