2026年6月18日

2026年6月18日

WordPressの検索機能をカスタマイズする方法

はじめに

「WordPressの検索が投稿タイトルしかヒットしない」「特定の投稿タイプだけ検索したい」「検索結果ページのデザインを改善したい」——pre_get_postsフックを使うとWordPressの検索動作を柔軟にカスタマイズできます。

症状・原因

WordPressのデフォルト検索はタイトルと本文のみを対象とし、カスタムフィールドやカスタム投稿タイプのコントロールが難しいです。pre_get_postsとAjaxを組み合わせることでUXを大幅に改善できます。

解決手順

ステップ1:pre_get_postsで検索対象を絞り込む

// functions.php: 検索対象の投稿タイプと並び順を制御
add_action( 'pre_get_posts', function( WP_Query $query ) {
    // 管理画面・メインクエリ以外は対象外
    if ( is_admin() || ! $query->is_main_query() ) return;

    if ( $query->is_search() ) {
        // 検索対象の投稿タイプを指定
        $query->set( 'post_type', [ 'post', 'portfolio', 'faq' ] );

        // 検索結果の並び順
        $query->set( 'orderby', 'relevance' ); // relevance / date / title

        // 1ページの表示件数
        $query->set( 'posts_per_page', 20 );

        // 特定カテゴリーを除外
        $query->set( 'category__not_in', [ 5, 12 ] ); // カテゴリーIDで除外
    }
} );

ステップ2:カスタムフィールドも検索対象に含める

// functions.php: カスタムフィールドを検索対象に追加
add_filter( 'posts_search', function( $search, WP_Query $query ) {
    global $wpdb;

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

    $search_term = $query->get( 's' );
    if ( empty( $search_term ) ) return $search;

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

    // カスタムフィールド「price」「description」も検索対象に
    $meta_search = $wpdb->prepare(
        " OR EXISTS (
            SELECT 1 FROM {$wpdb->postmeta}
            WHERE {$wpdb->postmeta}.post_id = {$wpdb->posts}.ID
            AND {$wpdb->postmeta}.meta_key IN ('product_description', 'client_name')
            AND {$wpdb->postmeta}.meta_value LIKE %s
        )",
        $like
    );

    $search .= $meta_search;

    return $search;
}, 10, 2 );

// JOINを追加してDISTINCTで重複を排除
add_filter( 'posts_distinct', function( $distinct, WP_Query $query ) {
    if ( $query->is_search() && $query->is_main_query() ) {
        return 'DISTINCT';
    }
    return $distinct;
}, 10, 2 );

ステップ3:search.phpで検索結果ページをデザインする

<?php
// search.php: カスタム検索結果ページ
get_header();

$search_query = get_search_query();
$found_posts  = $GLOBALS['wp_query']->found_posts;
?>

<main class="search-results-page">
    <div class="container">

        <header class="search-header">
            <h1 class="search-title">
                <?php if ( $search_query ) : ?>
                    「<strong><?php echo esc_html( $search_query ); ?></strong>」の検索結果
                    <span class="found-count"><?php echo esc_html( $found_posts ); ?> 件</span>
                <?php else : ?>
                    検索
                <?php endif; ?>
            </h1>

            <!-- 検索フォームを再表示 -->
            <div class="search-form-wrap">
                <?php get_search_form(); ?>
            </div>
        </header>

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

            <div class="search-results">
                <?php while ( have_posts() ) : the_post(); ?>
                    <article class="search-result-item">
                        <div class="result-meta">
                            <span class="post-type-badge">
                                <?php echo esc_html( get_post_type_object( get_post_type() )->labels->singular_name ); ?>
                            </span>
                            <time datetime="<?php echo get_the_date( 'c' ); ?>">
                                <?php the_date(); ?>
                            </time>
                        </div>

                        <h2 class="result-title">
                            <a href="<?php the_permalink(); ?>">
                                <?php the_title(); ?>
                            </a>
                        </h2>

                        <?php if ( has_post_thumbnail() ) : ?>
                            <div class="result-thumbnail">
                                <?php the_post_thumbnail( 'thumbnail' ); ?>
                            </div>
                        <?php endif; ?>

                        <div class="result-excerpt">
                            <?php the_excerpt(); ?>
                        </div>
                    </article>
                <?php endwhile; ?>
            </div>

            <?php the_posts_pagination( [
                'prev_text' => '← 前のページ',
                'next_text' => '次のページ →',
            ] ); ?>

        <?php else : ?>
            <div class="no-results">
                <p>「<?php echo esc_html( $search_query ); ?>」に一致するコンテンツが見つかりませんでした。</p>
                <p>別のキーワードをお試しください。</p>
            </div>
        <?php endif; ?>

    </div>
</main>

<?php get_footer(); ?>

