テクニック

WordPress検索結果のカスタマイズ|除外設定と並び順の実装

この記事の対象: WordPressサイトの検索フォームと検索結果ページを改善したい制作者・実装担当者

サイト内検索は、公開直後はほとんど使われません。ところが数ヶ月経つと、検索ログの中に「採用」「料金」「〇〇 事例」といった、そのサイトで一番売りたいものの名前が並びはじめます。そこで「該当する記事はありませんでした」が返っていたら、探しに来た人を自分の手で追い返していることになります。

WordPressの標準検索は、投稿・固定ページ・公開中のカスタム投稿タイプを、日付の新しい順にまとめて返します。制作の現場ではこれがそのまま使えることはほぼありません。この記事では、pre_get_posts で検索対象を絞り込み・除外する方法、タイトル一致を上位に持ち上げる並び順の実装、そして0件のときに何も返さない状態をなくすところまで、コピーして動く形で載せます。


前提と作業環境

この記事のコードが動く条件

項目 条件
WordPress 5.0以降。6.x系で動作を想定
テーマ クラシックテーマ・ブロックテーマの両方(PHPは functions.php に置く)
権限 テーマファイルを編集できる(FTP/SSH、または管理画面のテーマファイルエディター)
PHP 7.4以降。8.x でもそのまま動きます
プラグイン 不要。素のWordPressだけで完結します

検索結果の絞り込みは、プラグインを入れれば数分で終わる領域でもあります。ただし検索は全ページから叩かれる導線なので、そのためだけに1本プラグインを増やすと、更新のたびに壊れていないかを確認する対象が増えます。20〜30行のPHPで済むなら、書いたほうが後の保守が軽くなります。

WordPress全体の保守方針を整理したい場合は、こちらもあわせて読んでください。

コードをどこに置くか

この記事のPHPコードは、すべて次のどちらかに置きます。

  • 子テーマの functions.php — 有料テーマ・配布テーマを使っている場合はこちら。パスは wp-content/themes/親テーマ名-child/functions.php
  • オリジナルテーマの functions.php — 自作テーマならテーマ直下。パスは wp-content/themes/テーマ名/functions.php

有料テーマの functions.php を直接編集すると、テーマ更新で消えます。子テーマがまだない場合は先に作ってください。

もうひとつの選択肢がミニプラグイン化です。 テーマを乗り換えても検索の挙動を残したいなら、wp-content/plugins/site-search-tweaks/site-search-tweaks.php を作って、下記のヘッダーコメントを先頭に置き、あとは同じコードを書きます。

<?php
/**
 * Plugin Name: Site Search Tweaks
 * Description: サイト内検索の対象・除外・並び順のカスタマイズ
 * Version: 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

管理画面の「プラグイン」に出てくるので、有効化すれば動きます。以降のコードはこの下に追記してください。

触る前に確認しておくこと

pre_get_posts はサイト内のあらゆるクエリを通過するフックです。検索まわりを触ると、意図しない場所の一覧が同時に壊れることがあります。作業前に、次の3ヶ所を開いてスクリーンショットを撮っておくと、比較が一瞬で終わります。

  • トップページ(新着一覧を出している場合)
  • アーカイブページ(カテゴリー・タグ・カスタム投稿タイプ一覧)
  • 管理画面の投稿一覧・固定ページ一覧

検索対象の絞り込みと除外(pre_get_posts)

検索対象の絞り込みと除外(pre_get_posts)

標準の検索が返しているもの

WordPressの検索は、exclude_from_searchfalse になっている投稿タイプをすべて対象にします。標準の post(投稿)と page(固定ページ)は最初から検索対象で、カスタム投稿タイプは登録時の設定次第です。

register_post_type()'public' => true を指定すると、exclude_from_search は自動的に false(=検索対象になる)になります。つまり、公開設定にしたカスタム投稿タイプは、何もしなくても検索結果に混ざります。

標準検索が拾う範囲と、絞り込み後の範囲

絞り込み後(推奨)

  • 投稿(お知らせ・ブログ)
  • 実績・事例
  • よくある質問

標準のまま

  • 投稿
  • 固定ページ(サンクスページ含む)
  • 公開設定の全カスタム投稿タイプ
  • 添付ファイル(設定次第)

標準のままだと、「お問い合わせ完了ページ」「404の説明ページ」「テスト用の固定ページ」まで検索結果に並びます。読者が探しているのはコンテンツであって、システム上必要なだけのページではありません。

投稿タイプを指定して絞り込む

まずは一番よく使う形です。検索結果を「投稿」と「実績(works)」だけにします。

/**
 * サイト内検索の対象投稿タイプを絞り込む
 */
