2026年5月30日

2026年5月30日

MySQLのフルテキスト検索の設定と使い方|FULLTEXTインデックスとngramで日本語を検索する

MySQL のフルテキスト検索は、検索したい列に FULLTEXT インデックスを作り、MATCH(列) AGAINST('語句') で検索する仕組みです。日本語は単語の区切りに空白を使わないため、そのままでは期待どおりに検索できず、MySQL 標準の ngram パーサーを指定してインデックスを作るのが基本になります。この記事では、インデックスの作り方、検索モードの違い、日本語で検索できないときに確認する設定、運用上の注意点、WordPress の検索に使う場合の考え方までを説明します。

LIKE 検索との違い

WHERE post_content LIKE '%語句%' のような LIKE 検索は、先頭に % が付くと通常のインデックスが使えず、全行を一つずつ調べます。行数が少ないうちは問題ありませんが、記事が数万件を超えると検索が目に見えて遅くなることがあります。

フルテキスト検索は、文章をあらかじめ「語」に分けて索引を作っておくため、大きなテーブルでも検索が速く、どれだけ一致しているかを示す関連度(スコア)で並べ替えることもできます。その代わり、インデックスの作成や更新に時間とディスク容量を使い、部分一致の動きも LIKE とは異なります。

使える環境を確認する

InnoDB テーブルの FULLTEXT インデックスは MySQL 5.6 から、日本語などに使う ngram パーサーは MySQL 5.7 から標準で使えます。現在主流の MySQL 8 系では、どちらも追加の設定なしで使えます(2026年10月現在)。

注意したいのは MariaDB です。MariaDB にも FULLTEXT インデックスはありますが、MySQL の ngram パーサーは含まれていません。MariaDB で日本語のフルテキスト検索をする場合は、Mroonga などの別の仕組みを検討することになります。使っているのがどちらかは、次のコマンドで確認できます。

SELECT VERSION();
SHOW VARIABLES LIKE 'ngram_token_size';

ngram_token_size が表示されれば、ngram パーサーが使える MySQL です。

FULLTEXT インデックスを作る

WordPress の投稿テーブルで、タイトルと本文を対象にインデックスを作る例です。

ALTER TABLE wp_posts
  ADD FULLTEXT INDEX ft_title_content (post_title, post_content)
  WITH PARSER ngram;

WITH PARSER ngram を付けると、文章を指定した文字数ずつずらしながら区切って索引にします。標準の文字数(ngram_token_size)は 2 で、「データベース」は「デー」「ータ」「タベ」「ベー」「ース」のように分けられます。英語のように空白で区切られた文章だけを扱うなら、WITH PARSER ngram を付けない標準のパーサーで構いません。

本番で作る前の注意

  • テーブルに初めて FULLTEXT インデックスを作るときは、内部でテーブルの再構築が行われることがあり、記事数が多いと時間がかかります。ステージング環境で所要時間を測り、アクセスの少ない時間帯に行ってください。
  • 作業前にデータベースのバックアップを取ります。
  • インデックスの分だけディスク容量を使います。ngram は区切りの数が多いため、元のデータより大きな索引になることもあります。空き容量を確認してから作成します。
  • wp_posts は WordPress 本体のテーブルです。独自のインデックスを追加したことを記録に残し、本体の更新後にも残っているか確認できるようにしておきます。

検索の書き方と検索モード

検索には MATCH() と AGAINST() を使います。MATCH() に書く列の組み合わせは、インデックスを作ったときの列と同じにする必要があります。(post_title, post_content) で作ったインデックスを、MATCH(post_content) だけで使うことはできません。

自然言語モード

何も指定しない場合は自然言語モードになり、関連度の高い順に結果が返ります。

SELECT ID, post_title,
       MATCH(post_title, post_content) AGAINST('データベース') AS score
FROM wp_posts
WHERE MATCH(post_title, post_content) AGAINST('データベース' IN NATURAL LANGUAGE MODE)
  AND post_status = 'publish'
ORDER BY score DESC
LIMIT 20;

score が関連度で、数値が大きいほど検索語との一致が強いと判断されています。スコアの数値そのものに決まった意味はなく、同じ検索の中で順位を比べるための値です。

ブーリアンモード

演算子を使って、条件を細かく指定できるモードです。

SELECT ID, post_title
FROM wp_posts
WHERE MATCH(post_title, post_content)
      AGAINST('+データベース -バックアップ' IN BOOLEAN MODE);
  • +語句:その語を含むものだけ
  • -語句:その語を含むものを除く
  • "語句":その並びのまま含むもの(フレーズ検索)
  • 語句*:その語で始まるもの(前方一致)

ブーリアンモードは便利ですが、利用者が入力した文字列をそのまま渡すと、+ や -、" が演算子として解釈され、意図しない結果やエラーになることがあります。検索フォームの入力をそのまま使う場合は、自然言語モードにするか、演算子にあたる記号を取り除いてから渡します。

日本語で検索できないときの確認ポイント

