タグアーカイブ CJK

Relevanssiで日本語検索クエリが原因のテーブルフルスキャンを防ぐ

Relevanssiで日本語検索クエリが原因のテーブルフルスキャンを防ぐ

Relevanssiで日本語だけの検索クエリがデータベースの全テーブルスキャンを引き起こす問題は、トークナイズ結果が空文字になるケースをカスタムコードで事前に検知し、SQLを発行せずに空の結果を返すことで根本的に回避できる。設定のチューニングと併用すれば、サイト全体の検索パフォーマンスを維持できる。

なぜ日本語の検索クエリでRelevanssiがテーブル全体を読み込むのか

なぜ日本語の検索クエリでRelevanssiがテーブル全体を読み込むのか

Relevanssiは登録された投稿のタイトルや本文を分解し、単語単位でインデックスを作る全文検索プラグインだ。英語などスペースで区切られた言語では問題なく機能するが、日本語や中国語、韓国語といったCJK(中国語・日本語・韓国語)テキストでは事情が異なる。

CJKの文字列は単語境界の空白が存在しないため、Relevanssiが検索クエリを受け取ると、内部のトークナイザ(単語への分割処理)で適切な単位に区切れないことがある。特に、形態素解析エンジンがインストールされていない標準環境では、クエリが解析不能と見なされ、トークンがゼロ個、つまり空の状態になりやすい。

この「空のクエリ」が問題の本質だ。Relevanssiの検索SQLでは、本来なら検索語に基づいてWHERE term = '検索語'のように絞り込む。しかしトークンが空になると、検索語を表す変数に何も入らず、SQLがWHERE term = termという常に真になる条件へと崩れる。結果としてwp_relevanssiテーブルの何百万行という全レコードを走査するフルスキャンに陥り、応答に数十秒かかる事態を引き起こす。

これはバグではなく、空のクエリに対するフォールバック(予備動作)が設計上考慮されていないために発生する。本来なら検索語が存在しないと判断された時点で、データベースに問い合わせず「該当なし」を返すのが望ましい。

検索クエリが空になるのをコードで検出し早期リターンする

検索クエリが空になるのをコードで検出し早期リターンする

最も確実な解決策は、RelevanssiがSQLを構築する前に検索クエリの内容をチェックし、有効なトークンがなければ検索処理を打ち切る仕組みをテーマのfunctions.phpに組み込むことだ。Relevanssiは複数のフィルターフックを提供しており、そのうちrelevanssi_search_okまたはrelevanssi_modify_wp_queryを利用する。

具体的な実装コードと設置手順

STEP 1 子テーマまたはカスタムプラグインの準備
STEP 2 functions.php にフィルターフックを追加
STEP 3 検索クエリのトークン有無を判定する条件を記述
STEP 4 空の場合は検索をキャンセルし空結果を返す

このデモが示す流れで、コードを追加する。以下は実際に利用できる実装例だ。

add_filter( 'relevanssi_search_ok', function( $ok, $query ) {
    // 検索クエリが文字列であり、内容が空でないか確認する
    if ( ! is_string( $query->query_vars['s'] ) || '' === trim( $query->query_vars['s'] ) ) {
        return false; // 検索を実行せず早期リターン
    }

    // スペースを除いたテキストがCJK文字だけで構成されているか簡易チェック
    $search_term = trim( $query->query_vars['s'] );
    // CJK統合漢字・ひらがな・カタカナ・ハングルの正規表現
    $cjk_pattern = '/[\x{4e00}-\x{9faf}\x{3040}-\x{309f}\x{30a0}-\x{30ff}\x{ac00}-\x{d7af}]+/u';
    preg_match_all( $cjk_pattern, $search_term, $matches );

    // マッチしたCJK文字列が存在するかを確認
    if ( empty( $matches[0] ) ) {
        // CJK文字がなければ通常の検索を続行
        return $ok;
    }

    // 簡易的なトークン判定: Relevanssiが実際に使うトークナイザを再現
    // ここではCJKクエリが本当にインデックス可能か簡易判定する
    $tokens = relevanssi_tokenize( $search_term, true );
    if ( empty( $tokens[0] ) ) {
        return false; // 有効なトークンがないため検索中止
    }

    return $ok;
}, 10, 2 );

このコードでは、Relevanssiの内部関数relevanssi_tokenize()を呼び出し、実際に検索に使われるトークンが生成されるかどうかを見ている。もし空の配列が返ってきたら、それは全テーブルスキャンを引き起こす危険な状態だと判断し、falseを返すことで検索SQLの実行そのものをブロックする。