function soreiine_limit_search_post_types( $query ) {
    // 管理画面・メインクエリ以外・検索以外は触らない
    if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
        return;
    }

    $query->set( 'post_type', array( 'post', 'works' ) );
}
add_action( 'pre_get_posts', 'soreiine_limit_search_post_types' );

変更する箇所は1つだけです。 array( 'post', 'works' ) の中身を、そのサイトの投稿タイプスラッグに書き換えてください。works は例なので、実際のスラッグに合わせます。

投稿タイプのスラッグがわからないときは、管理画面でその投稿タイプの一覧を開き、URLの post_type= の後ろを見ます。edit.php?post_type=works なら works です。

冒頭の3条件は毎回書きます。理由はそれぞれ違います。

条件 外すと何が起きるか
is_admin() 管理画面の投稿一覧・検索まで同じ絞り込みを受け、編集者から見えない投稿が出る
is_main_query() サイドバーの「関連記事」など、テーマ内の WP_Query にも適用されて一覧が壊れる
is_search() カテゴリー一覧やトップの新着など、検索以外のページにも適用される

is_admin() は「管理画面かどうか」であって「管理者かどうか」ではありません。読み間違えやすいところです。

固定ページだけを除外する

投稿タイプを列挙する方式は確実ですが、カスタム投稿タイプを後から追加したときに、ここへ追記するのを忘れると検索から漏れます。「増えたものは基本的に検索させたい。固定ページだけ要らない」という方針なら、除外側で書いたほうが運用が楽です。

/**
 * サイト内検索から固定ページを除外する(他は自動的に対象に含める)
 */
function soreiine_exclude_pages_from_search( $query ) {
    if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
        return;
    }

    // 検索対象になっている投稿タイプを取得
    $types = get_post_types( array( 'exclude_from_search' => false ) );

    // 除外したい投稿タイプをここに列挙する
    $exclude = array( 'page', 'attachment' );

    $query->set( 'post_type', array_diff( array_keys( $types ), $exclude ) );
}
add_action( 'pre_get_posts', 'soreiine_exclude_pages_from_search' );

変更する箇所は $exclude の中身です。 上の例では固定ページ(page)と添付ファイル(attachment)を外しています。除外したいカスタム投稿タイプがあれば、そのスラッグを足してください。

array_diff() は「検索対象の全投稿タイプ」から「除外リスト」を引いた配列を返します。後からカスタム投稿タイプを追加しても、自動的に検索対象に入ります。

投稿タイプの登録時点で除外する

自分で登録しているカスタム投稿タイプなら、register_post_type() の時点で外すのが一番素直です。フックを1つ減らせます。

register_post_type( 'internal_doc', array(
    'label'               => '社内資料',
    'public'              => true,
    'exclude_from_search' => true,  // 検索結果に出さない
    'has_archive'         => false,
) );

exclude_from_searchpublic の指定から自動で決まる値ですが、明示的に書けばそちらが優先されます(配列内の順序は関係ありません)。この設定が効くのはサイト内検索の対象範囲だけで、URLを直接叩けばページ自体は表示されます。見せたくないページを隠す用途には使えません。

プラグインが登録したカスタム投稿タイプを外したいときは、register_post_type_args で登録時の引数に割り込みます。プラグイン本体を編集せずに済みます。

/**
 * プラグイン由来のカスタム投稿タイプを検索対象から除外する
 */
