I think the problem essentially relates to sql query structure and I am not an expert….
I need to search for posts (custom post type) by 2 parameters:
-
pd_city
-
pd_country
Please note that meta_query relation is ‘OR’ so if either of above two is LIKE we should have some results.
Third key (is_sponsored) is used to sort posts! It can be 1 or 0 and posts whose “is_sponsored” value equals to 1 should be listed on top.
So here’s the WordPress thing:
$sfp_query_args = array(
'sfp_complex_search' => 'yeap',
'tax_query' => array( array( 'taxonomy' => 'sfp_post_category', 'terms' => $term_id ) ),
//'meta_key' => 'is_sponsored',
'post_type' => 'sfpposts',
'post_status' => 'publish',
'showposts' => (int)$per_page,
'paged' => $paged,
'meta_query' => array( 'relation' => 'OR',
array( 'key' => 'pd_city', 'value' => $sfp_search_meta, 'compare' => 'LIKE' ),
array( 'key' => 'pd_country', 'value' => $sfp_search_meta, 'compare' => 'LIKE' ),
array( 'key' => 'is_sponsored' )
)
);
$sfp_search = new WP_Query( $sfp_query_args );
I also need to filter results with “posts_orderby” in order to get those sponsored to the top:
add_filter( 'posts_orderby', 'sfp_modify_search' );
function sfp_modify_search( $orderby ) {
if( !is_admin() && is_page( $this->options[ 'sfp_page_entries_search' ] ) ) {
global $wpdb;
$orderby = " CASE WHEN mt2.meta_value = 0 THEN 1 END, $wpdb->posts.post_date DESC ";
}
return $orderby;
}
The real problem relies in fact that with this query ALL POSTS from “sfp_post_category” are returned, not only those who match “pd_city” or “pd_country” because ALL POSTS HAVE “is_sponsored” meta key (and value set to 1 or 0).
Once again: “is_sponsored” is needed for sorting!
When var_dump
var_dump( $sfp_search->request );
…WordPress’ sql looks like this:
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
INNER JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id)
INNER JOIN wp_postmeta ON (wp_posts.ID = wp_postmeta.post_id)
INNER JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id)
INNER JOIN wp_postmeta AS mt2 ON (wp_posts.ID = mt2.post_id)
WHERE 1=1
AND ( wp_term_relationships.term_taxonomy_id IN (77) )
AND wp_posts.post_type = 'sfpposts'
AND (wp_posts.post_status = 'publish')
AND ( (wp_postmeta.meta_key = 'pd_city'
AND CAST(wp_postmeta.meta_value AS CHAR)
LIKE '%something%')
OR (mt1.meta_key = 'pd_country'
AND CAST(mt1.meta_value AS CHAR)
LIKE '%something%')
OR mt2.meta_key = 'is_sponsored' )
GROUP BY wp_posts.ID
ORDER BY CASE WHEN mt2.meta_value = 0 THEN 1 END, wp_posts.post_date DESC
LIMIT 0, 10
How do I eliminate all posts that do not match “pd_city” or “pd_country” from results?
The culprit
The culprit of the matter is meta queries not supporting different and/or nested relations – a shortcoming by the way, that has driven me nuts before as well. In a recent instance with a search scenario also.
What you want to do simply cannot be accomplished with
WP_Query
as well as one loop alone.As you appear to have noticed, whether you place the sorting key in the
meta_query
array or outside of it as a general query argument does not make a difference. If you set the meta query relation toOR
and specify ameta_key
anywhere of the query args without setting the accompanyingmeta_value
parameter, the query will always return at least all posts where that meta_key is set.By the way and for the sake of completeness: When you use a single meta_query with
!=
as a value formeta_compare
, the query will return all results with themeta_key
set and not equal to the givenmeta_value
– it will not return any posts that do not have themeta_key
used at all. Another point where meta queries fail.Solution 1
I see two options. For one, you could omit the
is_sponsored
meta key from the query, omit pagination as well, get the correct posts and do the sorting with a second instance ofWP_Query
, passing it the filtered post IDs via thepost__in
parameter:Note, that the
$orderby
paramter ofWP_Query
will take multiple values separated by a space. Your search modification might be more complex than necessary.Solution 2
Since I like your idea of var_dumping the query object’s
request
property, let me fire a quick – and, note, untested – secondary suggestion:If you slightly modified the given SQL by changing the logical operator of
OR mt2.meta_key = 'is_sponsored'
toAND
and moving it accordingly, you could pull the posts with$wpdb
:At this point you have two options as well:
Either iterate over the
$sfp_post_ids
array with a simpleforeach
and pull the post data withget_post()
individually within that loop, or, if you want the niceties ofWP_Query
– paging, template tags and so on – feed$sfp_post_ids
to thepost__in
parameter as in Solution 1.This all happens because of the
OR
relation onmeta_query
and the way WordPress generates the actual query string. I ended up hooking into theposts_clauses
filter to modify thewhere
andorderby
pieces of the query:Just add the filter before setting up your WP_Query object and then make sure to remove it after running your query to not affect other queries:
Remember to leave out the
meta_key
andorderby
from the query args.That is complicated 🙂
I was going to suggest that you may not need to have the
array( 'key' => 'is_sponsored' )
value in the ‘meta_query’ array and that you could do this by adding a ‘meta_key’ to main array, but it looks like you’ve tried that. Did you get the same results?JOIN
s can make things complicated. You hooked intoposts_orderby
. Have you considered hooking intoposts_fields
and adding a subquery that would get you your meta_value?(SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'is_sponsored' AND post_id = {$wpdb->posts}.ID) as is_sponsored