もうひとつ重要なのは、relevanssi_search_okフィルターがSQL構築の直前で動作するため、無駄なクエリがデータベースに発行される前に検索を止められる点だ。サイトの規模が大きく、wp_relevanssiテーブルが数百万行を超える場合でも、安全に空の結果を返せるようになる。

テーマの functions.php に追加する際の注意点

コードは必ず子テーマのfunctions.phpまたは専用のカスタムプラグインに記述する。親テーマのfunctions.phpを直接編集すると、テーマのアップデートで変更が失われる。また、PHPのバージョンが7.4以上であることをあらかじめ確認しておく。

コードを追加したら、日本語だけで構成された検索クエリを実際に投げてみる。検索結果がゼロ件で返ってくることを確認し、同時にMySQLのスロークエリログやQuery Monitorプラグインで、フルスキャンが発生していないかチェックすると確実だ。

データベースの負荷を根本的に下げるRelevanssiの設定

データベースの負荷を根本的に下げるRelevanssiの設定

コードによる早期リターンと並行して、インデックスと検索設定そのものを見直すことで、CJKクエリ以外の場面でもパフォーマンスを向上させられる。

最小文字数制限をCJKに合わせて調整する

Relevanssiの管理画面には「インデックスを作成する最小文字数」という設定がある。デフォルトでは2文字程度に設定されていることが多いが、CJK環境では1文字でも意味を持つ(例: 「本」「水」など)ため、1に下げるのが基本だ。ただし、1文字にするとインデックスサイズが膨張しやすいため、サイトの投稿規模に応じて2以上にするか、あるいは後述の文字種フィルタリングと組み合わせる。

検索から除外する投稿タイプやステータスを絞る

wp_relevanssiテーブルが肥大化する大きな要因は、リビジョンや自動下書き、非公開のカスタム投稿タイプまでインデックスに含めているケースだ。設定画面の「インデックスを作成する投稿タイプ」で、実際にサイトのフロントエンド検索で必要になる投稿タイプと公開済みのものだけに限定する。リビジョンが無駄に行を占有しているだけで、テーブルサイズが数割変わることもある。

MySQL / MariaDB のバッファ設定を最適化する

13万投稿、1300万行のインデックスを持つ規模では、サーバーのデータベース設定そのものがボトルネックになる。特にinnodb_buffer_pool_sizeをサーバーの物理メモリの70%程度に設定し、Relevanssiのテーブル全体がメモリに収まるようにすると、たとえスキャンが発生してもディスクI/Oを避けられる。設定変更は必ず本番環境でテストした後に適用する。

よくある質問

コードを追加した後、通常の英語検索に影響はないか

上記のコードは、トークンが空になる場合にのみ検索を中止する。英語のスペース区切りクエリや、英数字が混在する日本語クエリでは通常どおりRelevanssiのインデックスが使われるため、影響はない。万が一、正常な検索がブロックされていると感じたら、relevanssi_tokenize()の結果をエラーログに出力して確認する。

Relevanssi以外の全文検索プラグインでもこの問題は起こるのか

起こりうる。特にPHPベースのトークナイザに依存するプラグインでは、CJK文字の分割に失敗すると同様の空クエリ問題が発生する可能性がある。検索プラグインを選定する際は、形態素解析(MeCabなど)に対応しているか、もしくは外部の検索エンジン(Elasticsearch、Algoliaなど)と統合できるかを基準にするとよい。

大量のクローラーからCJKクエリを繰り返し受けている場合の対策は

検索クエリが空になるパターンは、ボットがランダムな文字列や日本語の記事タイトルを検索ボックスに投げ込むことで頻発する。コードによる早期リターンでサーバー負荷は防げるが、さらにCloudflareやWAFのレート制限で同一IPからの過剰な検索リクエストを制限すると、クローラー起因のリソース浪費全体を抑えられる。

functions.php を触れない環境で他にできることはあるか

管理画面のRelevanssi設定で「検索を許可する最低文字数」を意図的に上げる(例: 3文字)方法がある。ただし、これは短い日本語の単語が検索できなくなる副作用を伴う。根本解決にはならないが、緊急のパフォーマンス低下を抑える応急処置としては有効だ。

この記事のポイント

  • 日本語のみの検索クエリでRelevanssiが全テーブルスキャンに陥る根本原因は、トークナイズ結果が空になりSQLが常に真になるため
  • カスタムコードでSQL実行前に空トークンを検出し、早期リターンさせることでデータベース負荷を回避できる
  • インデックスの最小文字数制限や対象投稿タイプの絞り込みをCJK向けに最適化すると、さらなるパフォーマンス改善につながる
  • コード追加後も通常の英数字検索には影響を与えず、安全に運用できる
  • クローラーからの大量アクセスに対してはWAFやレート制限との併用が効果的