function soreiine_exclude_cpt_from_search( $args, $post_type ) {
    if ( 'plugin_cpt_slug' === $post_type ) {
        $args['exclude_from_search'] = true;
    }

    return $args;
}
add_filter( 'register_post_type_args', 'soreiine_exclude_cpt_from_search', 10, 2 );

変更する箇所は 'plugin_cpt_slug' です。 複数あるなら in_array( $post_type, array( ... ), true ) に変えてください。

一部の固定ページだけ検索に残したい

「会社概要」「料金」など、固定ページで作った主要ページは検索に残したいことがよくあります。この場合は、投稿タイプ単位ではなくID単位で制御します。

/**
 * 固定ページのうち、指定したIDだけを検索対象に残す
 */
function soreiine_search_allow_specific_pages( $query ) {
    if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
        return;
    }

    // 検索に残したい固定ページのID
    $allow_page_ids = array( 12, 34, 56 );

    // 除外対象=全固定ページ − 残したいページ
    $all_page_ids = get_posts( array(
        'post_type'      => 'page',
        'post_status'    => 'publish',
        'posts_per_page' => -1,
        'fields'         => 'ids',
    ) );

    $query->set( 'post__not_in', array_diff( $all_page_ids, $allow_page_ids ) );
}
add_action( 'pre_get_posts', 'soreiine_search_allow_specific_pages' );

変更する箇所は $allow_page_ids です。 管理画面で固定ページの編集画面を開き、URLの post=123 の数字がIDです。

この方式を使うときは、前述の「固定ページだけを除外する」コードと併用しないでください。page を投稿タイプごと外していると、ID単位で残す指定は届きません。post_type には page を含めたまま、post__not_in で個別に落とす形になります。

固定ページが数百枚あるサイトでは、毎回の検索で全ID取得が走るため負荷になります。その場合は逆に、除外したい固定ページ側にカスタムフィールド(例: exclude_from_search1)を持たせて、meta_query で外すほうが軽くなります。ページ数が数十枚程度なら、上のコードで問題ありません。

特定カテゴリーの投稿を除外する

「お知らせのうち、社内向けカテゴリーだけ検索に出したくない」といった要件は、投稿タイプではなくタクソノミー単位で外します。

/**
 * 指定カテゴリーの投稿を検索結果から除外する
 */
function soreiine_exclude_categories_from_search( $query ) {
    if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
        return;
    }

    // 除外したいカテゴリーのID
    $query->set( 'category__not_in', array( 7, 12 ) );
}
add_action( 'pre_get_posts', 'soreiine_exclude_categories_from_search' );

変更する箇所はIDの配列です。 タグなら tag__not_in、カスタムタクソノミーなら tax_query'operator' => 'NOT IN' を指定します。カテゴリーIDは管理画面のカテゴリー編集画面のURL、tag_ID= の数字で確認できます。

検索結果ページ自体を検索エンジンから除外する

サイト内検索の結果ページがGoogleにインデックスされると、中身の薄いページが大量に登録されます。WordPressは検索結果ページに noindex を自動で出力しますが、SEO系プラグインがrobotsメタを自前で管理している場合は、そちらの設定が優先されることがあります。実際の検索結果ページのHTMLを表示して、noindex が入っているかを一度目で確認してください。


並び順をタイトル優先にする

並び順をタイトル優先にする

日付順が使いにくい理由

WordPressの検索結果は、標準では日付の降順です。「料金」で検索したとき、「料金改定のお知らせ(3年前)」より「昨日書いた雑記に料金という単語が1回出てくる記事」が上に来ます。これは求めている挙動ではありません。

探している人は、たいていタイトルにその言葉が入っているページを探しています。タイトル一致を最優先、次に本文一致、その中で新しい順――これが検索結果として自然な並びです。

タイトル優先の並び替えが動く順番
検索語を受け取るクエリから入力語を取得
SQLに列を足すタイトル一致なら0、それ以外は1のスコア列
並び替えるスコア昇順 → 日付降順

WordPressの検索は最終的にSQLに変換されるため、並び替えもSQLの ORDER BY に手を入れるのが確実です。取得後にPHPで並べ替えると、ページ送りの区切りが取得順のままになるため2ページ目以降が崩れます。