ステップ4:Ajax検索(インクリメンタルサーチ)を実装する

// functions.php: Ajax検索エンドポイントを登録
add_action( 'wp_ajax_live_search', 'my_live_search' );
add_action( 'wp_ajax_nopriv_live_search', 'my_live_search' );

function my_live_search() {
    check_ajax_referer( 'live_search_nonce', 'nonce' );

    $keyword = sanitize_text_field( $_POST['keyword'] ?? '' );

    if ( strlen( $keyword ) < 2 ) {
        wp_send_json_success( [] );
    }

    $results = get_posts( [
        's'              => $keyword,
        'posts_per_page' => 8,
        'post_type'      => [ 'post', 'portfolio' ],
        'post_status'    => 'publish',
    ] );

    $data = array_map( function( $post ) {
        return [
            'id'        => $post->ID,
            'title'     => esc_html( $post->post_title ),
            'url'       => get_permalink( $post->ID ),
            'thumbnail' => get_the_post_thumbnail_url( $post->ID, 'thumbnail' ),
            'type'      => get_post_type_object( $post->post_type )->labels->singular_name,
        ];
    }, $results );

    wp_send_json_success( $data );
}

// JavaScriptファイルをエンキュー
add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_script( 'live-search', get_stylesheet_directory_uri() . '/js/live-search.js', [], '1.0', true );
    wp_localize_script( 'live-search', 'liveSearch', [
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'live_search_nonce' ),
    ] );
} );
// js/live-search.js: インクリメンタルサーチ
(function() {
    const input   = document.querySelector('.search-field');
    const form    = input?.closest('form');
    if (!input || !form) return;

    let dropdown = null;
    let timer    = null;

    function createDropdown() {
        if (dropdown) return;
        dropdown = document.createElement('ul');
        dropdown.className = 'live-search-dropdown';
        form.style.position = 'relative';
        form.appendChild(dropdown);
    }

    function removeDropdown() {
        if (dropdown) { dropdown.remove(); dropdown = null; }
    }

    input.addEventListener('input', function() {
        clearTimeout(timer);
        const keyword = this.value.trim();

        if (keyword.length < 2) { removeDropdown(); return; }

        timer = setTimeout(async function() {
            const body = new FormData();
            body.append('action',  'live_search');
            body.append('keyword', keyword);
            body.append('nonce',   liveSearch.nonce);

            const res  = await fetch(liveSearch.ajaxUrl, { method: 'POST', body });
            const json = await res.json();

            if (!json.success || !json.data.length) { removeDropdown(); return; }

            createDropdown();
            dropdown.innerHTML = json.data.map(item => `
                <li>
                    <a href="${item.url}">
                        ${item.thumbnail ? `<img src="${item.thumbnail}" alt="">` : ''}
                        <span class="result-type">${item.type}</span>
                        <span class="result-title">${item.title}</span>
                    </a>
                </li>
            `).join('');
        }, 300);
    });

    document.addEventListener('click', function(e) {
        if (!form.contains(e.target)) removeDropdown();
    });
})();

ステップ5:検索フォームのHTMLをカスタマイズする

// functions.php: get_search_form()の出力をカスタマイズ
add_filter( 'get_search_form', function( $form ) {
    $form = '<form role="search" method="get" class="search-form" action="' . esc_url( home_url( '/' ) ) . '">
        <div class="search-input-wrap">
            <label class="screen-reader-text" for="search-field">検索:</label>
            <input
                type="search"
                id="search-field"
                class="search-field"
                placeholder="キーワードを入力..."
                value="' . esc_attr( get_search_query() ) . '"
                name="s"
                autocomplete="off"
                aria-label="サイト内検索"
            >
            <button type="submit" class="search-submit" aria-label="検索する">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
                    <path d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
                </svg>
            </button>
        </div>
    </form>';

    return $form;
} );

注意事項

  • カスタムフィールド検索をposts_searchフィルターで実装する場合、JOINの重複や重複レコードが発生しやすいためDISTINCTを忘れずに追加してください。
  • Ajax検索のエンドポイントは必ずcheck_ajax_referer()でNonceを検証してください。Nonceなしのエンドポイントはスパムリクエストの踏み台になりえます。
  • 検索キーワードは必ずsanitize_text_field()またはget_search_query()でサニタイズしてから使用してください。

まとめ

WordPressの検索カスタマイズは「pre_get_postsで投稿タイプ・件数を制御→posts_searchフィルターでカスタムフィールドを検索対象に追加→search.phpで結果ページをデザイン→Ajax検索でインクリメンタルサーチを実装→get_search_formフィルターで入力フォームを改善」の流れで整備します。関連記事:WordPressにXMLサイトマップを設定してSEOを改善する方法WordPressのページネーションをカスタマイズする方法

お気軽にご相談ください

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