In this tutorial we are going to create a custom search form in WordPress. Specifically, we are going to add a custom search form on the archive of a custom post type.

Below is the final result.

0. Initial Set Up (Optional)

This tutorial is going to cover searching against a custom post type tagged with a custom taxonomy, custom fields, and relational data. For context, my setup is below.

A director custom post type

custom post type A movie_category custom taxonomy

custom taxonomy A movie custom post type A rating custom field A director custom relational field The ability to categorize each movie with the movie_category custom taxonomy



1. Add Custom Query Vars

WordPress has the concept of Query Vars. By default, WordPress ships with several dozen Public and Private Query Vars.

Query vars are fed into WP_Query, WordPress' post querying API. Public query vars can be used in the URL querystring. Private query vars cannot.

Since public query vars can be passed into the URL, we can alter the current loop just by appending the URL. For example, if you navigate to the blog page for a WordPress site and pass ?s=test into the URL, the loop will show all posts that contain test in the title or description.

Wouldn't it be nice if we could search against custom fields, related content and other data? For example, what if I wanted to search for all movies that have at least a 3 star rating? I can't just add ?rating=3 to the URL and expect it to work.

In order to for WordPress to recognize these custom parameters, we need to create Custom Query Vars.

Open your theme's functions.php file and enter the following.

function add_query_vars_filter ( $vars ) { $vars [ ] . = 'director_id' ; $vars [ ] . = 'rating' ; $vars [ ] . = 'movie_category_ids' ; return $vars ; } add_filter ( 'query_vars' , 'add_query_vars_filter' ) ;

What's happening here? The query_vars filter allows you to register new custom query vars. This means that WordPress will now recognize the director_id, rating and movie_category_ids if they're used in the URL as parameters.

You can name these custom query vars anything you want. However, the names of these custom query vars cannot conflict with existing Query Vars.

2. Override The Archive Query Served By WordPress

In my example, we are going to add a search form to the archive of a custom post type. I'm doing this because I personally thinks its a cleaner solution because...

By default, WordPress automatically generates an archive page. The URL structure is clean. In my example, the archive url is /movie/. That means that when I append the URL, it will look like this /movie/?rating=3

In order to override an existing query, we need to use the pre_get_posts action.

Open your theme's functions.php file and enter the following.

function movie_archive ( $query ) { if ( $query - > is_archive ( 'movie' ) && $query - > is_main_query ( ) && ! is_admin ( ) ) { $rating = get_query_var ( 'rating' , FALSE ) ; $director = get_query_var ( 'director_id' , FALSE ) ; $category = get_query_var ( 'movie_category_ids' , FALSE ) ; $meta_query_array = array ( 'relation' = > 'AND' ) ; $director ? array_push ( $meta_query_array , array ( 'key' = > 'director' , 'value' = > '"' . $director . '"' , 'compare' = > 'LIKE' ) ) : null ; $rating ? array_push ( $meta_query_array , array ( 'key' = > 'rating' , 'value' = > $rating , 'compare' = > '>=' ) ) : null ; $query - > set ( 'meta_query' , $meta_query_array ) ; $tax_query_array = array ( 'relation' = > 'OR' ) ; $category ? array_push ( $tax_query_array , array ( 'taxonomy' = > 'movie_category' , 'field' = > 'term_id' , 'terms' = > $category ) ) : null ; $query - > set ( 'tax_query' , $tax_query_array ) ; } } add_action ( 'pre_get_posts' , 'movie_archive' ) ;

So, what's going on with this function?

First, we make sure this function will only run on the movie archive page. We also make sure this query does not affect admin pages by adding !is_admin() to the conditional. You can update this conditional to meet your needs. Make sure to reference the pre_get_posts action API.

if ( $query - > is_archive ( 'movie' ) && $query - > is_main_query ( ) && ! is_admin ( ) )

Next, we save the query_vars passed into the URL. We use the get_query_var function to get the value of each custom query_vars passed into the URL. I set the second parameter of the function to FALSE in order to conditionally set a meta_query later in the function.

$rating = get_query_var ( 'rating' , FALSE ) ; $director = get_query_var ( 'director_id' , FALSE ) ; $category = get_query_var ( 'movie_category_ids' , FALSE ) ;

Next, we conditionally build a meta_query to search against the custom fields. What we do here is see if either the director_id or rating custom query_vars exist in the URL. If they do, we append them to the $meta_query_array array.

