Skip to content

Get relevant search results in WordPress using Relevanssi and Ajax

The purpose of this article is to potentially solve a critical problem with the default WordPress search engine. The common problems we face are as follows:

  • It doesn’t obtain results by keyword density (relevance)
  • There are problems when trying to include AND / OR / NOT keywords in searches
  • It doesn’t search phrases
  • It doesn’t fuzzy match

It is possible to tackle these problems yourself by trying to hook into the array of search filters WordPress offers during it’s search processing, but rather than splitting your fingertips trying to work around the horrible default WordPress search mechanism, Mikko Saari has already written the Relevanssi plugin that aims to solve all of these problems.

Relevanssi has a premium version that adds additional features and support from the author. You can see the feature comparisson here.

The fundamentals in this article can be achieved using the free version of Relevanssi. But in order to gain the most benefit it is recommended you purchase the premium version.

What are we going to do?

Relevanssi plugin does work out of the box as you would expect. It will hook into the default WordPress search and add all of it’s features to it. But we’re going to be doing something slightly different. We’re going pass the search query to WP_Query(); using Ajax, and return the results back to the DOM without a page refresh. Then, we are going to demonstrate how to use the relevanssi_hits_filter to filter those results by date ranges.

So what we will end up with is a search engine that is capable of returning the results for keyword search using the AND / OR operators and a specified date from and to in order of keyword density (relevance).

The Keyword Search

The keyword search backed by Relevanssi accepts the following operators to help filter your results.

  • OR (default)
  • AND (+) Premium Only
  • NOT (-) Premium Only
  • Phrase (“”)

So in order to perform a search of articles that might contain WordPress or Ajax or Relevanssi:

wordpress ajax relevanssi

Or if you wanted articles that may contain WordPress or Ajax and must contain Relevanssi:

wordpress ajax +relevanssi ## premium only

Or if they must contain these keywords:

+wordpress +ajax +relevanssi ## premium only

Or if you wanted articles that may contain WordPress or Ajax, but not Relevanssi:

wordpress ajax -relevanssi ## premium only

Or if you wanted to search a phrase

"Using Relevanssi and Ajax"

The date search

You may choose to add some kind of date picker here. Since this article is purely to demonstrate how to pass queries through the search engine, I am just going to write in the date format like so DD-MM-YYYY. We will need to convert this into a proper timestamp later in order to compare the dates of the articles.

Let’s start

I have decided that this would be most scalable and easier to demonstrate by creating a WordPress shortcode. But you can implement this inside templates if you like.

First, let’s create a basic HTML layout where we will be able to do our search and where our results will appear. In your functions.php file, add this:

function my_search(){
 
    $html = '<div id="my_search_wrapper">';
    // The Form
    $html .= '<label for="keyword_search">Keyword Search</label><br/>';
    $html .= '<input type="text" name="keyword_search" id="keyword_search" /><br/>';
    $html .= '<label for="date_from">Date From (DD-MM-YYYY)</label><br/>';
    $html .= '<input type="text" name="date_from" id="date_from" /><br/>';
    $html .= '<label for="date_to">Date To (DD-MM-YYYY)</label><br/>';
    $html .= '<input type="text" name="date_to" id="date_to" /><br/>';
    $html .= '<input type="submit" name="search_submit" id="search_submit" /><br/>';
 
    // The Results
    $html .= '<div id="my_results"><b>RESULTS WILL APPEAR HERE</b></div>';
 
    $html .= '</div>'; // End my_search_wrapper
 
    return $html;
}
 
// Init Shortcode
add_shortcode( 'my-search', 'my_search' );

Now if you create a new page and add the shortcode [my-search] the form should display in your page.

Getting the user input

Now we need to get the the search query from the user. We are going to use jQuery to retrieve this data field by field and store as JSON so that it can be passed back to the search engine for processing. You can choose to have this more dynamic if you like but in this case I am going to use the submit button to trigger this action.

