Blog

WordPressで特定のキーのカスタムフィールドの値をキーワード検索対象に含める方法…ってこれでいいのか?

Posted by admin at 5:49 日時 2019/12/12

注:技術記事ではなくただの愚痴のような記事です。frown

数年ぶりにWordPress案件に関わっているのですが、その中で「キーワード検索の対象に、特定のキーのカスタムフィールドも入れてほしい」という要件が出てきました。

通常、キーワード検索の対象は、投稿のタイトル、概要、本文です。プラスアルファで、カスタムフィールドに入れているデータの中にキーワードが入っていても、検索結果に出てほしいということです。

「こんなもん楽勝やろ」と思ったんですね。concrete5であれば、この

検索インデックスにコンテンツが含まれます。

チェックボックスに、チェックを入れればいいだけです。

とはいえ、いくらしばらくWordPress触っていないからといっても、このオプションがWordPressにないことくらいは覚えてました。

なくても、最近はWP_Meta_Queryがあるので、何とかなるやろと思いました。

調べてみると、全然何ともならないんですよね。meta_queryを使うことで、カスタムフィールド内をキーワード検索できるんですが、結果としてSQL文はこうなります。

SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
    INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id )
WHERE 1=1
    AND (
        (
            (wp_posts.post_title LIKE '%test%')
            OR (wp_posts.post_excerpt LIKE '%test%')
            OR (wp_posts.post_content LIKE '%test%')
        )
    )
    AND (
        (wp_postmeta.meta_key = 'test' AND wp_postmeta.meta_value LIKE '%test%' )
    )
    AND wp_posts.post_type = 'post'
    AND (
        wp_posts.post_status = 'publish'
        OR wp_posts.post_status = 'private'
    )
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_title LIKE '%test%' DESC, wp_posts.post_date DESC
LIMIT 0, 10;

投稿のタイトルか抜粋か本文にキーワードが含まれ、かつカスタムフィールドにもキーワードが含まれる時、というSQLになっちゃってます。そこじゃないんだよ…

投稿のタイトルか抜粋か本文かカスタムフィールドか、そのどれかにキーワードが含まれる時、としたいじゃないですか。できないんです。\WP_Meta_Query::get_sql_clauses の中で、メタクエリーが生成するSQL文全体をANDでつなげちゃってます。ハードコードされてるので、変更できない。

	protected function get_sql_clauses() {
		/*
		 * $queries are passed by reference to get_sql_for_query() for recursion.
		 * To keep $this->queries unaltered, pass a copy.
		 */
		$queries = $this->queries;
		$sql     = $this->get_sql_for_query( $queries );

		if ( ! empty( $sql['where'] ) ) {
			$sql['where'] = ' AND ' . $sql['where'];
		}

		return $sql;
	}

いっそのこと、meta_query側に全部持って来ればいいのかとも思いましたが、WP_Meta_Query側にフィルターが全然ないのでダメ…。ということは…いにしえのカスタマイズ手法、WP_Queryが生成するSQL文を正規表現でゴニョゴニョしかないのか…。

というわけで、ゴニョゴニョしてみました…。

<?php
/*
Plugin Name: Include custom field in keyword search
Version: 0.1
*/

// Include custom field with these keys only
$meta_keys_to_include = ['foo', 'bar'];

add_filter('posts_join', function ($join, $query) {
    /**
     * @var string $join
     * @var WP_Query $query
     * @var wpdb $wpdb
     */
    global $wpdb;

    // Do not change join clause when these conditions...
    if (is_admin() || !$query->is_main_query() || !$query->is_search()) {
        return $join;
    }

    // Otherwise, join wp_postmeta table
    $join .= "INNER JOIN {$wpdb->postmeta} ON ( {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id )";

    return $join;
}, 10, 2);

add_filter('posts_search', function ($search, $query) use ($meta_keys_to_include) {
    /**
     * @var string $search
     * @var WP_Query $query
     * @var wpdb $wpdb
     */
    global $wpdb;

    // Do not change where clause when these conditions...
    if (is_admin() || !$query->is_main_query() || !$query->is_search()) {
        return $search;
    }

    // Otherwise, modify where clause
    $meta_where = "";
    if (is_array($meta_keys_to_include)) {
        foreach ($meta_keys_to_include as $meta_key) {
            $meta_where .= " OR ({$wpdb->postmeta}.meta_key = '{$meta_key}' AND {$wpdb->postmeta}.meta_value LIKE $1)";
        }
    }
    if ($meta_where) {
        $search = preg_replace(
            "/\(\s*{$wpdb->posts}\.post_title\s+LIKE\s*(\'[^\']+\')\s*\)/",
            "({$wpdb->posts}.post_title LIKE $1)" . $meta_where,
            $search
        );
    }

    return $search;
}, 10, 2);

add_filter('posts_groupby', function ($groupby, $query) {
    /**
     * @var string $groupby
     * @var WP_Query $query
     * @var wpdb $wpdb
     */
    global $wpdb;

    // Do not change where clause when these conditions...
    if (is_admin() || !$query->is_main_query() || !$query->is_search()) {
        return $groupby;
    }

    // Otherwise, let's add a group by clause
    if (empty($groupby)) {
        $groupby = "{$wpdb->posts}.ID";
    }

    return $groupby;
}, 10, 2);

このプラグインを有効化で、投稿のタイトル、または抜粋、または本文、またはfooとbarのいずれかのキーのカスタムフィールドの値にキーワードが含まれていると検索結果に出るようになります。

もしサブクエリで使いたい場合は、$query->is_main_query() のチェックを外してください。

しかし、この手法って、結局WP_Queryが吐き出すSQL文の仕様が変わったら死ぬわけです。で、結構アップデートで仕様変わるんですね。意外と。

そもそも、常々「WordPressはWP_Queryから脱却すべき」という持論を持ってまして、どこかでPDOに移行すべきだと思うんです。PDOを正しく使っていれば、よほどのレアケースでない限り、99.99%SQLインジェクションから守られます。PDOを使っていないから、いまだにWordPressではSQLインジェクションが発生し得るし、その度コミュニティがコストを払って対策するわけですが、PDOさえ使えれば不要なコストだと思います。

じゃあなぜすんなり移行できないかというと、上記のような現状のWP_Queryに過度に依存したカスタマイズで成り立ってるサイトがたくさんあるからだというのは間違いなく大きな理由だと思います。だから、こういうカスタマイズは出来るだけしたくない…。

が、他に方法が思いつかない…。


Share this entry