or exist in the URL. If they do, we append them to the array. Advanced Custom Fields allows you to easily Query relationship fields

The meta_query is not specific to Advanced Custom Fields, but this is a common pattern.

$meta_query_array = array ( 'relation' = > 'AND' ) ; $director ? array_push ( $meta_query_array , array ( 'key' = > 'director' , 'value' = > '"' . $director . '"' , 'compare' = > 'LIKE' ) ) : null ; $rating ? array_push ( $meta_query_array , array ( 'key' = > 'rating' , 'value' = > $rating , 'compare' = > '>=' ) ) : null ; $query - > set ( 'meta_query' , $meta_query_array ) ;

Following a similar pattern to step 2.3, we conditionally build a tax_query. What we do here is see if the category custom query_vars exist in the URL. If it does, we append the values to the $tax_query_array array.

exist in the URL. If it does, we append the values to the array. I chose to use a 'relation' => 'OR' to make searches more broad. This means that a user can select multiple terms, and any post assigned to those terms will appear. If we set 'relation' => 'AND' a post would only appear if it was assigned to ALL selected terms.

$tax_query_array = array ( 'relation' = > 'OR' ) ; $category ? array_push ( $tax_query_array , array ( 'taxonomy' = > 'movie_category' , 'field' = > 'term_id' , 'terms' = > $category ) ) : null ; $query - > set ( 'tax_query' , $tax_query_array ) ;

3. Add a Search Form to the Archive

Now that we have a way to dynamically affect queries, we need a way for a user to append the correct parameters to the URL. Fortunately, this is very easy to do using a simple HTML form.

Create a custom template for the archive. In my case, I created a archive-movie.php file. This is necessary since the search form will be specific to the movie post type. Add a form with a corresponding input for each custom query_vars.

< form method = " GET " action = " <?php echo get_post_type_archive_link ( 'movie' ) ; ?> " > <?php $directors = new WP_Query ( array ( 'post_type' = > 'director' , 'posts_per_page' = > - 1 ) ) ; $categories = get_terms ( array ( 'taxonomy' = > 'movie_category' , 'hide_empty' = > false , ) ) ; ?> < div > <?php if ( $directors - > have_posts ( ) ) { ?> < label for = " director_id " > Director </ label > < select name = " director_id " id = " director_id " > < option value = " " > --Any-- </ option > <?php while ( $directors - > have_posts ( ) ) { ?> <?php $directors - > the_post ( ) ; ?> < option value = " <?php echo the_ID ( ) ; ?> " <?php echo get_the_ID ( ) == get_query_var ( 'director_id' , FALSE ) ? 'selected' : null ?> `` > <?php echo the_title ( ) ; ?> </ option > <?php } ?> </ select > <?php } ?> <?php wp_reset_postdata ( ) ; ?> < label for = " rating " > Minimum Rating </ label > < select name = " rating " id = " rating " > < option value = " " > --Any-- </ option > < option value = " 1 " <?php echo get_query_var ( 'rating' , FALSE ) == 1 ? 'selected' : null ; ?> > 1 Star </ option > < option value = " 2 " <?php echo get_query_var ( 'rating' , FALSE ) == 2 ? 'selected' : null ; ?> > 2 Stars </ option > < option value = " 3 " <?php echo get_query_var ( 'rating' , FALSE ) == 3 ? 'selected' : null ; ?> > 3 Stars </ option > < option value = " 4 " <?php echo get_query_var ( 'rating' , FALSE ) == 4 ? 'selected' : null ; ?> > 4 Stars </ option > < option value = " 5 " <?php echo get_query_var ( 'rating' , FALSE ) == 5 ? 'selected' : null ; ?> > 5 Stars </ option > </ select > </ div > <?php if ( ! empty ( $categories ) ) { ?> < div > <?php foreach ( $categories as & $category ) { ?> < input type = " checkbox " id = " <?php echo $category - > name ; ?> " value = " <?php echo $category - > term_id ; ?> " name = " movie_category_ids[] " <?php echo in_array ( $category - > term_id , get_query_var ( 'movie_category_ids' , FALSE ) ) ? 'checked' : null ; ?> /> < label for = " <?php echo $category - > name ; ?> " > <?php echo $category - > name ; ?> </ label > <?php } ?> </ div > <?php } ?> < div > < button > Search </ button > < a href = " <?php echo get_post_type_archive_link ( 'movie' ) ; ?> " > Reset </ a > </ div > </ form >