実装コード

posts_search_orderby フィルターを使います。検索語があり、かつクエリ側で orderby を明示していないときに効きます。

/**
 * 検索結果をタイトル一致優先で並べる
 * 1. タイトルに検索語を含むもの
 * 2. それ以外(本文・抜粋の一致)
 * 各グループ内は投稿日の新しい順
 */
function soreiine_search_orderby_title_first( $orderby, $query ) {
    global $wpdb;

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

    $search_term = $query->get( 's' );
    if ( '' === $search_term ) {
        return $orderby;
    }

    $like = '%' . $wpdb->esc_like( $search_term ) . '%';

    return $wpdb->prepare(
        "CASE WHEN {$wpdb->posts}.post_title LIKE %s THEN 0 ELSE 1 END ASC, {$wpdb->posts}.post_date DESC",
        $like
    );
}
add_filter( 'posts_search_orderby', 'soreiine_search_orderby_title_first', 10, 2 );

このコードはそのまま使えます。 変更する箇所はありません。

読んでおいてほしいのは $wpdb->prepare()esc_like() の2つです。検索語はユーザーが自由に入力する値なので、SQLに直接埋めるとSQLインジェクションの入口になります。prepare() がエスケープと引用符付けをまとめてやってくれます。esc_like() は検索語に %_ が含まれていたとき、それをワイルドカードではなくただの文字として扱わせるためのものです。「50%OFF」で検索したときに全件が一致しないのは、この一行のおかげです。

完全一致をさらに上に持ち上げる

3段階にしたい場合はこうします。タイトル完全一致 → タイトル部分一致 → その他、の順です。

/**
 * 検索結果を3段階で並べる
 * 0: タイトルが検索語と完全一致
 * 1: タイトルに検索語を含む
 * 2: それ以外
 */
function soreiine_search_orderby_three_steps( $orderby, $query ) {
    global $wpdb;

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

    $search_term = $query->get( 's' );
    if ( '' === $search_term ) {
        return $orderby;
    }

    $exact   = $search_term;
    $partial = '%' . $wpdb->esc_like( $search_term ) . '%';

    return $wpdb->prepare(
        "CASE
            WHEN {$wpdb->posts}.post_title = %s THEN 0
            WHEN {$wpdb->posts}.post_title LIKE %s THEN 1
            ELSE 2
        END ASC, {$wpdb->posts}.post_date DESC",
        $exact,
        $partial
    );
}
add_filter( 'posts_search_orderby', 'soreiine_search_orderby_three_steps', 10, 2 );

前のコードとどちらか一方だけを使ってください。両方を有効にすると、後から登録したほうが ORDER BY を上書きします。

投稿タイプごとに順番を固定する

「実績を先に、次にブログ」のように、投稿タイプ単位で表示順を決めたい場合もあります。FIELD() を使うとSQLひとつで書けます。

/**
 * 投稿タイプの指定順 → タイトル一致 → 新着順 で並べる
 */
function soreiine_search_orderby_by_post_type( $orderby, $query ) {
    global $wpdb;

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

    $search_term = $query->get( 's' );
    if ( '' === $search_term ) {
        return $orderby;
    }

    $like = '%' . $wpdb->esc_like( $search_term ) . '%';

    // 表示したい順に投稿タイプを並べる
    return $wpdb->prepare(
        "FIELD({$wpdb->posts}.post_type, %s, %s, %s) ASC,
         CASE WHEN {$wpdb->posts}.post_title LIKE %s THEN 0 ELSE 1 END ASC,
         {$wpdb->posts}.post_date DESC",
        'works',
        'post',
        'page',
        $like
    );
}
add_filter( 'posts_search_orderby', 'soreiine_search_orderby_by_post_type', 10, 2 );

変更する箇所は 'works', 'post', 'page' の3つです。 出したい順に投稿タイプのスラッグを並べます。数を増やすなら、FIELD() 内の %s と、prepare() の引数を同じ数だけ足してください。

