FullText Search for WP
Fixes WordPress admin search. Find posts by tag, custom field, or SKU — not just title and content.
by FullText Search for WP Contributors · github.com/roots-and-fruit/fulltext-search-for-wp · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/roots-and-fruit/fulltext-search-for-wp/archive/refs/heads/main.zipReadme
FullText Search for WP
Fixes WordPress admin search. Find posts by tag, custom field, or SKU — not just title and content.
The Problem
Every search box in wp-admin runs the same query:
WHERE post_title LIKE '%term%' OR post_content LIKE '%term%' OR post_excerpt LIKE '%term%'
Three columns. No index. No custom fields. No tags. No relevance ranking.
If you tagged a post "infrastructure" but that word isn't in the title or content, you'll never find it by searching. If you store a color in a custom field and search for "chartreuse" — 0 results. If a user's first name is "Jane" but their display name is "jsmith42" — searching "Jane" on the Users page returns nothing.
This plugin fixes all of that.
What It Does
| Admin Surface | What Changes |
|---|---|
| Posts / Pages list table | Search includes tags, categories, and all public custom fields. FULLTEXT relevance ranking with title boost. |
| Block editor link search | Same index, faster results. Title matches ranked first. |
| Users list table | Search includes first_name, last_name, nickname, bio, and (if WooCommerce is active) billing company, city, phone. |
| WooCommerce Products | Search includes SKU, GTIN, product categories, product tags, and attribute values (size, color, etc.). Uses WooCommerce's own woocommerce_product_pre_search_products filter. |
Why Not Just Use Elasticsearch?
Enterprise hosts (like WordPress VIP) correctly warn that adding FULLTEXT indexes directly to the core wp_posts table is a bad idea. It causes write-contention and table locks on high-traffic sites. They usually recommend offloading search to a SaaS like Elasticsearch.
This plugin gives you the speed and relevance of an inverted index using the infrastructure you already have, without the $50/month SaaS fee. We avoid the write-contention problem by creating a separate, dedicated index table (wp_fswp_index). The core WordPress tables are never modified.
How It Compares to Other Plugins
vs. "Full-Text Search" (by ishitaka): That plugin is excellent for frontend search and parsing text out of PDF/Word documents. However, it does not index custom fields, taxonomy terms, or user profiles. If you need to search for a WooCommerce SKU or a tag name, it won't find it.
vs. Relevanssi: Relevanssi is a powerhouse for frontend search, but its admin search is a separate Dashboard page. It does not replace the native search boxes on the Posts, Pages, or Users list tables, and it doesn't improve the block editor link search.
vs. WP Spotlight / Better WP-Admin Search: These add a global "command palette" search bar to the admin toolbar. They are useful for navigation, but they don't fix the underlying WP_Query engine that powers the actual list tables.
How It Works Under the Hood
The Index Table
The plugin creates a single table (wp_fswp_index) that denormalizes all searchable content:
object_id | object_type | subtype | title | content | excerpt | meta_text | tax_text | slug | post_status | ...
contentis stripped clean —do_blocks()→do_shortcode()→wp_strip_all_tags(). No block delimiters, no HTML, no shortcode brackets.meta_textconcatenates all non-private meta values into one searchable column. No JOINs at query time.tax_textconcatenates all taxonomy term names (categories, tags, WooCommerce attributes) into one column.
Two FULLTEXT indexes cover this table:
ft_title— title only (used for boost scoring)ft_all— title + content + excerpt + meta_text + tax_text
The Search Query
SELECT object_id,
(MATCH(title) AGAINST('term') * 3.0
+ MATCH(title, content, excerpt, meta_text, tax_text) AGAINST('term'))
AS relevance
FROM wp_fswp_index
WHERE MATCH(title, content, excerpt, meta_text, tax_text) AGAINST('term' IN NATURAL LANGUAGE MODE)
AND object_type = 'post'
AND post_status IN ('publish')
ORDER BY relevance DESC
LIMIT 20
Title matches get 3x the weight. Everything goes through $wpdb->prepare().
Environment Detection
| Environment | Search Mode |
|---|---|
| MySQL 5.6+ | FULLTEXT (MATCH...AGAINST with TF-IDF scoring) |
| MySQL 5.7.6+ with CJK locale | FULLTEXT with ngram parser |
| MariaDB 10.0.5+ | FULLTEXT |
| SQLite (WordPress Studio, Playground) | LIKE on the denormalized table (still better than core — includes meta + taxonomy, no JOINs) |
Hook Points
The plugin intercepts at these points:
| Hook | Surface |
|---|---|
posts_pre_query |
Admin list tables (Posts, Pages, CPTs) |
found_posts |
Pagination correction for list tables |
wp_rest_search_handlers |
Block editor link search (replaces WP_REST_Post_Search_Handler) |
users_pre_query |
Admin Users list table |
found_users_query |
Pagination correction for Users |
woocommerce_product_pre_search_products |
WooCommerce Products admin search |
save_post, before_delete_post, set_object_terms, meta hooks |
Incremental index updates |
user_register, profile_update, delete_user, user meta hooks |
Incremental user index updates |
Adding Your Plugin's Meta to the Index
Public meta (no underscore prefix)
Already indexed by default. If your plugin stores color, region, event_date as post meta — it's searchable out of the box.
Private meta (underscore prefix)
Excluded by default because most private meta is internal (_edit_lock, _wp_old_slug, etc.). To include specific private keys:
add_filter( 'fswp_meta_allowlist_private', function( array $keys ): array {
$keys[] = '_my_plugin_sku';
$keys[] = '_my_plugin_serial_number';
return $keys;
} );
Excluding public meta you don't want indexed
add_filter( 'fswp_meta_denylist', function( array $keys ): array {
$keys[] = 'cache_hash';
$keys[] = 'tracking_pixel_id';
return $keys;
} );
Adding user meta fields
The plugin indexes first_name, last_name, nickname, description by default, plus WooCommerce billing/shipping fields when Woo is active. To add your own:
add_filter( 'fswp_user_meta_keys', function( array $keys, int $user_id ): array {
$keys[] = 'my_crm_company';
$keys[] = 'my_crm_phone';
return $keys;
}, 10, 2 );
Excluding taxonomies
add_filter( 'fswp_taxonomy_denylist', function( array $slugs ): array {
$slugs[] = 'nav_menu';
$slugs[] = 'internal_taxonomy';
return $slugs;
} );
Adjusting relevance
// Title matches get 5x weight instead of the default 3x.
add_filter( 'fswp_title_boost', function(): float {
return 5.0;
} );
Adjusting cache TTL
// Cache search results for 10 minutes instead of the default 5.
add_filter( 'fswp_cache_ttl', function(): int {
return 600;
} );
WP-CLI Commands
wp fswp status # Index health, FULLTEXT support, row count
wp fswp index [--force] # Build/rebuild the index
wp fswp flush # Empty the index
wp fswp search "term" [--type=post] # Search and display results
wp fswp log [--lines=50] [--category=SEARCH] # Tail the debug log
wp fswp log clear # Clear the debug log
Debug Logging
When WP_DEBUG is enabled, the plugin writes structured log entries to wp-content/fswp-debug.log:
[2026-03-17T14:32:01Z] [SEARCH] Query executed | {"term":"chartreuse","mode":"fulltext","results":83,"total":83,"time_ms":2.4}
[2026-03-17T14:32:01Z] [INDEX] Indexed post | {"post_id":1234,"type":"post"}
[2026-03-17T14:31:55Z] [CRON] Indexed 200 posts (cursor at ID 1600) | {"batch_size":200,"count":200,"cursor":1600}
Categories: SCHEMA, INDEX, SEARCH, REST, CRON, ERROR
The log auto-rotates at 5MB. View it with wp fswp log or read the file directly.
All Filters
| Filter | Purpose | Default |
|---|---|---|
fswp_meta_denylist |
Public meta keys to exclude from indexing | [] |
fswp_meta_allowlist_private |
Private meta keys to include | ['_sku', '_global_unique_id', '_purchase_note'] (when Woo active) |
fswp_taxonomy_denylist |
Taxonomy slugs to exclude | [] |
fswp_user_meta_keys |
User meta keys to index | ['first_name', 'last_name', 'nickname', 'description'] + Woo billing fields |
fswp_batch_size |
Posts per cron batch | 200 |
fswp_max_search_length |
Maximum search term length | 200 |
fswp_cache_ttl |
Search cache TTL in seconds | 300 |
fswp_use_ngram_parser |
Force ngram FULLTEXT parser on/off | Auto-detected from locale |
Requirements
- WordPress 6.0+
- PHP 7.4+
- MySQL 5.6+ / MariaDB 10.0.5+ (for FULLTEXT). Works on SQLite with LIKE fallback.