Code Your Own Autosuggest Enabled Advanced Search WordPress Plugin

Want to add advanced search functionality without depending on third-party plugins or themes? Build your own advanced WordPress Search Plugin with support for autosuggest, custom post types, taxonomies, custom fields, and caching from the ground up.

In this article, I’ll show you how to build an object-oriented plugin to add a shortcode-based advanced search form that can be filtered using custom post types (CPTs), custom taxonomies as well as custom metadata created using Advanced Control Fields (ACF). The search form will also feature an AJAX powered autosuggest, and make good use of WordPress transients to cache results.

While free plugins are a popular way to add greater functionality, they often come with their own set of problems. And there are so many factors to consider when getting that combination right. At times, developing your own plugin for areas that require a more fine-tuned approach may prove far more beneficial in the larger scheme of things. It will also allow you greater control over usability, performance and application security.

Note: This article is intended for intermediate-advanced WordPress developers. It assumes that you have a working knowledge of PHP, JavaScript, the WordPress Loop, Transients, and the WordPress Plugin API. If you’d like a refresher, I recommend that you read through the following:

For this article, I’ve prepared a custom WordPress plugin that adds an autosuggest enabled search form to any WordPress page using a shortcode. It also lets you fine-tune the search results to a single or multiple custom post types, and displays them in a flexbox grid layout. You can download my search plugin from here to follow along with the article.

Later in the article, I’ll extend the same plugin, and add complex search features such as filtering on multiple taxonomies and custom fields for a fictitious video library but the example can be easily extended to any scenario such as a product search or even faceted search.

CPTs and ACFs add tremendous power and functionality to WordPress, and are fairly common in any bespoke development project. My goal is to help you integrate several such advanced WordPress features to play together in a real-world application. So let’s get started!

Note: For the rest of the article and throughout the code, the term Post Type refers to the default Posts and Pages post types as well as any registered Custom Post Types.

Flow of Control of the Custom Search Plugin

Before I dive into the code, here’s a high-level view of how the plugin works behind the scenes. The plugin’s search engine kicks in once the user enters the keywords in the search bar, or when the search form is submitted. And then, a lot depends on the state of the transient.

advanced-search-plugin-flow-of-control
Flow of Control of the Custom Search Plugin

Note that I’m not only passing the cached post titles via wp_localize_script but also caching the data locally in JavaScript. This is because the autosuggest can have a huge impact on the performance if an AJAX request is made each time a search key is entered.

advanced-search-plugin-delete-transients
Sates in which the transient is deleted

For a resource-sensitive area like application search, I centered my implementation around the following ideas:

  • Cache only what is needed in the Transient, and not the entire WP_Query object
  • Ensure that as few AJAX calls are made for the autosuggest
  • Ensure that the transient does not hold stale information
  • Provide settings to control which Post Types will be included in the search

Naturally, one size will not fit all, and some amount of tweaking will be required based on the complexity of your application.

Let’s examine the structure of the plugin to get a sense of what goes where.

Structure of the Advanced Search Plugin