FIELD() はMySQL・MariaDBの関数で、指定した並びの何番目かを返します。リストにない値は0を返して先頭に来てしまうので、検索対象の投稿タイプはすべて列挙してください。前半で post_type を絞り込んでいれば、その配列と同じ顔ぶれを書けば足ります。


0件を出さない設計にする

検索されているのに何も出ない状態

ここが一番効きます。絞り込みと並び順を整えても、0件のときに「該当する記事はありませんでした」だけが表示されるなら、そこで離脱します。

検索結果テンプレート(search.php、ブロックテーマなら templates/search.html)で、0件時に必ず何かを出す形にします。クラシックテーマの search.php を編集する場合はこうです。配置場所は wp-content/themes/テーマ名/search.php です。

<?php get_header(); ?>

<main class="site-main">

    <h1 class="search-title">
        「<?php echo esc_html( get_search_query() ); ?>」の検索結果
    </h1>

    <?php if ( have_posts() ) : ?>

        <p class="search-count">
            <?php
            printf(
                '%s件見つかりました',
                esc_html( number_format_i18n( $wp_query->found_posts ) )
            );
            ?>
        </p>

        <ul class="search-list">
            <?php while ( have_posts() ) : the_post(); ?>
                <li class="search-item">
                    <a href="<?php the_permalink(); ?>">
                        <span class="search-item__type">
                            <?php
                            $type_obj = get_post_type_object( get_post_type() );
                            if ( $type_obj ) {
                                echo esc_html( $type_obj->labels->singular_name );
                            }
                            ?>
                        </span>
                        <span class="search-item__title"><?php the_title(); ?></span>
                    </a>
                </li>
            <?php endwhile; ?>
        </ul>

        <?php the_posts_pagination(); ?>

    <?php else : ?>

        <div class="search-empty">
            <p class="search-empty__lead">
                「<?php echo esc_html( get_search_query() ); ?>」に一致するページは見つかりませんでした。
            </p>

            <p class="search-empty__hint">
                言葉を短くする、ひらがな・カタカナを変えると見つかることがあります。
            </p>

            <?php get_search_form(); ?>

            <h2 class="search-empty__heading">よく読まれているページ</h2>
            <ul class="search-fallback">
                <?php
                // ここを、そのサイトで見せたいページのIDに書き換える
                $fallback_ids = array( 12, 34, 56 );

                $fallback = new WP_Query( array(
                    'post__in'       => $fallback_ids,
                    'orderby'        => 'post__in',
                    'posts_per_page' => count( $fallback_ids ),
                    'post_type'      => 'any',
                ) );

                while ( $fallback->have_posts() ) :
                    $fallback->the_post();
                    ?>
                    <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
                    <?php
                endwhile;
                wp_reset_postdata();
                ?>
            </ul>
        </div>

    <?php endif; ?>

</main>

<?php get_footer(); ?>

変更する箇所は3つです。

  1. $fallback_ids の配列 — そのサイトで見せたいページ・記事のIDに書き換える
  2. class 名(site-main search-list など)— 既存テーマのCSS設計に合わせる
  3. get_header() / get_footer() — テーマによってはラッパーdivの構造が違うので、同テーマの index.php の書き方に合わせる

'orderby' => 'post__in' は、post__in に書いた並び順のまま出す指定です。これを書かないと日付順になり、せっかく順番を決めた意味がなくなります。

空文字の検索を弾く

検索フォームで何も入力せずに送信すると、WordPressは検索ではなくトップページを返します。これは検索したつもりの人には壊れて見えます。空のときは検索結果ページに送って、上のフォールバック表示を出しましょう。

functions.php に追記します。

/**
 * 空の検索でトップページに飛ばず、検索結果ページを表示する
 */
function soreiine_allow_empty_search( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }

    if ( isset( $_GET['s'] ) && '' === trim( wp_unslash( $_GET['s'] ) ) ) {
        $query->is_search = true;
        $query->is_home   = false;
        $query->set( 's', '' );
    }
}
add_action( 'pre_get_posts', 'soreiine_allow_empty_search' );

