WP Manifestindependent plugin directory
manifest / admin / fulltext-search-for-wp

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

15stars
0forks

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.zip

Readme

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 | ...
  • content is stripped clean — do_blocks()do_shortcode()wp_strip_all_tags(). No block delimiters, no HTML, no shortcode brackets.
  • meta_text concatenates all non-private meta values into one searchable column. No JOINs at query time.
  • tax_text concatenates 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.

Read the full README on GitHub →