meta_queryでOR検索を行うと実用に耐えられないほど重いのを何とかする

query_postsにmeta_queryを指定してカスタムフィールド内の検索を行うというのはよく使われる処理ですが、'relation'=>'OR' や 'compare'=>'LIKE' を指定すると途端に重くなります。
具体的には以下のようなコードは実用に耐えられないほど重くなる可能性があります。

meta_queryは結局のところSQL文を出力するものですので、「LIKEは重い」「ORは重い」というSQLの常識はmeta_queryでも同様で、条件が増えれば増えるほど重くなり実用に耐えるものではありません。

そこでmeta_queryを指定した際に発行されるSQL文を書き換え、LIKEやORを使わないようにします。
具体的にはget_meta_sqlというフィルターフックを使います。これはmeta_query使用時のSQL文に対するフックで、where句とjoin句の配列を返します。

たとえば前述のような住所検索を実行する場合、

・検索対象として必要なのはカスタムフィールド入力値の「○○区」あるいは「○○県」まで

という点を満たすSQLに書き換えればLIKEを使用することはありません。

具体的には以下の記述をfunctions.phpに追加します。
検索対象とするカスタムフィールドの値を「区」(が存在しなければ「市」)より左側の値のみ切り出しています。

実際に使用する際はvar_dump($result["where"]);で出力されるSQLを確認しながら置換処理を書く必要があると思います。

AD

Share

Commentsコメント

メールアドレスは公開されません。コメントは必ずご入力ください。

HTMLタグは使用できません。ソースコードを書き込みたい場合はCodetterGistCodePenなどのご利用が便利です。

匿名 さんより
同様の事象で困っており大変参考になりました。ありがとうございました。
haru さんより
こちらに書かれていることをやりたいのですが、わたしにはまったく理解できないです。。
もう少し初心者のわたしにも理解できるレベルまで下げて詳しく解説していただけませんか!?
お願いします。
管理人 さんより
haru様
コメントありがとうございます。

行いたい処理は

・meta_queryのOR検索を実装したい(meta_queryがよく分からない)
・meta_queryは分かるけど重すぎるのでなんとかしたい

のいずれでしょうか?

なお、当記事は2年以上前のWordPressで検証した結果になりますので、現行のWordPressでは参考にならない内容かもしれません。

※meta_queryが高速化しているとか、WP Super Cacheなどのプラグインで充分速度が出るなど。
haru さんより
ありがとうございます。

>・meta_queryのOR検索を実装したい(meta_queryがよく分からない)
>・meta_queryは分かるけど重すぎるのでなんとかしたい
> のいずれでしょうか?

すみません。。どちらもです。

実は、勉強も兼ねて「Theme My Login」を使った会員制サイトを作ろうとしていました。
いろいろな方のサイトを参考にしながら、下記のコードを書いて登録された会員情報を絞り込み検索できるところまでできたのです。(ここまでで約1ヵ月)

------------------------------------------
if ( !empty($_POST['aa']) ) {
$args['meta_query'][] = array(
'key' => 'user_prefecture',
'value' => $_POST['aa'],//都道府県
'compare' => '=',
);
}

if ( !empty($_POST['bb']) ) {
$args['meta_query'][] = array(
'key' => 'user_address',
'value' => $_POST['bb'],//市区町村
'compare' => 'LIKE'
);
}

if ( !empty($args['meta_query']) && count($args['meta_query']) > 1 ) {
$args['relation'] = 'AND';
}

