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.
You can actually simplify this a bit by passing the date parameter in $wp_query->date_query, and removing the separate filter function.