このコードはそのまま使えます。空検索は結果0件になるので、search.phpelse 側が表示されます。

検索フォーム自体を素で書く

検索フォームはプラグインもJavaScriptも要りません。wp-content/themes/テーマ名/searchform.php を作ると、get_search_form() が自動でこのファイルを使います。

<form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
    <label class="search-form__label" for="search-field">
        サイト内を検索
    </label>
    <input
        type="search"
        id="search-field"
        class="search-form__input"
        name="s"
        value="<?php echo esc_attr( get_search_query() ); ?>"
        placeholder="キーワードを入力"
        autocomplete="off"
    >
    <button type="submit" class="search-form__submit">検索</button>
</form>

変更する箇所はクラス名とボタンのラベルだけです。 name="s" は変えないでください。WordPressが検索語として読む名前がこれです。

<label> は装飾上いらなくても消さないでください。消すとスクリーンリーダーが「編集可能なテキスト」としか読み上げず、何を入力する欄なのかがわからなくなります。見た目に出したくないなら、CSSで視覚的にだけ隠します。

/* ラベルを画面から隠しつつ、読み上げには残す */
.search-form__label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

clip-path: inset(50%) は主要ブラウザで長く使えます。古いブラウザまで拾いたい場合は clip: rect(0 0 0 0); を併記してください。

特定の投稿タイプだけを検索するフォーム

hidden を1つ足すだけで、投稿タイプを固定した検索フォームになります。実績ページの中に置く「実績だけを検索」のような用途です。

<form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
    <label class="search-form__label" for="search-works">実績を検索</label>
    <input type="search" id="search-works" class="search-form__input" name="s" value="<?php echo esc_attr( get_search_query() ); ?>">
    <input type="hidden" name="post_type" value="works">
    <button type="submit" class="search-form__submit">検索</button>
</form>

value="works" を対象の投稿タイプスラッグに変えてください。

ここで注意があります。前半で $query->set( 'post_type', ... ) を書いていると、URLの post_type パラメータは上書きされて効きません。両立させるには、絞り込み側でこう書き換えます。

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

    // URLで post_type が指定されていれば、それを尊重する
    if ( ! empty( $_GET['post_type'] ) ) {
        return;
    }

    $query->set( 'post_type', array( 'post', 'works' ) );
}
add_action( 'pre_get_posts', 'soreiine_limit_search_post_types' );

URLパラメータが来ているときは何もせず、WordPressの標準処理に任せます。存在しない投稿タイプ名を渡された場合は、exclude_from_searchtrue の投稿タイプと同様に結果0件になるだけで、非公開ページが露出することはありません。


掲載サイトに学ぶ、検索結果ページの作り

掲載サイトに学ぶ、検索結果ページの作り

Soreiine!!のギャラリーで検索まわりまで作り込まれているサイトを開いて、実際に検索を叩いてみると、共通して同じ4つのことをやっています。どれもこの記事のコードで再現できる範囲です。

【画像挿入: ギャラリー掲載サイトの検索結果ページ、各行の左に投稿タイプのラベルが付いた一覧】

1. 各行の頭に投稿タイプのラベルが出ている。 「実績」「お知らせ」「よくある質問」が行の左に小さく入っているだけで、10件並んだときに目的の種類だけを拾い読みできます。上の search.phplabels->singular_name を出しているのはこれです。ラベルの文言は投稿タイプ登録時の labels がそのまま出るので、works の単数ラベルが「Works」のままになっていないか、管理画面の表示で確認してください。読者向けの日本語になっているかがそのまま画面に出ます。

2. 件数と検索語を最初の1行で返している。 「『料金』の検索結果 8件」のように、何で検索したかと何件あるかがh1直下にあります。入力した語が画面に残っていると、打ち間違いにその場で気づけます。get_search_query()found_posts の2つで済みます。

3. 0件のページに必ず次の行き先がある。 よく読まれているページの一覧、カテゴリー一覧、問い合わせ導線のいずれかが置かれ、検索フォームがもう一度出ています。「見つかりませんでした」の一行で終わっているサイトは、そこが行き止まりになります。

