WordPressのキーワード検索の対象を「カスタムフィールド」と「タクソノミー名」に広げたいという希望で、単純に「WP Extended Search」や「Search Everything」というPluginを入れればいいと思ってたのですが、実際に入れてみるとどうも思ったのと挙動が違う。

例えば、
記事1
タイトル「りんごジュース」フィールド1「ジュース」

記事2
タイトル「みかんジュース」フィールド1「ジュース」

という記事があったとします。

「みかん ジュース」で検索をかけたとき、検索結果で期待するのは「記事2」のみがひっかかることなんですが、「Search Everything」だと「記事2」も「記事1」もひっかかるんです。

タイトルで「みかん」「ジュース」で検索すると記事2だけ引っかかるんですが、フィールド1で「みかん」「ジュース」で検索すると記事1もひっかかるからか。タイトルとフィールド1ではOR関係になっているのではないかと思います。

理想は「タイトル」+「フィールド1」を一つの項目にして、その中で「みかん」も「ジュース」も入っているものを検索するという形になりますが、「Search Everything」だと「タイトル」と「カスタムフィールド」それぞれで検索して、かつ、複数キーワードの片方どちらかでも引っかかると検索結果に出してしまうというようです。これでは「絞り込み検索」になりません。

「WP Extended Search」もうまく行きませんでした。

https://www.webernote.net/wordpress/custom-fields-search.html
https://taneakashi.ad-mk.com/wordpress-site-search-customize.html

上記の二つの記事をドッキングさせて「カスタムフィールド」と「タクソノミータイトル」をキーワード検索対象にすることができました。

function custom_search($search, $wp_query) {
	global $wpdb;

	if (!$wp_query->is_search)
			return $search;
	if (!isset($wp_query->query_vars))
			return $search;

	$search_words = explode(' ', isset($wp_query->query_vars['s']) ? $wp_query->query_vars['s'] : '');
	if ( count($search_words) > 0 ) {
			$search = '';
			foreach ( $search_words as $word ) {
					if ( !empty($word) ) {
							$search_word = '%' . esc_sql( $word ) . '%';
							$search .= " AND (
								 {$wpdb->posts}.post_title LIKE '{$search_word}'
								OR {$wpdb->posts}.post_content LIKE '{$search_word}'
								OR {$wpdb->posts}.ID IN (
								SELECT distinct post_id
								FROM {$wpdb->postmeta}
								WHERE meta_value LIKE '{$search_word}'
								)
                                OR {$wpdb->posts}.ID IN (
                                 SELECT distinct r.object_id
                                 FROM {$wpdb->term_relationships} AS r
                                 INNER JOIN {$wpdb->term_taxonomy} AS tt ON r.term_taxonomy_id = tt.term_taxonomy_id
                                 INNER JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id
                                 WHERE t.name LIKE '{$search_word}'
                               )
							) ";
						
					}
			}
	}
	return $search;
}
add_filter('posts_search','custom_search', 10, 2);

さて、このままではよくわからないことがあります。add_filterとかフックとかはなんとなく知っているのですが、いい機会なので改めて勉強してみることにしました。

add_filter
指定したフィルターフックに、関数を登録します。

add_filter( フィルターフックの名前【WordPressが用意している】,  フィルターが適用されたときに呼び出される関数の名前【自分が作ります】, フィルターフックに登録された関数の中で、この関数を実行する順序, 関数が受け取る引数の個数 )

つまり「add_filter('posts_search','custom_search', 10, 2);」は、 post_searchというフィルターフックが適用されたときにcustom_searchという関数を実行します。関数が実行されるときに受け取る引数は2個です。ということです。 さて、post_searchというフィルターフックの方を見てみます。

apply_filters_ref_array( ‘posts_search’, string $search[Search SQL for WHERE clause.], WP_Query $this )

function custom_search($search, $wp_query)で受け取ってる$searchはmysqlのWHERE部分で、$wp_queryは実行中のなんていえばいいかわかりませんがqueryのオブジェクトです。

posts_searchというフィルターフックで、custom_searchという自分で設定した関数を実行するんですが、そのときにWHERE条件の入っている$searchを受け取って、WHERE条件を変更して、返すということをしています。

もともとの$searchは「タイトル」だけ検索して返しているっぽいです。

なので、「タイトル」と「コンテンツ」とpostIDでくっつけた「カスタムフィールド(metavalue)」と「タクソノミー」を検索対象にしています。

カスタムフィールド部分ですが

 {$wpdb->posts}.ID IN (SELECT distinct post_id FROM {$wpdb->postmeta}
 WHERE meta_value LIKE '{$search_word}')

の「SELECT」以下で、{$search_word}が入っているmeta_value(カスタムフィールド)のpost_idを重複を消して(distinct)引っ張ってきて、「$wpdb->posts}.ID IN」で、そのpost_idたちにIDが含まれている記事、を取得して、それらをORで繋げてます。

つまりーこのsearchは「キーワードn」まであるとして、

「キーワード1」が含まれるタクソノミーを持っている記事IDの記事か、「キーワード1」が含まれるカスタムフィールド(meta_value)を持っている記事IDの記事か、「キーワード1」を持っているタイトルか、コンテンツ

そして(AND)

「キーワードn」が含まれるタクソノミーを持っている記事IDの記事か、「キーワードn」が含まれるカスタムフィールド(meta_value)を持っている記事IDの記事か、「キーワードn」を持っているタイトルか、コンテンツ

というのを、n個分繰り返して、ANDによって記事を絞り込んでいます。

なるほど!!(一人納得)

自分でわかるように書いたので、$wp_queryとかの記述が間違ってたらごめんなさい。

これ、後日見て自分でわかるかなぁ……。

(2021/04/01追記 エイプリルフールではない)
このあと、この検索の変更が、管理画面のメディアの検索などにも影響することを知りました。「$search .= “AND post_type = ‘post'”;」という限定が邪魔をしていたので、その1行は削除しました。