The search plugin is based on my own plugin template which is a fork of the original WordPress Plugin Boilerplate project. Here’s how it’s structured in the backend:

  • inc/core/* – Core functionality of the plugin
  • inc/core/class-init.php – Registration of hooks for the admin menu, shortcode, AJAX handler, admin notices, scripts and styles
  • inc/admin/class-admin.php – Functionality for the settings area of the plugin in the admin dashboard
  • inc/common/class-common.php – Functionality for providing the shortcode based search and autosuggest AJAX handler
  • inc/common/views/* – The search form and the search results
  • inc/common/js/* – Autosuggest Handler
  • inc/common/css/* – CSS for the search form

On activation, the plugin adds an Options Page as a sub-level menu item to the Settings administration menu. It lists the registered custom post types along with the default Posts and Pages post types as options to include in the advanced search.

advanced-search-plugin-settings
Plugin settings page to select post types

I won’t go into the details of this but to see how I added the admin page, take a look at the define_admin_hooks() method of inc/core/class-init.php and add_plugin_admin_menu() method of inc/admin/class-admin.php. The markup for the settings page can be found in inc/admin/views/html-nds-advanced-search-admin-options.php.

The place where things actually start to get interesting is inc/common/class-common.php. The entry-point to it is through the define_common_hooks() method in inc/core/class-init.php.

So let’s start with the shortcode.

Shortcode to Load the Custom Search Form

The register_shortcodes() method of inc/common/class-common.php adds the shortcode [nds-advanced-search] which is used to plug in the custom search form.

Looking at the gist above, you’ll notice that the content for the shortcode ( i.e. the search form ) is returned by the shortcode_nds_advanced_search() callback function. I could have done something like this:

However, I took a slightly different approach.

Rather than directly output the markup of the search form in the shortcode callback, I used the get_search_form filter and the get_search_form() function to load the search form.

I’ve done this to allow you more control over the rendering of the search form. Let’s see how that works.

The get_search_form Filter Hook and Template Tag

The get_search_form() template tag is a great way of taking control over the display of the WordPress search. It first looks to see if the theme contains searchform.php and loads the markup defined by it; otherwise, it loads the built-in search form provided by WordPress.

Or you could completely replace the search form using the get_search_form filter hook, in which case get_search_form() will return the markup defined by the filter callback.

On submitting the default search form, WordPress will take over and render the search results using the search.php template, provided the following conditions are met:

  • The search form does a GET to the homepage of the site
  • The name attribute of the input text field is named s

In my example, I don’t want WordPress to handle the search, and I’ve replaced the default form using the filter hook: add_filter( 'get_search_form', array( $this, 'advanced_search_form_markup' ) )

wordpress_get_search_form_filter_hook
Contents of $form before the get_search_form filter is applied

Here, I’ve paused execution to inspect the $form variable passed by the filter. It holds the form HTML provided by searchform.php of the Twenty Sixteen theme which will be replaced by the markup of my search form in inc/common/views/html-nds-advanced-search-form.php.

As I’m not following any of the above rules about the form action or the name of the input attribute, WordPress will do nothing. This is fine as I want to take full control of the search results, and also load it on the same page as that of the shortcode.

custom search form with shortcode
Loading the custom search form with a shortcode

The use of a custom form with get_search_form is purely a design decision, and you may have a different take on it. I intentionally kept the default search form and the one provided by the plugin separate. It also ensures that the search form used by a theme in the sidebar, header etc. are not affected by the custom search form.

Handling Form Submission

The search form of the plugin submits to the same page as that of the shortcode, and so, the shortcode callback handles the form submission as well.

If, for some reason, you want to handle the form-data on a different page, have a look at my article on Handling Form Submissions in WordPress.

Let’s now see how the plugin renders the search results.

Using WP_Query for Querying Posts in Post Types & the Search Expression

The search results are garnered using a custom WP_Query and rendered through inc/common/views/html-nds-advanced-search-results.php.

My custom WP_Query takes the following arguments:

  • 's' => $search_term – Where $search_term is the keyword to search for
  • 'sentence' => true – Perform a full phrase search
  • 'post_type' => $post_types – Include posts belonging to $post_types which holds the Post Types specified in the plugin settings
  • 'post__in' => $cached_post_ids – Only search against Post IDs available in $cached_post_ids
  • 'no_found_rows' => true – Speed up query execution by not counting the number of rows found

The sentence parameter will cause WP_Query to return posts where the full search term is present. This is like a strict search, and will probably reduce the result set. It may or may not work in your favor, and you should tweak it based on your requirements. The no_found_rows is another parameter that works for me as I don’t need pagination in my loop.

I am also making use of the post__in parameter to further restrict the search to Posts belonging to specific Post IDs. The Post IDs will vary based on the selected Post Types, and as new posts are created or old ones are deleted.

If you look at the flowchart at the beginning of the article, you’ll notice the conditional logic for checking cached posts in the transient in the gist above.

Using WordPress Transients to Cache Posts

The get_transient() function returns the state of the transient. If it exists, it returns the array containing the cached Post IDs and Titles of posts for the required Post Types. If the transient expires or does not exist or is deleted during a post or settings update, it will return false, and get_posts() will be invoked through the cache_posts_in_post_types() method.

You may need to adjust the arguments to get_posts() based on your requirements as it controls which Post IDs are cached.

The cache_posts_in_post_types() method is also used by the autosuggest handler to cache post titles. If you don’t intend to use autosuggest, you could further speed up get_posts() by only retrieving IDs using the return fields parameter.

advanced-search-results-flexbox
Custom query search results in a flexbox layout

Let’s now look at the final piece of the puzzle – the search autosuggest.

Adding an Autosuggest Feature to the Search Form

I’ve made use of the jQuery UI Autocomplete widget to provide the search suggestions as it’s already included in WordPress. However, there are many other external libraries that you could also use.

The enqueue_scripts method of inc/common/class-common.php takes care of loading the required script.

If cached post titles are available in the transient, they are also made available to the autosuggest JavaScript via the wp_localize_script function. This helps to minimize the number of AJAX calls made to WordPress for search suggestions.

The code below is fairly rudimentary but serves the purpose of handling the autosuggest.

Here are few important aspects of the autosuggest script:

  • It makes use of jQuery.grep() to filter the post titles. You can further refine the match using Regular Expressions
  • Search keys are again cached in a local object to reduce the grep calls but you could also take advantage of the HTML Web Storage API to make the cache persistent
  • An AJAX request is made only when wp_localize_script passes an empty value for post titles

So let’s see how the AJAX request is handled.

The Autosuggest AJAX Handler

The action: "nds_advanced_search_autosuggest" property of the AJAX request specifies the wp_ajax_{action} WordPress hook to be executed and allows handling the AJAX request on the server-side.

The handler indirectly invokes get_posts() through cache_posts_in_post_types() which you saw earlier, and then sends the post titles as the AJAX response using the wp_send_json() function.

advanced-search-autosuggest-in-action
Autosuggest for the custom search.

Ensuring Data in the Transient is Not Stale

It is important to delete the transient when posts belonging to the required post types are created, updated etc. The delete_post_cache_for_post_type() method of class-admin.php takes care of this, and is invoked using the transition_post_status action hook in define_common_hooks() of class-init.php.

Also, note that expired Transients won’t clutter up the database as WordPress will clear them each time a reference is made to it.

Great! At this stage, we have a fully functional advanced search plugin. Let’s take this to next level by implementing a search page with custom taxonomies and advanced custom fields.

Case Study: Implementing Custom Video Search with Search Filters

A more advanced search would typically also allow users to refine the search using filters. You can easily do this by using the post metadata such as the taxonomy terms and the custom fields for the search filters. This is what I mean:

advanced-search-with-search-filters-cutomfields-taxonomies
Advanced search with search filters using custom taxonomies and advanced custom fields

I’ve used Advanced Custom Fields to add custom metadata for the Video custom post type. Some of these appear as search filters as shown above, and the rest are used in the display of the search results.

custom-fields-with-acf
Custom meta for the Video custom post type created with Advanced Custom Fields

I’ve also added a couple of custom taxonomies: Video Types and Video Locations to the custom post type. The taxonomy terms are used as checkboxes in the search filter.

custom-taxonomies-for-cpt
Custom Taxonomies for the Video Custom Post Type

Note: Here, I’ve assumed that the custom taxonomies and custom fields are registered for a single post type or are shared across the required post types. I’ve also assumed that the post types do not use the built-in post_tag and category taxonomies.

The entire code for implementing the search filters can be downloaded from here for reference.

Defining the Structure of the Form-Data

It’s a good idea to define the structure of the HTML form before adding the markup. I decided to use an associative array to group related information. On form submission, I wanted the form-data to be available in the $_POST superglobal as shown below:

search-form-input-using-arrays
Capturing form input stored in an associative array in the shortcode callback

This is purely a coding preference as it allows me to capture the form input into fewer variables that I can easily loop over, but it does make the form HTML more complex.

So let’s start with modifying the search form in inc/common/views/html-nds-advanced-search-form.php to add the custom taxonomies as search filters.

Creating a Dynamic List of Checkboxes Using Custom Taxonomies for the Search Filter

To retrieve the taxonomies associated with the Video post type, I made use of the get_object_taxonomies() function with the second parameter set to objects. This gave me access to the taxonomy’s slug, name and label, which I then used to collect the respective terms with the get_terms() function as shown below:

Based on the number of custom taxonomies and terms returned, the HTML markup for the checkboxes are dynamically created in the for loop.

Querying posts using taxonomies will require you to set the tax_query parameter of WP_Query.

Creating a Dynamic tax_query to Query Posts Using Custom Taxonomies

The tax_query parameter takes an array of arrays where each inner array represents a taxonomy.

Here, I am creating a dynamic tax_query based on the terms selected by the user. If the user selects terms from multiple taxonomies in the search filter, multiple taxonomy arrays are generated along with the Relation parameter set to ‘OR’ in the outer array.

Also, the tax_query is added to the WP_Query arguments only if the user selects a term in the search filter checkboxes.

Similarly, let’s see how custom fields can be used as search filters to further refine the custom search.

Creating Search Filters Using Custom Metadata Created with Advanced Custom Fields

The custom fields for the Video post type that you saw above were created using the Group field type provided with the premium version of ACF. To use the language and duration custom fields as dropdown lists, I first had to extract their unique values. To achieve this, I made use of the ACF function get_field_object() which returns a unique array based on the type of custom field.

get-custom-fields-from-acf-group-field-type
Inspecting the data object returned by the get_field_object() function of ACF

This is where a debugger would come in very handy as identifying the required field in large arrays may get tedious. Once I identified that the information I needed was available in the choices array, I could generate the markup for the HTML select element.

The search filters for the date-range were created using jQuery UI Datepicker. I won’t go into the details of this but you can refer the entire code for the search filters here.

Querying posts using custom fields will require you to set the meta_query parameter of WP_Query.

Creating a Dynamic meta_query to Query Posts Using Custom Fields

Similar to tax_query the meta_query parameter can also take an array of arrays, where each inner array represents metadata in the form of key/value pairs. Here, based on the type of custom field selected by the user, I am generating the inner meta_query array.

In the gist above, you’ll notice that I’m using the compare 'BETWEEN' operator to query posts between a date range. For this to work, the date needs to be stored in YYYY-MM-DD, and also requires converting the date returned by jQuery UI Datepicker into the same format for the comparison to work.

Note: In my example, as the taxonomies and custom fields are not shared across post types, only posts belonging to the Video post type will be returned if the search filters are used.

Adding Filter Buttons to Search Results

To improve the usability and user experience of search forms, features such as sorting and filter buttons are commonly employed in applications. I’d recommend you take a look at a popular library Isotope.js that does the job of sorting and filtering the DOM really well.

Here’s a crude example of using HTML buttons to further filter the displayed search results with jQuery.

sub-filter-buttons-in-search-results
Adding sub filter buttons to search results

To make this work, I took advantage of the WordPress Loop where I added HTML buttons based on the selected taxonomy terms, and also the taxonomy of each post to the class attribute of its list item container.

The entire code for rendering the search results with filter buttons is available here for reference.

Bonus Code: Loading Search Results in a Modal Lightbox

My case study for the video search won’t be complete without a mechanism to load the videos returned in the search results. Here, I am using a jQuery library Colorbox to load the videos returned by the search in a Modal dialog box.

load-search-results-in-modal-dialog
Loading Videos from the search results in a Modal Dialog box with Colorbox

The URL and the credits of the video are extracted from the advanced custom fields that you saw above.

I’ve used HTML data attributes to display the credits in the modal, but with this approach, you can load pretty much anything you want.

Wrapping Up

We’ve covered a lot of ground here, and I hope you’ve found this article useful. Feel free to use my search-plugin and the case study example to build your own awesome advanced search page.

Karan Gupta
Do you use a custom search on your site? Let us know in the comments below.