【画像挿入: 検索窓がヘッダー右側の同じ位置に固定配置されているサイトの例、下層ページを複数並べた比較】

4. 検索窓の位置が全ページで同じ。 ヘッダー右側なら全ページのヘッダー右側にあります。トップにだけ大きな検索窓があって下層に無いサイトは、下層から入ってきた人が「このサイトには検索がない」と判断して離脱します。位置を変えないことは実装コストがゼロで、効果だけが残る部分です。

この4つは、ページ数や機能一覧を並べた見積書の項目には出てきません。同じ「サイト内検索あり」の1行でも、この作り込みが入っているかどうかで実装量は変わります。見積の金額差がどこから出ているのかを読むときの視点は、こちらでまとめています。


うまくいかないとき

検索結果が0件のまま変わらない

post_type に指定したスラッグが間違っている可能性が高いです。管理画面でその投稿タイプの一覧を開き、URLの post_type= の値と一字一句合っているか確認してください。大文字小文字も区別されます。

投稿タイプ名は最大20文字という制限があるため、長い名前で登録しようとして途中で切れているケースもあります。

一時的にデバッグ出力を入れて確認するなら、search.php の先頭に次を置きます。確認後は必ず消してください。

<?php
// デバッグ用。確認したら削除する
if ( current_user_can( 'manage_options' ) ) {
    global $wp_query;
    echo '<pre>';
    echo esc_html( print_r( $wp_query->query_vars['post_type'], true ) );
    echo '</pre>';
}
?>

current_user_can( 'manage_options' ) で囲んでいるので、管理者以外には出ません。それでも本番に置きっぱなしにはしないでください。

管理画面の投稿一覧がおかしくなった

is_admin() のチェックが抜けています。pre_get_posts は管理画面のクエリも通過するので、この1行がないと編集画面の一覧にも同じ絞り込みがかかります。

サイドバーの関連記事が消えた

is_main_query() のチェックが抜けています。テーマが new WP_Query() で組んでいるサブループにも適用されてしまっている状態です。

並び順が変わらない

原因は3つのどれかです。

症状 確認すること
まったく変わらない add_filter() の第4引数に 2 を渡しているか。省略すると $query が渡らずエラーになる
一部だけ変わる posts_orderby など別のフィルターや、SEOプラグインが並び順を上書きしていないか
管理画面まで変わった is_admin() の判定が入っているか

検索フォーム側や pre_get_postsorderby を明示している場合も、posts_search_orderby の指定は使われません。切り分けは、該当プラグインを一時停止して挙動を見るのが早いです。

日本語で検索してもヒットしない

WordPressの標準検索はSQLの LIKE で部分一致を取るので、単語の切れ目に関係なくヒットします。それでも出ないなら、次を疑ってください。

  • 検索語に全角スペースが入っている。WordPressは半角スペースを区切りとして扱うため、全角スペースはそのまま1つの文字列として検索される
  • 探している言葉が本文ではなくカスタムフィールドに入っている。標準検索はカスタムフィールドを見ない
  • 対象の投稿タイプが絞り込みから漏れている、または除外設定に入っている

全角スペースを半角に寄せるなら、functions.php にこれを足します。

/**
 * 検索語の全角スペースを半角に統一する
 */
function soreiine_normalize_search_query( $query ) {
    if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
        return;
    }

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

    $query->set( 's', preg_replace( '/[\x{3000}\s]+/u', ' ', $s ) );
}
add_action( 'pre_get_posts', 'soreiine_normalize_search_query' );

\x{3000} は全角スペースのUnicodeコードポイントです。/u 修飾子はUTF-8として扱う指定で、日本語を扱うときは必須です。

検索結果の件数がページ送りと合わない

検索結果の1ページあたり件数を変えるなら、テンプレート側ではなく pre_get_posts で設定します。

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

    // 1ページあたりの件数
    $query->set( 'posts_per_page', 20 );
}
add_action( 'pre_get_posts', 'soreiine_search_posts_per_page' );