$users = new WP_User_Query( $args );
foreach ( $users->results as $user ) {
    ・
    ・
------------------------------------------

ユーザ情報を10件ほど登録してテストしていまして、少ないからなのかもわかりませんが、ここまではまだ重たい感じはでていませんでした。

次にしようとしたのが、市区町村の検索キーワードを1つではなくて、『「神戸」か「芦屋」か「西宮」が含まれる』などのように、空白区切りの複数キーワードで絞り込み検索できるようにしようと思い、市区町村のところを下記のコードをに変更しました。

------------------------------------------
$bb= mb_ereg_replace(" ", " ", mysql_real_escape_string($_POST['bb']));
$bb = explode(' ',$bb);

if ( !empty($_POST['bb0']) ) {
$args['meta_query'][][] = array(
'key' => 'user_address',
'value' => $_POST['bb0'],//市区町村
'compare' => 'LIKE'
);
}
if ( !empty($_POST['bb1']) ) {
$args['meta_query'][][] = array(
'key' => 'user_address',
'value' => $_POST['bb1'],//市区町村
'compare' => 'LIKE'
);
$args['meta_query']['relation'] = 'OR';

------------------------------------------

訳も分からず書いていると一応できるようにはなったのですが、たぶんおかしいのだと思います。
本当は$_POST['bb0']や$_POST['bb1']を一つづつ書かなくてもできる方法があるのでしょうけど、今はこれが限界でした。(これにまた1ヵ月以上費やす)

このコードでいくと、キーワード2つまでは2秒程度考えて結果を表示してくれるのですが、キーワードを3つにすると、ずーっと考え中のまま結果を出してくれないのです。
たぶん、重たいとかの問題ではなくてコードの書き方がダメだからだと思うのですが。。
どちらにしろ、この問題がいずれ解決できたとしても、実用段階では登録ユーザ数がどんどん増えるに伴い重たくなるのだろうなと思い、もうやめようと半ば諦めていた時に管理人さまのサイトに出会いました。

でも。。どこをどうやって自分用に置き換えればいいのかわからなくて、おそらく、preg_replaceの中のどこかだと思うのですが、なんて書いてあるのか全然わからない状態です。

長々とすみませんでした。
そんなこんなでコメントを送ってしまいました。
お返事いただけるとうれしいですが、わたしに理解できるのか不安です。
よろしくお願いいたします。
haru さんより
管理人さま

すみません、投稿させていただいたあとで読み直してまして言葉足らずのところがありました。
$_POST['bb0']や$_POST['bb1']のところは、実際には$_POST['bb5']ぐらいまで書いてます。
$_POST['bb5']まで書けば大丈夫だろうと言う単純な考えです。

それとですけど、
情けない話ですが、「LIKEは重い」「ORは重い」という"SQLの常識"もなぜなのかわかっていなくて、

その解決方法として、下記のように解説されてますが、
>・検索対象として必要なのはカスタムフィールド入力値の「○○区」あるいは「○○県」まで
>という点を満たすSQLに書き換えればLIKEを使用することはありません。

SQLに書き換える!? 状態です。
管理人 さんより
haru様

コメントありがとうございます。

記事中の処理で想定してるケースは、

・「addr1」というカスタムフィールドに「神戸市西区本町1-2-3」や「明石市中央町3-4-5明石ビル」みたいな値が入っている

という場合に、

・「区」が存在する市の場合は区単位で、なければ市単位で記事を取り出す

という処理を行ないたいというものです。

SQLの書き換えで行っている内容は

・「神戸市西区本町1-2-3」の場合は「神戸市西区」までしか検索しない
・「明石市中央町3-4-5明石ビル」の場合は「明石市」までしか検索しない

という処理を行うようにしています。こうすることで「'compare'=>'='」であったとしてもヒットするようになります。
この処理はカスタムフィールドが「神戸市」か「明石市」からはじまるという前提があるので使えるものであって、たとえば「兵庫県神戸市」という入力が交じる場合は使うことができません。


WP_User_Queryの場合でもmeta_query使用時は「get_meta_sql」フックが使えますので、functions.phpに

add_filter('get_meta_sql', 'get_meta_sql_hook');
function get_meta_sql_hook( $result ){
var_dump($result); //ここで$resultの内容を出力
     return $result;
}

と書いてやると、実際どのようなSQLが発行されているかが分かります。
そのSQLを見て、どんな検索ワードが来るか、その検索ワードを最短で一致させるにはどうすればよいか、を考えることになります。


SQLというのはデータベースからデータを取得するためのプログラム言語の一種なのですが、正直なところ結構難しいのとめんどくさいです。私自身も積極的には書かないですし書きたくありません。
meta_queryなどのような記事やユーザーを抽出する仕組みは、SQLを書かずに好みのデータを取得するために作られているものですので、極力SQLを書かない方法を考えるほうが本来は望ましいと思います。

私がこの記事のような方法を取ったのはカスタムフィールドに「神戸市西区○○町」みたいな住所を既に入力してしまっていたからであって、もし入力前に気付いていたら「神戸市西区」「○○町」と2つのカスタムフィールドを使うか「神戸市西区」をタクソノミーにしていたと思います。いわば必要に迫られて仕方なくこの記事のような方法を取った、とも言えます。


たとえば「user_address」を区町番地のみにし、「user_pref」のようなユーザーメタに市の名前だけを保持するのはどうでしょうか?そうすると「'compare'=>'='」の完全一致で検索することもできますし、meta_queryの延長で考えられますのでまだ理解しやすいのではないかと思います。


ちなみにLIKEやORというのは要は部分一致とOR検索のことで、検索する範囲と回数が多くなれば重くなる、という話です。jQueryの$('#hoge')は早いけど$('.fuga')は遅いというのと意味合いは似ています(1個しかないと分かっているIDと、何個あるか分からないクラスではどちらが探すのが楽か、ということです)。
haru さんより
詳しく説明して頂きましてありがとうございます。
少しわかったように思います。

今作成中のサイトは、「兵庫県」 「神戸市」 「西区本町1-2-3」 と言うように3つのフィールドに分けて入力するようにしてます。

こちらで紹介されている内容はこのような場合を想定した処理ではないのですね。

でも検索サイトを構築する場合には極力LIKEやORを使用しないで処理できるように考えることが重要だと言うことがわかりました。

今後サイトを作りこんでいく中で、ひょっとしたら必要となることがあるかもわかりません。
その時はまたこちらのサイトを訪ねさせていただきます。
その頃には、このコードも理解できるようになっていればいいのですが。

わたしのようなド素人に優しく対応していただきまして本当に嬉しく思いました。
ありがとうございました。