Create a my_search.js somewhere in your theme folder (I’m using the /js folder) then make sure you tell WordPress to load this script.

In functions.php

wp_register_script( 'my_search',  get_template_directory_uri() . '/js/my_search.js', array('jquery'),null,true );
wp_enqueue_script( 'my_search' );

In my_search.js

jQuery(document).ready(function() {
    jQuery('#search_submit', '#my_search_wrapper').on('click', function() {
 
        // Instantiate our filter var
        var filter = [];
 
        // Get the values
        var s_keywords = jQuery('#keyword_search', '#my_search_wrapper').val();
        var s_date_from = jQuery('#date_from', '#my_search_wrapper').val();
        var s_date_to = jQuery('#date_to', '#my_search_wrapper').val();
 
        // If keywords exist, push to filter
        if (s_keywords != '') {
            filter.push({
                s_keywords: s_keywords
            });
        }
 
        // If date from exists, push to filter
        if (s_date_from != '') {
            filter.push({
                s_date_from: s_date_from
            });
        }
 
        // If date to exists, push to filter
        if (s_date_to != '') {
            filter.push({
                s_date_to: s_date_to
            });
        }
 
        // Output the results in the console
        console.log(JSON.stringify(filter));
    });
});

One tip when selecting elements using jQuery is that the element selector accepts a second argument called context. This tells the selector engine where to start, saving resources from having to iterate through all of the elements in the DOM.

Now if you start inputting information into the form and click ‘Submit’ the data should be returned into the console as a JSON string.

Building the search function

Now we need a function that accepts these input values and uses them to get the search results. Since we are using Ajax, we will need to tell WordPress that this function can be used with Ajax.

In your functions.php file:

function my_search_results() {
    // Get the filter
    $filter = $_REQUEST['filter'];
 
    // Print array
    print_r($filter);
 
    // Kill
    die(); 
}
 
add_action( 'wp_ajax_nopriv_my_search_results', 'my_search_results' );
add_action( 'wp_ajax_my_search_results', 'my_search_results' );

And we need to send our results to this function.

In my_search.js underneath console.log(JSON.stringify(filter));

...
console.log(JSON.stringify(filter));
 
// Send to my_search_results
var Ajax = {
    ajaxurl: "/wp-admin/admin-ajax.php"
};
jQuery.post( Ajax.ajaxurl, {
    filter: filter,
    action: 'my_search_results'
}, function(res) {
 
    // Return the results from the function
    jQuery('#my_results', '#my_search_wrapper').html(res);
});

Now if you go back to your search page, add some value and click submit. You should see your query in the form of an array being output.

Example output:

Array
(
    [0] => Array
        (
            [s_keywords] => wordpress ajax +relevanssi
        )
 
    [1] => Array
        (
            [s_date_from] => 1-1-2013
        )
 
    [2] => Array
        (
            [s_date_to] => 30-1-2013
        )
 
)

Processing the input

What we want to do here is modify our current my_search_results() function so that it processes and outputs the results we expect it to. We are using WordPress’s WP_Query() object to achieve this.

We need to get the data we want from the Ajax passed array. We also need a global query variable so that we can use it later in a function to filter the date range results. Then we need to feed this date into our WP_Query() object.

Focus on keywords

in the my_search_results() function in functions.php

function my_search_results() {
 
    // Global date range vars
    global $date_from;
    global $date_to;
 
    // Sanitise the array
    $i = 0;
    foreach($_REQUEST['filter'] as $item) {
        $key = key($item);
        $val = current($item);
 
        if(!isset($result[$key])) {
            $result[$key] = array();
        }
        $filter[$i][$key][] = $val;
    }
 
    // Store the values in variables
    foreach ($filter as $value) {
        $keyword_search = $value["s_keywords"][0];
        $date_from = $value["s_date_from"][0];
        $date_to = $value["s_date_to"][0];
    }
 
    // Send search string to WP_Query args
    $args = array(
        's' => $keyword_search,
 
        // Feel free to add other args
    );
 
    $query = new WP_Query( $args );
 
    // Add relevanssi query args
    relevanssi_do_query($query);
 
    // The Loop
    if ( $query->have_posts() ) {
        while ( $query->have_posts() ) {
            $query->the_post();
 
            echo '<a href="'.get_permalink().'">';
            echo get_the_title();
            echo '</a>';
            echo '<br />';
            echo get_the_date();
            echo '<br />';
            the_excerpt();
 
        }
    }
 
    // Kill
    die();
}

