5

Next in my quest to extend WordPress is altering how WordPress uses the the $sentence variable in posts_search to include an additional string, when the $sentence variable meets a certain criteria.

The specific usecase here is that when someone types in something like TL123 it should also search for TL-123, with TL% being the wildcard here. This is to cut-down on search mistakes (for those that do not include the hyphen).

I've seen how we can filter into posts_search with additional SQL queries (I'm actually using this one with pretty good success so far), but am a little fuzzy on how this could work. Any help would be greatly appreciated. Thanks!

Edit - to include more information about needs thanks to @kaiser

  • Searches are typically done by typing in TL123, when in reality the actual post title is TL-123 in most cases - so, the goal here is to intercept when the search query includes TL(NUMBER) and also search TL-(NUMBER).

Again, the trick here is that some posts do not include TL- but only TL, so that's why I'm looking for search queries that match that pattern to also search an additional "partial phrase".

Thanks!

Update

Alright, so based off of kaiser's starter function, I have come up with the following:

function wpse66815_search_query_string( $search, &$wp_query )
{
    if (!is_admin() && is_search()) {

        print_r($search);

        global $wp_query;

        // get search term
        $search_term = array_shift($wp_query->query_vars['search_terms']);
        // specify string we'll use to replace
        $replace_var = 'TL';

        // find matches for that string
        preg_match_all("/{$replace_var}(?:[^0-9]*)(\d+)/i", $search_term, $out);

        // if there's no matches, return the normal search
        if ( empty($out[0]) )
            return $search;

        // find/generate the search term with the replacement
        $modified_search_term = preg_replace("/{$replace_var}(?:[^0-9]*)(\d+)/i", "{$replace_var}-$1", $search_term);

        // combine both the regular and modified search term
        $new_search[] = $search_term;
        $new_search[] = $modified_search_term;

        //var_dump($new_search);

        // generate the new search query
        foreach ( $new_search as $keyword )
        {
            $new_string_parts[] = $GLOBALS['wpdb']->prepare(
                 "
                    AND ((%s.post_title LIKE '%%%s%%') OR (%s.post_content LIKE '%%%s%%'))
                 "
                ,"{$GLOBALS['wpdb']->prefix}posts"
                ,like_escape( $keyword )
                ,"{$GLOBALS['wpdb']->prefix}posts"
                ,like_escape( $keyword )
            );
        }

        // set $search equal to results
        $search = implode( " ", $new_string_parts );

        //print_r($search);
    }

    return $search;
}
add_filter('posts_search', 'wpse66815_search_query_string',500,2);

The part that I can't get past is actually is the actual SQL query -> error below:

WordPress database error: [You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '.post_title LIKE 'tl123') OR ('wp__posts'.post_content LIKE 'tl123')) ' at line 2]

SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND (('wp__posts'.post_title LIKE 'tl123') OR ('wp__posts'.post_content LIKE 'tl123')) AND (('wp__posts'.post_title LIKE 'TL-123') OR ('wp__posts'.post_content LIKE 'TL-123')) AND wp_posts.post_type IN ('post', 'page', 'attachment', 'galleries', 'idea_gallery', 'moulding_profiles', 'moulding_collection', 'moulding_combination') AND (wp_posts.post_status = 'publish' OR wp_posts.post_author = 2 AND wp_posts.post_status = 'private') ORDER BY wp_posts.post_date DESC LIMIT 0, 80

Any pointers on where I went wrong? Thanks!

Update #2

I've started to see a few issues:

  • It was doing wp__posts instead of wp_posts (I updated that above)
  • As kaiser mentioned, the LIKE %s portion may need to be LIKE %%s% but that is not parsing correctly.

Error is now:

SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND (('wp_posts'.post_title LIKE 'tl123') OR ('wp_posts'.post_content LIKE 'tl123')) AND (('wp_posts'.post_title LIKE 'TL-123') OR ('wp_posts'.post_content LIKE 'TL-123')) AND wp_posts.post_type IN ('post', 'page', 'attachment', 'galleries', 'idea_gallery', 'moulding_profiles', 'moulding_collection', 'moulding_combination') AND (wp_posts.post_status = 'publish' OR wp_posts.post_author = 2 AND wp_posts.post_status = 'private') ORDER BY wp_posts.post_date DESC LIMIT 0, 80

