2026年8月18日

2026年8月18日

WordPressにElasticsearch全文検索を実装してUX・SEOを改善する方法

はじめに

WordPressのデフォルト検索はMySQLのLIKE検索に依存しており、大規模サイトではパフォーマンスの低下や検索精度の問題が顕著になります。Elasticsearchを導入することで日本語形態素解析・ファセット検索・検索サジェストなどの高度な機能を実現し、UXとSEO(直帰率低下)の両面で改善が期待できます。

症状・原因

  • 投稿数が増えるにつれてサイト内検索の速度が著しく低下している
  • 検索結果の精度が低く、ユーザーが目的のコンテンツに辿り着けない
  • ファセット(絞り込み)検索を実装したいが、WordPressの標準機能では困難

解決手順

ステップ1:ElasticsearchとElasticPressプラグインをインストールする

Dockerを使ったElasticsearch環境とElasticPressプラグインの設定です。

# Docker ComposeでElasticsearchを起動する
cat > docker-compose.yml << 'EOF'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false  # 開発環境のみ(本番はSSL必須)
      - ES_JAVA_OPTS=-Xms512m -Xmx512m
    ports:
      - "9200:9200"
    volumes:
      - es-data:/usr/share/elasticsearch/data

volumes:
  es-data:
EOF

docker-compose up -d

# Elasticsearchの動作確認
curl -s http://localhost:9200/_cluster/health?pretty | grep '"status"'

# 日本語形態素解析プラグイン(analysis-kuromoji)をインストールする
docker exec -it <container_id> \
  bin/elasticsearch-plugin install analysis-kuromoji

ステップ2:ElasticPressのインデックス設定とカスタムマッピングを構成する

ep_index_post_kill フィルターで除外投稿を制御し、カスタムマッピングを適用します。

<?php
// functions.php に追加 ─ ElasticPress インデックス制御

// 特定条件の投稿をインデックスから除外する
add_filter( 'ep_index_post_kill', function( $kill, $post ) {
    // 下書き・プライベート投稿はインデックスしない
    if ( in_array( $post->post_status, [ 'draft', 'private' ], true ) ) {
        return true; // trueを返すとインデックスをスキップする
    }

    // noindex メタがある投稿もスキップする
    if ( get_post_meta( $post->ID, '_noindex', true ) ) {
        return true;
    }

    return $kill;
}, 10, 2 );

// カスタムフィールドをインデックス対象に追加する
add_filter( 'ep_prepare_meta_allowed_keys', function( $keys ) {
    // 商品価格・著者名・カスタムカテゴリをElasticsearchに格納する
    $custom_keys = [ 'price', 'author_name', 'custom_category', 'product_sku' ];
    return array_merge( $keys, $custom_keys );
} );

// 日本語向けにkuromojiアナライザーを使ったカスタムマッピング
add_filter( 'ep_config_mapping', function( $mapping ) {
    // title フィールドに日本語アナライザーを適用する
    $mapping['mappings']['properties']['post_title'] = [
        'type'     => 'text',
        'analyzer' => 'kuromoji', // 日本語形態素解析
        'fields'   => [
            'raw' => [ 'type' => 'keyword' ], // ソート・集計用の未解析フィールド
        ],
    ];
    return $mapping;
} );

ステップ3:ep_formatted_argsフィルターで検索クエリをカスタマイズする

Elasticsearchへ送信するクエリをフィルターでチューニングします。

<?php
// functions.php に追加 ─ 検索クエリのカスタマイズ

add_filter( 'ep_formatted_args', function( $formatted_args, $args ) {
    if ( ! isset( $args['s'] ) || empty( $args['s'] ) ) {
        return $formatted_args;
    }

    // multi_matchクエリでtitleにより高いboostを付与する
    $formatted_args['query'] = [
        'bool' => [
            'should' => [
                [
                    'multi_match' => [
                        'query'  => $args['s'],
                        'fields' => [
                            'post_title^3',    // タイトルは3倍のスコアブースト
                            'post_content^1',
                            'post_excerpt^2',  // 抜粋は2倍のスコアブースト
                            'terms.name^1.5',  // タクソノミー名も検索対象に含める
                        ],
                        'type'     => 'best_fields',
                        'analyzer' => 'kuromoji', // 日本語形態素解析を使用する
                    ],
                ],
            ],
            'filter' => [
                [ 'term' => [ 'post_status' => 'publish' ] ], // 公開済みのみ
            ],
        ],
    ];

    return $formatted_args;
}, 10, 2 );