If you were to simply type keywords now, the most relevant articles to your search will appear in order.

Now, the date range

In order to handle the additional date range filter, we are going to use relevanssi_hits_filter which allows us one last chance to hook into the search process before it is output.

First, we need to make sure this filter is added before the query has started.

In functions.php right before $query = new WP_Query( $args ); and right after global $query;

...
global $query;
 
// Date range filter
if(isset($date_from) || isset($date_to)) {
    add_filter( 'relevanssi_hits_filter', 'query_date_range' );
}
 
$query = new WP_Query( $args );

Create a new function in functions.php called query_date_range() that will be executed just before the results are output from the query.

function query_date_range( $hits ) {
 
    // Allow the function to access the vars
    global $date_from;
    global $date_to;
 
    $date_searches = array();
 
    foreach($hits[0] as $hit) {
 
        // Default false
        $date_search = false;
 
        // WordPress has a default time stamp of Y-m-d H:i:s
        $post_date =  DateTime::createFromFormat('Y-m-d H:i:s', $hit->post_date);
        $post_date = $post_date->format('d-m-Y');
 
        // If the from post date is greater than or equal to date from
        if(strtotime($post_date) >= strtotime($date_from)) {
            // Return this result
            $date_search = true;
        }
 
        // If the to post date is greater than or equal to date to
        if(strtotime($post_date) >= strtotime($date_to)) {
            // Don't return this result
            $date_search = false;
        }
 
        // Filter the array with our new date conditions
        $date_search ? array_push($date_searches, $hit) : array_push($hit);
 
    }
 
    $hits[0] = $date_searches;
    return $hits;
}

Conclusion

This is a good example of how you can enhance the default WordPress search mechanism to return results that are a bit more relevant. There are many ways you can be much more creative with this but hopefully this article provides the base to work from.

The full code

/js/my_search.js

jQuery(document).ready(function() {
    jQuery('#search_submit', '#my_search_wrapper').on('click', function() {
 
        // Instantiate our filter var
        var filter = [];
 
        // Get the values
        var s_keywords = jQuery('#keyword_search', '#my_search_wrapper').val();
        var s_date_from = jQuery('#date_from', '#my_search_wrapper').val();
        var s_date_to = jQuery('#date_to', '#my_search_wrapper').val();
 
        // If keywords exist, push to filter
        if (s_keywords != '') {
            filter.push({
                s_keywords: s_keywords
            });
        }
 
        // If date from exists, push to filter
        if (s_date_from != '') {
            filter.push({
                s_date_from: s_date_from
            });
        }
 
        // If date to exists, push to filter
        if (s_date_to != '') {
            filter.push({
                s_date_to: s_date_to
            });
        }
 
        // Output the results in the console
        console.log(JSON.stringify(filter));
 
        // Send to my_search_results
        var Ajax = {
            ajaxurl: "/wp-admin/admin-ajax.php"
        };
        jQuery.post( Ajax.ajaxurl, {
            filter: filter,
            action: 'my_search_results'
        }, function(res) {
 
            // Return the results from the function
            jQuery('#my_results', '#my_search_wrapper').html(res);
        });
    });
});

functions.php

