2026年7月4日
2026年7月4日
WordPressのテーブルにFULLTEXTインデックスを追加して全文検索を高速化する方法
WordPress標準のサイト内検索は LIKE '%語%' でテーブル全体を読むため、記事数が増えると遅くなります。MySQLのFULLTEXTインデックスを wp_posts に追加し、posts_search フィルターで検索条件を MATCH ... AGAINST に置き換えると、インデックスを使った検索に切り替えられます。この記事では、日本語で使うための前提条件(ngramパーサー)、インデックスの追加手順と所要時間の注意、WordPress側の実装コード、効果の確認方法と戻し方までを説明します。
コアのテーブル構造に手を加える作業なので、作業前にデータベースのバックアップを取り、ステージング環境で試してから本番に適用してください。
まず前提条件を確認する
日本語にはngramパーサーが要る
FULLTEXTインデックスの標準のパーサーは、空白や記号で単語を区切ります。日本語は単語の間に空白がないため、標準パーサーでは文章全体がひとつの長い「単語」として扱われ、検索にほとんど使えません。日本語を扱うには、文字列を2文字ずつなどの断片に分けて索引化する ngramパーサー を使います。
ngramパーサーはMySQL 5.7.6以降のInnoDBで使えます。一方、MariaDBにはngramパーサーがありません。MariaDBで日本語の全文検索をしたい場合は、Mroongaストレージエンジンを使う方法がありますが、サーバー側でのプラグイン導入が必要で、レンタルサーバーでは使えないことが多いです。お使いの環境を次のコマンドで確認してください。
SELECT VERSION();
SHOW VARIABLES LIKE 'ngram_token_size';
SHOW TABLE STATUS WHERE Name = 'wp_posts';
VERSION() にMariaDBと表示される場合は、この記事の手順はそのままでは使えません。ngram_token_size が表示されればngramパーサーが使えます(既定値は2)。テーブルのEngineが InnoDB であることも確認します。テーブル名の wp_ は、wp-config.php の $table_prefix に合わせて読み替えてください。
プラグインで済むなら先に検討する
検索の質を上げたいだけなら、Relevanssi(独自の索引テーブルで関連度順の検索を行うプラグイン)や、Elasticsearchと連携するElasticPressなど、コアのテーブルを変えずに済む選択肢があります。機能の範囲や無料版・有料版の区分はバージョンによって変わるため、公式サイトで最新情報を確認してください(2026年10月現在)。この記事の方法は、追加のサービスやプラグインを増やさずにMySQLの機能だけで速くしたい場合に向いています。
手順1:FULLTEXTインデックスを追加する
検索対象のタイトルと本文の2列にインデックスを作ります。まずバックアップを取ります。
wp db export before-fulltext.sql
次にインデックスを追加します。WP-CLIの wp db query を使うか、phpMyAdminのSQL画面で実行します。
ALTER TABLE wp_posts
ADD FULLTEXT INDEX ft_title_content (post_title, post_content)
WITH PARSER ngram;
ここで注意したいのは所要時間とロックです。InnoDBでは、テーブルに初めてFULLTEXTインデックスを作るとき、内部的な列を追加するためにテーブルの再構築が行われ、作成中は書き込みが待たされることがあります。記事数が多いサイトでは数分以上かかる場合もあるので、アクセスと投稿作業が少ない時間帯に実行し、ステージングで所要時間を測っておくと安心です。
作成後、次のSQLでインデックスができていることを確認します。
SHOW INDEX FROM wp_posts WHERE Key_name = 'ft_title_content';
手順2:WordPressの検索をMATCH ... AGAINSTに置き換える
WordPressは検索キーワードがあると、posts_search フィルターを通して AND ((post_title LIKE ...) OR ...) という条件を組み立てます。この条件をFULLTEXTの検索に差し替えます。並び順も posts_search_orderby フィルターで関連度順にできます。コードは独自プラグインかmu-pluginに置きます。
<?php
/**
* Plugin Name: Fulltext Search
* Description: サイト内検索を FULLTEXT インデックス(ngram)で行う
*/
defined( 'ABSPATH' ) || exit;
/**
* 対象にするのはフロントのメイン検索クエリだけ。
*/
function my_ft_is_target( WP_Query $query ) {
return ! is_admin() && $query->is_main_query() && $query->is_search() && '' !== my_ft_terms( $query );
}
/**
* 検索語を BOOLEAN MODE 用の文字列に変換する。
* 演算子として解釈される記号を除き、2文字以上の語に + を付けて「すべての語を含む」検索にする。
* 条件を満たす語がなければ空文字を返し、標準の LIKE 検索に任せる。
*/
function my_ft_terms( WP_Query $query ) {
$s = preg_replace( '/[+\-<>()~*"@]+/u', ' ', (string) $query->get( 's' ) );
$words = preg_split( '/\s+/u', trim( $s ), -1, PREG_SPLIT_NO_EMPTY );
$words = array_filter( $words, function ( $w ) {
return mb_strlen( $w ) >= 2;
} );
return $words ? '+' . implode( ' +', $words ) : '';
}
add_filter( 'posts_search', function ( $search, WP_Query $query ) {
if ( ! my_ft_is_target( $query ) ) {
return $search;
}
global $wpdb;
return $wpdb->prepare(
" AND MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content) AGAINST (%s IN BOOLEAN MODE) ",
my_ft_terms( $query )
);
}, 10, 2 );
add_filter( 'posts_search_orderby', function ( $orderby, WP_Query $query ) {
if ( ! my_ft_is_target( $query ) ) {
return $orderby;
}
global $wpdb;
return $wpdb->prepare(
"MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content) AGAINST (%s IN BOOLEAN MODE) DESC",
my_ft_terms( $query )
);
}, 10, 2 );
コードのポイント
**検索語は $wpdb->prepare() で渡す**: 検索フォームの入力はそのままSQLに埋め込むとSQLインジェクションの原因になります。%s のプレースホルダーで渡し、エスケープは prepare() に任せます。
**BOOLEAN MODEの記号を取り除き、語ごとに + を付ける**: BOOLEAN MODEでは + - * " などが演算子として扱われます。利用者が意図せず入力すると検索結果が0件になったりするため、記号はいったん空白に置き換えています。そのうえで各語の先頭に + を付けています。BOOLEAN MODEで記号なしの複数語を渡すと「いずれかの語を含む記事」がヒットしますが、WordPress標準の検索は「すべての語を含む記事」を返すため、挙動をそろえるためです。なお、ngramパーサーを使うと、BOOLEAN MODEの各語は断片の並びとして(フレーズに近い形で)照合されます。
MATCHの列はインデックスと一致させる: MATCH() に書く列の組み合わせは、作成したFULLTEXTインデックスの列と同じにする必要があります。InnoDBでは一致するインデックスがないとエラーになります。抜粋(post_excerpt)も検索したい場合は、インデックスを3列で作り直し、コードの MATCH() にも追加します。
対象を絞る: 管理画面の投稿一覧の検索や、他のプラグインが発行する検索クエリまで変えると、予期しない不具合につながります。is_main_query() と is_search() でフロントのサイト内検索だけを対象にしています。公開状態や投稿タイプの条件はWordPressが別に組み立てるので、この差し替えで下書きが見えるようになることはありません。
手順3:効果を確認する
実際に使われるSQLを取り出し、EXPLAIN で実行計画を確認します。Query Monitorプラグインを使うと、検索結果ページで発行されたSQLを管理バーから確認できて便利です。
EXPLAIN SELECT ID FROM wp_posts
WHERE MATCH(post_title, post_content) AGAINST ('+ワードプレス' IN BOOLEAN MODE)
AND post_type = 'post' AND post_status = 'publish';
結果の type 列が fulltext、key 列が ft_title_content になっていれば、インデックスが使われています。あわせて、変更前と変更後で同じキーワードの検索時間を比べ、検索結果の件数や上位の記事に違和感がないかも確認します。
知っておきたい挙動の違い
FULLTEXT検索は LIKE 検索と同じ結果になるとは限りません。主な違いは次のとおりです。
- 短い語:
ngram_token_sizeが2の場合、1文字だけの語はFULLTEXTインデックスでは探せません。上のコードでは1文字の語を除外し、2文字以上の語がひとつもなければ標準のLIKE検索に任せるようにしています。 - ストップワード: InnoDBには既定のストップワード一覧(主に英語の短い語)があり、それを含む断片は索引化されないことがあります。英単語の検索結果が想定より少ない場合は確認します。
- HTMLやブロックのコメント:
post_contentにはHTMLタグやブロックエディターのコメントも含まれるため、タグ名や属性の文字列にもヒットすることがあります。気になる場合は、本文からタグを除いたテキストを別テーブルに持ち、そちらにインデックスを張る設計も選べます。 - インデックスの容量と更新負荷: ngramは断片の数が多いため、インデックスのサイズが大きくなり、投稿の保存時にも更新処理が増えます。サーバーのディスク容量に余裕があるか確認しておきます。
元に戻す方法
不具合があればプラグインを無効化すると、検索は標準の LIKE に戻ります。インデックス自体を削除する場合は、バックアップを取ってから次のSQLを実行します。
ALTER TABLE wp_posts DROP INDEX ft_title_content;
サーバー移転やバックアップの復元を行うツールによっては、追加したインデックスが移行先で再現されないことがあります。移転後は手順1の確認SQLでインデックスの有無を見直してください。
まとめ
- 日本語の全文検索にはngramパーサーが必要で、MySQL 5.7.6以降のInnoDBが前提。MariaDBではこの方法は使えない
- インデックスの初回作成はテーブル再構築を伴うことがあるため、バックアップを取り、空いている時間帯に実行する
posts_searchとposts_search_orderbyでフロントのメイン検索だけをMATCH ... AGAINSTに置き換え、検索語は$wpdb->prepare()で渡すEXPLAINでtypeがfulltextになることを確認し、検索結果の違いも見ておく- コアのテーブルを変えたくなければ、RelevanssiやElasticPressも検討する