WordPress’s built-in search uses LIKE ‘%keyword%’ queries against post_title and post_content, which force a full table scan on every search request. On sites with thousands of posts this quickly becomes a significant performance problem. MySQL’s full-text search engine offers a purpose-built alternative: you add a FULLTEXT index to the columns you want to search, and then use the MATCH(columns) AGAINST(‘query’) syntax to query them. Full-text search is dramatically faster than LIKE because MySQL maintains an internal inverted index of all words in the indexed columns, similar to how a search engine works. It also supports relevance scoring — MATCH ... AGAINST returns a floating-point score for each row indicating how well it matches the query, so you can ORDER BY that score to show the most relevant results first. MySQL supports three full-text search modes: natural language mode (the default), boolean mode (supports +, -, * operators), and query expansion mode (broadens results using words from the top matches). The minimum word length that gets indexed is controlled by the ft_min_word_len system variable (default 4) for MyISAM and innodb_ft_min_token_size (default 3) for InnoDB. WordPress uses InnoDB for all its tables. Adding a FULLTEXT index to an existing WordPress table is safe and online — it does not lock the table for reads. You can hook into posts_search and posts_search_orderby to replace WordPress’s default search SQL with a full-text query. Review the MySQL indexes guide and the EXPLAIN guide before adding indexes to understand the performance tradeoffs.
Problem: WordPress search uses LIKE ‘%keyword%’ queries that scan the entire wp_posts table and return results in arbitrary order, making search slow and irrelevant on large sites.
Solution: Add a FULLTEXT index to post_title and post_content, then replace the default search SQL using WordPress filters:
-- 1. Add a FULLTEXT index to wp_posts (runs online, no table lock)
ALTER TABLE wp_posts
ADD FULLTEXT INDEX ft_search (post_title, post_content);
-- 2. Natural language search — returns relevance score
SELECT ID, post_title,
MATCH(post_title, post_content) AGAINST('WordPress caching') AS score
FROM wp_posts
WHERE post_status = 'publish'
AND post_type = 'post'
AND MATCH(post_title, post_content) AGAINST('WordPress caching')
ORDER BY score DESC
LIMIT 10;
-- 3. Boolean mode — find posts with 'cache' but NOT 'redis'
SELECT ID, post_title
FROM wp_posts
WHERE post_status = 'publish'
AND post_type = 'post'
AND MATCH(post_title, post_content)
AGAINST('+cache -redis' IN BOOLEAN MODE)
LIMIT 10;
-- 4. Wildcard search in boolean mode (match 'optim*' → optimize, optimization)
SELECT ID, post_title
FROM wp_posts
WHERE post_status = 'publish'
AND MATCH(post_title, post_content)
AGAINST('optim*' IN BOOLEAN MODE)
LIMIT 10;
// Hook into WordPress search to use FULLTEXT instead of LIKE
add_filter( 'posts_search', 'ha_fulltext_search', 10, 2 );
add_filter( 'posts_search_orderby', 'ha_fulltext_orderby', 10, 2 );
function ha_fulltext_search( $search, WP_Query $query ) {
if ( ! $query->is_search() || ! $query->is_main_query() ) {
return $search;
}
global $wpdb;
$term = $query->get( 's' );
$search = $wpdb->prepare(
" AND MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content)
AGAINST(%s IN BOOLEAN MODE)",
$term . '*'
);
return $search;
}
function ha_fulltext_orderby( $orderby, WP_Query $query ) {
if ( ! $query->is_search() || ! $query->is_main_query() ) {
return $orderby;
}
global $wpdb;
$term = $query->get( 's' );
return $wpdb->prepare(
"MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content)
AGAINST(%s IN BOOLEAN MODE) DESC",
$term . '*'
);
}
NOTE: MySQL full-text search ignores words shorter than innodb_ft_min_token_size (default 3 characters) and common stopwords defined in innodb_ft_server_stopword_table. If a search term is always returning zero results, check whether it is in the stopword list with SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD;. After adding or changing the FULLTEXT index, rebuild it with OPTIMIZE TABLE wp_posts; to ensure all rows are indexed. The safe $wpdb queries guide explains how to always use $wpdb->prepare() to prevent SQL injection in custom search queries.