function my_search(){
 
    $html = '<div id="my_search_wrapper">';
    // The Form
    $html .= '<label for="keyword_search">Keyword Search</label><br/>';
    $html .= '<input type="text" name="keyword_search" id="keyword_search" /><br/>';
    $html .= '<label for="date_from">Date From (DD-MM-YYYY)</label><br/>';
    $html .= '<input type="text" name="date_from" id="date_from" /><br/>';
    $html .= '<label for="date_to">Date To (DD-MM-YYYY)</label><br/>';
    $html .= '<input type="text" name="date_to" id="date_to" /><br/>';
    $html .= '<input type="submit" name="search_submit" id="search_submit" /><br/>';
 
    // The Results
    $html .= '<div id="my_results"><b>RESULTS WILL APPEAR HERE</b></div>';
 
    $html .= '</div>'; // End my_search_wrapper
 
    return $html;
}
 
// Init Shortcode
add_shortcode( 'my-search', 'my_search' );
 
wp_register_script( 'my_search',  get_template_directory_uri() . '/js/my_search.js', array('jquery'),null,true );
wp_enqueue_script( 'my_search' );
 
function my_search_results() {
 
    // Global date range vars
    global $date_from;
    global $date_to;
 
    // Sanitise the array
    $i = 0;
    foreach($_REQUEST['filter'] as $item) {
        $key = key($item);
        $val = current($item);
 
        if(!isset($result[$key])) {
            $result[$key] = array();
        }
        $filter[$i][$key][] = $val;
    }
 
    // Store the values in variables
    foreach ($filter as $value) {
        $keyword_search = $value["s_keywords"][0];
        $date_from = $value["s_date_from"][0];
        $date_to = $value["s_date_to"][0];
    }
 
    // Send search string to WP_Query args
    $args = array(
        's' => $keyword_search,
 
        // Feel free to add other args
    );
 
    // If dates apply
    if(isset($date_from) || isset($date_to)) {
        add_filter( 'relevanssi_hits_filter', 'query_date_range' );
    }
 
    $query = new WP_Query( $args );
 
    // Add relevanssi query args
    relevanssi_do_query($query);
 
    // The Loop
    if ( $query->have_posts() ) {
        while ( $query->have_posts() ) {
            $query->the_post();
 
            echo '<a href="'.get_permalink().'">';
            echo get_the_title();
            echo '</a>';
            echo '<br />';
            echo get_the_date();
            echo '<br />';
            the_excerpt();
 
        }
    }
 
    // Kill
    die();
}
 
add_action( 'wp_ajax_nopriv_my_search_results', 'my_search_results' );
add_action( 'wp_ajax_my_search_results', 'my_search_results' );
 
function query_date_range( $hits ) {
 
    // Allow the function to access the vars
    global $date_from;
    global $date_to;
 
    $date_searches = array();
 
    foreach($hits[0] as $hit) {
 
        // Default false
        $date_search = false;
 
        // WordPress has a default time stamp of Y-m-d H:i:s
        $post_date =  DateTime::createFromFormat('Y-m-d H:i:s', $hit->post_date);
        $post_date = $post_date->format('d-m-Y');
 
        // If the from post date is greater than or equal to date from
        if(strtotime($post_date) >= strtotime($date_from)) {
            // Return this result
            $date_search = true;
        }
 
        // If the to post date is greater than or equal to date to
        if(strtotime($post_date) >= strtotime($date_to)) {
            // Don't return this result
            $date_search = false;
        }
 
        // Filter the array with our new date conditions
        $date_search ? array_push($date_searches, $hit) : array_push($hit);
 
    }
 
    $hits[0] = $date_searches;
    return $hits;
}

A demonstration will appear here very soon.

About 

I run a small web development agency in Salisbury, UK. We provide both front-end/back end solutions and infrastructure management. My specific role is to identify the needs of our clients and providing an online solution that is easy to administer, secure, scalable and maintainable.

 

My agency website is white-fire.co.uk. Contact us if you need a consultant.

Published inWordPress

One Comment

  1. You can actually simplify this a bit by passing the date parameter in $wp_query->date_query, and removing the separate filter function.

Leave a Reply

Your email address will not be published. Required fields are marked *