The error above (from what I can tell) is actually trying to look for a post that is both tl123 and TL-123, but I think OR is what I want (as I want to return posts in an either or situation).

Update #3

Updated function to properly escape %s, so error is now:

WordPress database error: [You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '.post_title LIKE '%tl123%') OR ('wp_posts'.post_content LIKE '%tl123%')) ' at line 2]

SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND (('wp_posts'.post_title LIKE '%tl123%') OR ('wp_posts'.post_content LIKE '%tl123%')) AND (('wp_posts'.post_title LIKE '%TL-123%') OR ('wp_posts'.post_content LIKE '%TL-123%')) AND wp_posts.post_type IN ('post', 'page', 'attachment', 'galleries', 'idea_gallery', 'moulding_profiles', 'moulding_collection', 'moulding_combination') AND (wp_posts.post_status = 'publish' OR wp_posts.post_author = 2 AND wp_posts.post_status = 'private') ORDER BY wp_posts.post_date DESC LIMIT 0, 80

Update #4

Here's what I ended up doing -> the foreach wasn't wrapping correctly (with the AND/OR operator as @kaiser mentioned), so I separated them out into their own array insertions.

function wpse66815_search_query_string( $search, &$wp_query )
{
    if (!is_admin() && is_search()) {

        //print_r($search);

        global $wp_query,$wpdb;

        // get search term
        $search_term = array_shift($wp_query->query_vars['search_terms']);
        // specify string we'll use to replace
        $replace_var = 'TL';

        // find matches for that string
        preg_match_all("/{$replace_var}(?:[^0-9]*)(\d+)/i", $search_term, $out);

        // if there's no matches, return the normal search
        if ( empty($out[0]) )
            return $search;

        // find/generate the search term with the replacement
        $modified_search_term = preg_replace("/{$replace_var}(?:[^0-9]*)(\d+)/i", "{$replace_var}-$1", $search_term);

        // combine both the regular and modified search term
        $new_search[] = $search_term;
        $new_search[] = $modified_search_term;

        var_dump($new_search);

        // generate the new search query
        $new_string_parts[] = $wpdb->prepare( "AND ((({$wpdb->posts}.post_title LIKE '%%%s%%') OR ({$wpdb->posts}.post_content LIKE '%%%s%%'))",like_escape( $new_search[0] ),like_escape( $new_search[0] ));
        $new_string_parts[] = $wpdb->prepare( "OR (({$wpdb->posts}.post_title LIKE '%%%s%%') OR ({$wpdb->posts}.post_content LIKE '%%%s%%')))",like_escape( $new_search[1] ),like_escape( $new_search[1] ));

        // set $search equal to results
        $search = implode( " ", $new_string_parts );

        //print_r($search);
    }

    return $search;
}
add_filter('posts_search', 'wpse66815_search_query_string',500,2);

It does have some trouble if someone were to type in tl123 tl456 (two occurences of the tl keyword), but I'm working on that portion. Thanks!