At first it seems like there is a lot going on, but it's actually fairly simple.

First, I make sure to set the form method to GET and the form action to <?php echo get_post_type_archive_link('movie'); ?> . This ensures the form will update the URL parameters on the correct page. The form action is specific to this tutorial. You will need to adjust this URL on your site.

Next, we create two loops. One $directors and the other $categories . This is simply to dynamically create the options and checkboxes for the Director select list, and the Category checkboxes. This ensures that every new Category or Director added to the site will be added to the search form.



<?php $directors = new WP_Query ( array ( 'post_type' = > 'director' , 'posts_per_page' = > - 1 ) ) ; $categories = get_terms ( array ( 'taxonomy' = > 'movie_category' , 'hide_empty' = > false , ) ) ; ?>

Then we need to loop through each array and dynamically create the correct input fields. Pay special attention to the name attribute of each field. It needs to match the name of the custom query_var we created in step 1.1 The value of each input needs to be in a format the pre_get_posts function expects. For example $director ? array_push($meta_query_array, array('key' => 'director', 'value' => '"' . $director . '"', 'compare' => 'LIKE') ) : null ; is expecting $director to be the ID . I know this because of the this is how you query relationship fields. Finally, I conditionally set the selected and checked values of each input. This is to ensure the values persist once a search is made.



I added a [] to movie_category_ids to allow for more than one option to be selected. This will turn the values for the movie_category_id key into an array. This is needed for the $tax_query_array we created in step 2.4

<?php if ( $directors - > have_posts ( ) ) { ?> < label for = " director_id " > Director </ label > < select name = " director_id " id = " director_id " > < option value = " " > --Any-- </ option > <?php while ( $directors - > have_posts ( ) ) { ?> <?php $directors - > the_post ( ) ; ?> < option value = " <?php echo the_ID ( ) ; ?> " <?php echo get_the_ID ( ) == get_query_var ( 'director_id' , FALSE ) ? 'selected' : null ?> `` > <?php echo the_title ( ) ; ?> </ option > <?php } ?> </ select > <?php } ?> <?php wp_reset_postdata ( ) ; ?> ... <?php if ( ! empty ( $categories ) ) { ?> < div > <?php foreach ( $categories as & $category ) { ?> < input type = " checkbox " id = " <?php echo $category - > name ; ?> " value = " <?php echo $category - > term_id ; ?> " name = " movie_category_ids[] " <?php echo in_array ( $category - > term_id , get_query_var ( 'movie_category_ids' , FALSE ) ) ? 'checked' : null ; ?> /> < label for = " <?php echo $category - > name ; ?> " > <?php echo $category - > name ; ?> </ label > <?php } ?> </ div > <?php } ?>

Finally, I manually add a select for the movie rating . Since I know there are only 5 possible values for this custom field, I wrote everything out instead of making a loop. I conditionally set the selected value to ensure the values persist once a search is made.



< label for = " rating " > Minimum Rating </ label > < select name = " rating " id = " rating " > < option value = " " > --Any-- </ option > < option value = " 1 " <?php echo get_query_var ( 'rating' , FALSE ) == 1 ? 'selected' : null ; ?> > 1 Star </ option > < option value = " 2 " <?php echo get_query_var ( 'rating' , FALSE ) == 2 ? 'selected' : null ; ?> > 2 Stars </ option > < option value = " 3 " <?php echo get_query_var ( 'rating' , FALSE ) == 3 ? 'selected' : null ; ?> > 3 Stars </ option > < option value = " 4 " <?php echo get_query_var ( 'rating' , FALSE ) == 4 ? 'selected' : null ; ?> > 4 Stars </ option > < option value = " 5 " <?php echo get_query_var ( 'rating' , FALSE ) == 5 ? 'selected' : null ; ?> > 5 Stars </ option > </ select >

Go ahead and make a search. You should see the custom query_vars appended to the URL.

It's important to note that I updated the markup for my loop to be in a table format.

Pro Tip: Add <?php global $wp_query; print_r($wp_query); ?> to the page in order to inspect a the actual query. This is helpful when debugging.

If you want to add a custom search to your WordPress site, follow this pattern.