ステップ4:カテゴリ・タグ・価格のファセット検索を実装する

Elasticsearchのアグリゲーション機能でファセット(絞り込み)検索を実装します。

<?php
// functions.php に追加 ─ ファセット検索のアグリゲーション設定

add_filter( 'ep_formatted_args', function( $formatted_args, $args ) {
    // アグリゲーション(ファセット集計)を追加する
    $formatted_args['aggs'] = [
        // カテゴリ別の投稿数を集計する
        'categories' => [
            'terms' => [
                'field' => 'terms.category.slug.raw',
                'size'  => 20, // 上位20カテゴリを取得する
            ],
        ],
        // タグ別の投稿数を集計する
        'tags' => [
            'terms' => [
                'field' => 'terms.post_tag.slug.raw',
                'size'  => 30,
            ],
        ],
        // 価格帯ごとのレンジ集計(ECサイト向け)
        'price_ranges' => [
            'range' => [
                'field'  => 'meta.price.double',
                'ranges' => [
                    [ 'to'   => 1000 ],
                    [ 'from' => 1000, 'to' => 5000 ],
                    [ 'from' => 5000, 'to' => 10000 ],
                    [ 'from' => 10000 ], // 1万円以上
                ],
            ],
        ],
    ];

    return $formatted_args;
}, 10, 2 );

// URLパラメーターからファセット条件を取得してクエリに反映する
add_filter( 'ep_formatted_args', function( $formatted_args, $args ) {
    $selected_category = sanitize_text_field( $_GET['fcat'] ?? '' );

    if ( ! empty( $selected_category ) ) {
        // 選択されたカテゴリでフィルタリングする(post_filterを使うとfacet数を維持できる)
        $formatted_args['post_filter'] = [
            'term' => [ 'terms.category.slug.raw' => $selected_category ],
        ];
    }

    return $formatted_args;
}, 20, 2 );

ステップ5:検索クエリとクリックをログに記録して検索品質を改善する

検索クエリとクリックデータを収集し、検索アナリティクスとして活用します。

<?php
// functions.php に追加 ─ 検索クエリのロギング

// カスタムテーブルを作成する(プラグイン有効化時に一度だけ実行)
function create_search_log_table() {
    global $wpdb;
    $table   = $wpdb->prefix . 'search_logs';
    $charset = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE IF NOT EXISTS {$table} (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        query VARCHAR(255) NOT NULL,
        result_count INT DEFAULT 0,
        clicked_post_id BIGINT UNSIGNED DEFAULT NULL,
        searched_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_query (query),
        INDEX idx_searched_at (searched_at)
    ) {$charset};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
}

// 検索実行時にクエリをログに記録する
add_action( 'pre_get_posts', function( $query ) {
    if ( ! $query->is_main_query() || ! $query->is_search() ) {
        return;
    }

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

    global $wpdb;
    // 検索クエリをDBに非同期で記録する(パフォーマンスへの影響を最小化)
    $wpdb->insert( $wpdb->prefix . 'search_logs', [
        'query'      => $search_query,
        'searched_at' => current_time( 'mysql' ),
    ] );
} );

// 検索クリックをAJAXで記録するエンドポイント
add_action( 'wp_ajax_nopriv_log_search_click', function() {
    $post_id = (int) ( $_POST['post_id'] ?? 0 );
    $log_id  = (int) ( $_POST['log_id'] ?? 0 );

    if ( $post_id && $log_id ) {
        global $wpdb;
        // クリックされた投稿IDを検索ログに記録する
        $wpdb->update(
            $wpdb->prefix . 'search_logs',
            [ 'clicked_post_id' => $post_id ],
            [ 'id' => $log_id ],
            [ '%d' ], [ '%d' ]
        );
    }

    wp_send_json_success();
} );

注意事項

  • インデックス再構築: 大量投稿の初回インデックス時はWP-CLIコマンド wp elasticpress index --setup をバックグラウンドで実行すること
  • 日本語解析: analysis-kuromoji プラグインを使用しない場合、日本語の形態素解析が行われず検索精度が大幅に低下する
  • Elasticsearchのメモリ: 最低2GBのJVMヒープを割り当てること。小メモリ環境ではOpenSearch Serverlessを検討する

まとめ

ElasticPressを使ったElasticsearch連携により、WordPressの検索精度とパフォーマンスを大幅に向上できます。検索アナリティクスを活用して継続的に検索品質を改善することが長期的なSEO・UX向上につながります。関連記事:WordPressのパフォーマンスをGrafana・Prometheusで監視する方法

お気軽にご相談ください

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