「インデックスを作ったのに結果が0件になる」場合は、次の順に確認します。

  1. パーサーの指定:SHOW CREATE TABLE wp_posts; で、FULLTEXT インデックスに WITH PARSER ngram が付いているか確認します。付いていなければ、日本語の文章が一つの長い語として扱われ、ほとんど一致しません。インデックスを削除して作り直します。
  2. 検索語の長さ:ngram_token_size が 2 の場合、1文字だけの検索では結果が返らないことがあります。1文字検索が必要なら ngram_token_size を 1 にする方法がありますが、索引が大きくなり、関係の薄い結果も増えます。
  3. 設定の変更とインデックスの作り直し:ngram_token_size はサーバーの起動時に読み込む設定で、実行中には変えられません。my.cnf などで変更し、MySQL を再起動した後、FULLTEXT インデックスを作り直して初めて反映されます。
  4. ストップワード:InnoDB には「検索しても意味の薄い語」を除外する一覧があります。英語の短い語が中心ですが、ngram パーサーでは、ストップワードを含む区切りが索引から外れる仕組みのため、想定外の取りこぼしが起きることがあります。innodb_ft_enable_stopword の設定と、INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD の内容を確認してください。
  5. 英語の短い単語:ngram を使わない標準のパーサーでは、innodb_ft_min_token_size(標準は 3)より短い語は索引に入りません。「PHP」は3文字なので入りますが、「WP」のような2文字の語は検索できません。

実際にどのような語が索引に入っているかは、innodb_ft_aux_table に対象のテーブルを指定すると、INFORMATION_SCHEMA.INNODB_FT_INDEX_TABLE で確認できます。検証はステージングで行うと安心です。

SET GLOBAL innodb_ft_aux_table = 'wp_database/wp_posts';
SELECT WORD, DOC_COUNT FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_TABLE LIMIT 50;

wp_database の部分は実際のデータベース名に置き換えます。データがまだ索引の本体に書き込まれていない場合は、INNODB_FT_INDEX_CACHE の方に表示されます。

運用で気を付けること

InnoDB のフルテキスト索引では、記事を更新・削除しても、古い語の情報がすぐには消えずに残ります。更新の多いサイトでは、索引が少しずつ大きくなり、検索が遅くなることがあります。整理するには、innodb_optimize_fulltext_only を有効にしてから OPTIMIZE TABLE を実行します。

SET GLOBAL innodb_optimize_fulltext_only = ON;
OPTIMIZE TABLE wp_posts;
SET GLOBAL innodb_optimize_fulltext_only = OFF;

この設定を有効にしないまま OPTIMIZE TABLE を実行すると、テーブル全体の再構築になり時間がかかります。どちらの場合も、アクセスの少ない時間帯に行ってください。

また、post_content にはブロックエディターのコメント(<!-- wp:paragraph --> など)や HTML タグも含まれます。ngram はこれらも区切って索引にするため、wp や paragraph のような語で検索すると、本文に書いていない記事まで一致することがあります。

WordPress の検索に使う場合

WordPress の標準の検索は、タイトルや本文に対する LIKE 検索です。フルテキスト検索に置き換えるには、posts_search フィルターで検索条件の SQL を書き換える方法があります。考え方を示す最小限の例です。

add_filter( 'posts_search', function ( $search, $query ) {
	global $wpdb;

	if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
		return $search;
	}

	$term = $query->get( 's' );
	if ( '' === $term ) {
		return $search;
	}

	return $wpdb->prepare(
		" AND MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content) AGAINST (%s IN NATURAL LANGUAGE MODE) ",
		$term
	);
}, 10, 2 );

検索語は $wpdb->prepare() を通して SQL に埋め込み、直接連結しないようにします。この例では並び順は標準のままなので、関連度で並べたい場合は posts_search_orderby フィルターも合わせて調整します。子テーマの functions.php か mu-plugin に置き、ステージングで検索結果と速度を確かめてから本番に反映してください。

記事数がそれほど多くないサイトでは、標準の検索でも十分な速さが出ることが多く、無理に置き換える必要はありません。検索の精度や表記ゆれの吸収(全角と半角、カタカナとひらがななど)まで求める場合は、Elasticsearch などの専用の検索エンジンを使う構成も選択肢になります。

まとめ

  • フルテキスト検索は FULLTEXT インデックスと MATCH() AGAINST() の組み合わせで使う。MATCH() の列はインデックスと同じ組み合わせにする。
  • 日本語は WITH PARSER ngram を付けてインデックスを作る。MariaDB には ngram パーサーが無い点に注意する。
  • 検索結果が0件のときは、パーサーの指定、検索語の長さ、ngram_token_size の変更後の作り直し、ストップワードの順に確認する。
  • 利用者の入力をブーリアンモードにそのまま渡さない。WordPress で使うときは $wpdb->prepare() を通す。
  • インデックス作成はバックアップを取ってステージングで所要時間を測り、更新の多いサイトでは定期的に索引を整理する。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