20 を好きな件数に変えてください。 ここで設定すれば found_posts と総ページ数も同じ条件で計算されます。テンプレート側で query_posts() を使ってクエリを組み直していると件数とページ送りがずれるので、テーマにその記述があれば先に消してください。

何を検索されているかを知りたい

そもそも何を直すべきかは、実際の検索語を見ないと決まりません。GA4にはサイト内検索を自動で計測する設定があり、検索結果ページのURLに s= パラメータが付いていれば拾えます。管理画面の「データストリーム」→「拡張計測機能」でサイト内検索がオンになっているか、対象のクエリパラメータに s が含まれているかを確認してください。

計測環境の組み方は、こちらの記事で扱っています。



よくある質問

プラグインを使わずに実装するメリットは何ですか

更新のたびに動作確認する対象が減ることです。検索まわりのプラグインはWordPress本体やテーマの更新で挙動が変わることがあり、原因の切り分けに時間がかかります。この記事のコードは20〜30行程度なので、何をしているかを読めば把握できますし、不要になったら削除するだけで元に戻ります。ただし、あいまい検索や日本語の形態素解析といった高度な機能が必要なら、専用のプラグインや外部検索サービスを検討してください。

exclude_from_search を設定すればページを非公開にできますか

できません。この設定が効くのはサイト内検索の対象範囲だけで、URLを直接開けばページは表示されますし、サイトマップやアーカイブからも辿れます。特定の人にだけ見せたいページは、パスワード保護・非公開設定・会員制プラグインなど、公開ステータス側の仕組みで制御してください。検索から外す設定と、アクセスを制限する設定は別物です。

ブロックテーマでも同じコードが動きますか

functions.php に置くPHPコード(絞り込み・除外・並び順・空検索の許可)は、ブロックテーマでもそのまま動きます。クエリを操作するフックはテーマの種類に依存しないためです。一方、search.phpsearchform.php はクラシックテーマ向けのテンプレート階層なので、ブロックテーマでは templates/search.html をサイトエディターで編集するか、クエリループブロックの「結果が見つからない場合」の中身を設定します。

pre_get_postsposts_search_orderby はどちらを使うべきですか

役割が違うので併用します。pre_get_posts はクエリの条件(どの投稿タイプを、何件取るか、何を除外するか)を決める段階のフックで、posts_search_orderby はSQLに変換された後の並び順だけを差し替えるフックです。絞り込みと除外は前者、タイトル一致優先などの複雑な並び替えは後者で書きます。単純な並び替え(日付順・タイトル順)だけなら pre_get_postsorderby を設定するだけで済みます。

カスタムフィールドの中身も検索対象にできますか

標準検索では対象になりません。posts_joinposts_where フィルターでメタテーブルを結合すれば可能ですが、SQLが複雑になり、同じ投稿が複数行返るのを防ぐ処理(posts_distinct)まで自分で面倒を見る必要があります。カスタムフィールドを本格的に検索対象にしたいなら、検索拡張プラグインを使うか、投稿本文側に情報を持たせる設計に変えるほうが保守しやすくなります。


まとめ

サイト内検索は、実装量に対して効果が読みやすい領域です。この記事でやったことは3つだけでした。

  • pre_get_posts で検索対象を絞り、要らないものを除外する — 固定ページ・システム用ページ・特定カテゴリーを結果から外す
  • posts_search_orderby でタイトル一致を上に持ち上げる — 探している人の期待に並びを合わせる
  • 0件のときに次の行き先を用意する — 検索結果ページを行き止まりにしない

どれも外部ライブラリなし、プラグインなしで書けます。まずは今開いているサイトで「サイト名」「料金」「採用」あたりを検索してみてください。何が返ってくるかを見れば、どこから手を付けるかは決まります。

作業のあとは、冒頭で挙げた3ヶ所(トップ・アーカイブ・管理画面の一覧)を必ず開き直してください。検索を直したつもりで別の一覧が壊れる事故が、この領域では一番よく起きます。

テーマ選定の段階から検索周りの拡張性を考えたい場合は、こちらもあわせてどうぞ。