11
  • Please add more details about what "divides" a search string. Is it every non alpha(numeric?) character? If so, then simply split the string and add all those as LIKE escaped by like_escape().
    – kaiser
    Commented Oct 1, 2012 at 20:57
  • @kaiser Done - good idea. Am a bit new to SQL queries, so it's not a call for "doing it for me", but rather examples that I can build off of or an explanation of how this could theoretically work (plugging into and adding terms into the $sentence variable). I definitely want to learn, but we sometimes all need a nudge (more like a shove here). Thanks!
    – Zach
    Commented Oct 2, 2012 at 12:51
  • So if string would be AB123CD-45, then the LIKE´ parst would be array( 'AB', '123', 'CD', '45' );`? In other words: Split the search query string in numbers and letters?
    – kaiser
    Commented Oct 2, 2012 at 13:19
  • More if the $sentence has TL (then a number), like TL123, no matter what else is attached in the $sentence, it should also search for TL-123. For example, TL123 Supercallafrajalistic Expialadocious would also search TL-123 Supercallafrajalistic Expialadocious or TL123withwords will also search TL-123withwords. TLmoreletters would only search TLmoreletters (as that could be a regular word)
    – Zach
    Commented Oct 2, 2012 at 13:29
  • I'm pretty sure that the problem is the quotes around 'wp_posts' and the brackets around AND/OR.
    – kaiser
    Commented Oct 6, 2012 at 15:48

1 Answer 1

3
+50

Gladly I've written two plugins for that yesterday:

Filter/Core

That's what a search query part looks like inside the posts_search filter:

' AND (((wp_XX_posts.post_title LIKE '%test%') OR (wp_XX_posts.post_content LIKE '%test%'))) '

where wp_XX_ is just the $wpdb->prefix for my WPSE test site inside my local MU installation.

Plugin #1 - drop searching post types that we don't need.

Here's a plugin modifying the searched post types, as this is needed often.

<?php
/** Plugin Name: (#66815) »kaiser« Limit search query post types */

/**
 * Alter the searched post types
 * 
 * @param  object $query
 * @return object $query
 */
add_action( 'pre_get_posts', 'wpse66815_pre_get_posts' );
function wpse66815_pre_get_posts( $query )
{
    if ( $query->is_main_query() )
    {
        $query->set( 'post_type', 'YOUR_POST_TYPE' );
    }

    return $query;
}

Plugin #2 - modify the search string

Now, that we know how the default search string for the post title and content looks, we just have to rebuild it the way we need it:

<?php 
/** Plugin Name: (#66815) »kaiser« Modify search query string */

function wpse66815_search_query_string( $search_string )
{
    global $wpdb;

    $searched_for = preg_match_all(
        // Match a prefix (%), but exclude it from the capture
        // Any character, any number of repetitions
        // Match a suffix (%), but exclude it from the capture
        "/(?<=\%)(.*)(?=\%)/",
        $search_string,
        $search_string_matches
    );

    // We only need one element
    $searched_for = array_shift( $search_string_matches );

    // Now we need to search for [LETTERS (min 1)][NUMBER (zero or more)][CHARACTER (zero or more)]
    preg_match_all(
        "/([^a-zA-Z][\d+]*[-_ ]*)/",
        $searched_for,
        $string_part_matches
    );

    // Here we now got matches - if not, we can simply abort and leave the default
    $searched_for = array_shift( $string_part_matches );
    if ( empty( $searched_for ) )
        return $search_string;

    // Finally we need to split the string by all parts that are allowed
    // YOU NEED TO EDIT MY ANSWER HERE AND FILL IN WHAT WORKS FOR YOU
    $keywords = preg_split(
         "/([\s]*[\d+]*[-_ ]*)/",
         $searched_for,
         -1, // 0 & -1 are NO limit
         PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
    );

    // Now loop and build an array for further processing
    // Our first search string is - of course - the default string
    $string_parts = array( $search_string );
    foreach ( $keywords as $keyword )
    {
        $new_string_parts[] = $GLOBALS['wpdb']->prepare(
            " AND ((%s.post_title LIKE '%s') OR (%s.post_content LIKE '%s')) ",
            $wpdb->posts,
            $wpdb->esc_like( $keyword ),
            $wpdb->posts,
            $wpdb->esc_like( $keyword )
        );
    }

    // Now lets glue them together, return and see what we get...
    return implode( " ", $new_string_parts );
}

This 2nd plugin is untested, as my needs are different and I guess some of the regexes are not completely what you need - you'll have to fix that and update this answer (this is your part in "giving the community back" in this Q/A). A nice tool to build regex, that I just found, is Expresso. It's damn ugly (as the web presence), but extremely helpful.

3
  • Hm. Not sure if it shouldn't be '%%s%' instead...
    – kaiser
    Commented Oct 2, 2012 at 17:10
  • 1
    Really appreciate it @kaiser will do some testing and report back
    – Zach
    Commented Oct 2, 2012 at 20:50
  • Updated my original question with progress
    – Zach
    Commented Oct 5, 2012 at 19:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.