WordPress custom fields allow you to store additional information about posts, pages, and custom post types. You can use this information for more than displaying metadata: custom fields can also help you build searchable directories, product catalogs, vendor listings, and other filterable content.
For example, imagine you manage a website with a custom post type called vendor. Each vendor has custom fields for country, state, ZIP code, and age. You want visitors to find vendors by selecting a location, entering an age range, and choosing one or more categories.
You can build this functionality with WordPress's WP_Query class and its meta_query parameter. The tax_query parameter handles category filtering, while PHP processes and validates the submitted search values.
In this guide, you will learn how to create a custom search page, filter posts using multiple custom fields, handle numeric ranges, and display matching results with a reusable WordPress page template.
What Are WordPress Custom Fields?
Custom fields, also called post metadata, store additional information associated with a WordPress post. Each field has a metadata key and a corresponding value.
For example, a vendor listing might contain these custom fields:
| Custom field key | Example value | Purpose |
|---|---|---|
country | Pakistan | Filter vendors by country. |
state | Punjab | Filter vendors by state or region. |
zip | 46000 | Find vendors by ZIP or postal code. |
age | 35 | Filter vendors by a numeric age value. |
You can create custom fields using WordPress's built-in custom fields interface, custom PHP code, or a plugin such as Advanced Custom Fields (ACF). The field names used in your search query must match the metadata keys saved in the database.
How Does WordPress Search Filtering Work?
A custom-field search form collects information from visitors and sends it to WordPress. PHP validates the submitted values, builds query arguments, and passes them to WP_Query.
Three WordPress features are particularly useful:
WP_Query: retrieves posts that match the specified conditions.meta_query: filters posts by custom-field values, including exact matches and numeric comparisons.tax_query: filters posts by categories or other registered taxonomies.
When multiple conditions are combined with an AND relationship, a post must satisfy all the specified conditions to appear in the results. Conditions that visitors leave empty are excluded from the query.
For official documentation, see the WordPress WP_Query reference and the WP_Meta_Query reference.
Step 1: Prepare Your Custom Post Type and Fields
This example uses a custom post type named vendor and four metadata keys: country, state, zip, and age.
Make sure that:
- The
vendorpost type is registered on your website. - Vendor entries have the custom fields used by the search form.
- The
agefield contains numeric values if you want to filter it using minimum and maximum ages. - The vendor post type supports the category taxonomy if you want to filter vendors using WordPress categories.
If you register your own custom post type, you can associate the built-in category taxonomy with it through the taxonomies argument in register_post_type(). If the post type is already registered by your theme or plugin, configure the taxonomy association in that registration rather than registering the same post type a second time.
Step 2: Create the Custom Search Page Template
Create a PHP template file named page-custom-search.php inside your active theme or child theme directory. This example provides a search form and displays up to 20 matching vendors per page.
Important: This is a complete page-template example. It assumes the vendor post type and its custom fields already exist. The category IDs in the form are examples; replace them with category IDs from your own WordPress installation.
<?php
/**
* Template Name: Custom Vendor Search
*/
get_header();
// Read and sanitize the submitted search values.
$text_fields = array( 'country', 'state', 'zip' );
$values = array();
foreach ( $text_fields as $field ) {
$values[ $field ] = isset( $_GET[ $field ] )
? sanitize_text_field( wp_unslash( $_GET[ $field ] ) )
: '';
}
$min_age = ( isset( $_GET['min_age'] ) && is_numeric( $_GET['min_age'] ) )
? (int) $_GET['min_age']
: '';
$max_age = ( isset( $_GET['max_age'] ) && is_numeric( $_GET['max_age'] ) )
? (int) $_GET['max_age']
: '';
// Ignore the range if the minimum is greater than the maximum.
if ( '' !== $min_age && '' !== $max_age && $min_age > $max_age ) {
$min_age = '';
$max_age = '';
}
$categories = ( isset( $_GET['category'] ) && is_array( $_GET['category'] ) )
? array_map( 'absint', wp_unslash( $_GET['category'] ) )
: array();
// Build the meta query from the filters that were filled in.
$meta_query = array( 'relation' => 'AND' );
foreach ( $text_fields as $field ) {
if ( '' !== $values[ $field ] ) {
$meta_query[] = array(
'key' => $field,
'value' => $values[ $field ],
'compare' => '=',
'type' => 'CHAR',
);
}
}
if ( '' !== $min_age ) {
$meta_query[] = array(
'key' => 'age',
'value' => $min_age,
'compare' => '>=',
'type' => 'NUMERIC',
);
}
if ( '' !== $max_age ) {
$meta_query[] = array(
'key' => 'age',
'value' => $max_age,
'compare' => '<=',
'type' => 'NUMERIC',
);
}
$paged = max( 1, get_query_var( 'paged' ) );
$args = array(
'post_type' => 'vendor',
'post_status' => 'publish',
'posts_per_page' => 20,
'paged' => $paged,
'orderby' => 'title',
'order' => 'ASC',
);
if ( count( $meta_query ) > 1 ) {
$args['meta_query'] = $meta_query;
}
// Category filter: match vendors in any of the selected categories.
if ( ! empty( $categories ) ) {
$args['tax_query'] = array(
array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $categories,
'operator' => 'IN',
),
);
}
$query = new WP_Query( $args );
?>
<div id="primary">
<div id="content" role="main">
<?php if ( $query->have_posts() ) : ?>
<?php while ( $query->have_posts() ) : $query->the_post(); ?>
<div class="post">
<a href="<?php echo esc_url( get_permalink() ); ?>">
<?php echo esc_html( get_the_title() ); ?>
</a>
</div>
<?php endwhile; ?>
<?php
echo wp_kses_post(
paginate_links( array(
'total' => $query->max_num_pages,
'current' => $paged,
'add_args' => map_deep( wp_unslash( $_GET ), 'sanitize_text_field' ),
) )
);
?>
<?php else : ?>
<p>No vendors match your search.</p>
<?php endif; ?>
<?php wp_reset_postdata(); ?>
</div><!-- #content -->
</div><!-- #primary -->
<div id="secondary" class="widget-area" role="complementary">
<form method="get">
<label for="country">Country:</label>
<input type="text" id="country" name="country" value="<?php echo esc_attr( $values['country'] ); ?>" />
<label for="state">State:</label>
<input type="text" id="state" name="state" value="<?php echo esc_attr( $values['state'] ); ?>" />
<label for="zip">Zip:</label>
<input type="text" id="zip" name="zip" value="<?php echo esc_attr( $values['zip'] ); ?>" />
<label for="min_age">Min Age:</label>
<input type="number" id="min_age" name="min_age" value="<?php echo esc_attr( $min_age ); ?>" />
<label for="max_age">Max Age:</label>
<input type="number" id="max_age" name="max_age" value="<?php echo esc_attr( $max_age ); ?>" />
<p>Category:</p>
<?php
// Example category IDs: replace with IDs from your own site.
$category_options = array(
5 => 'Category 1',
6 => 'Category 2',
7 => 'Category 3',
8 => 'Category 4',
);
foreach ( $category_options as $id => $label ) :
?>
<label>
<input type="checkbox" name="category[]" value="<?php echo esc_attr( $id ); ?>"
<?php checked( in_array( $id, $categories, true ) ); ?> />
<?php echo esc_html( $label ); ?>
</label><br />
<?php endforeach; ?>
<input type="submit" value="Search" />
</form>
</div>
<?php get_footer(); ?>
Step 3: Associate Categories With the Vendor Post Type
If your existing vendor post type does not support the built-in WordPress categories, the category checkboxes will not filter vendors correctly until you associate that taxonomy with the post type.
If you control the code that registers the post type, include category in its taxonomy registration arguments. For example:
register_post_type(
'vendor',
array(
'label' => 'Vendors',
'public' => true,
'show_in_rest' => true,
'supports' => array(
'title',
'editor',
'excerpt',
'thumbnail',
),
'taxonomies' => array( 'category' ),
)
);
This is an illustration of the relevant registration settings, not a replacement for an existing post type registration. Merge the taxonomy setting into your existing registration and keep its other required arguments.
Alternatively, if the post type is already registered and you cannot change its registration directly, you can associate the existing category taxonomy on the init hook:
function example_register_vendor_categories() {
register_taxonomy_for_object_type( 'category', 'vendor' );
}
add_action( 'init', 'example_register_vendor_categories' );
Use only the approach appropriate to your setup. If your website uses a custom taxonomy instead of standard WordPress categories, replace category in the query and register the matching taxonomy for the vendor post type.
Official references: register_post_type() and register_taxonomy_for_object_type().
Step 4: Add the Page Template in WordPress
After saving the PHP file:
- Upload
page-custom-search.phpto your active theme or child theme directory. - Open the WordPress dashboard and create a new page, such as “Vendor Search.”
- Select the Custom Vendor Search template in the page editor if your editor exposes the template selector.
- Publish the page and open its public URL.
- Test each filter individually, then test multiple filters together.
If the template does not appear, confirm that the file is in the active theme directory, the template header is correct, and your theme supports selectable page templates.
Step 5: Understand the Meta Query Conditions
The meta_query array defines the custom-field conditions used to retrieve vendors.
Exact match for text fields
array(
'key' => 'country',
'value' => 'Pakistan',
'compare' => '=',
'type' => 'CHAR',
)
This condition matches posts whose country metadata equals “Pakistan.” The example uses exact matching, so a different spelling or value will not match. Standard WordPress database collations may affect case sensitivity.
Minimum and maximum numeric values
array(
'key' => 'age',
'value' => 25,
'compare' => '>=',
'type' => 'NUMERIC',
)
This condition matches vendors with an age of 25 or higher. The NUMERIC type is important because age is a number, not ordinary text. The maximum-age condition uses <= to set the upper limit.
Combine multiple conditions
When a visitor selects Pakistan, Punjab, and a minimum age of 25, the example combines those active conditions with AND. The results must match the selected country, selected state, and minimum age.
The category filter is handled separately through tax_query. With the IN operator, a vendor can match any one of the selected categories. The metadata conditions still need to match as well.
Step 6: Improve Security and Performance
Search forms accept input from visitors, so it is important to validate incoming values and escape output correctly.
- Sanitize inputs: The example uses
wp_unslash()andsanitize_text_field()for text fields and validates numeric values before using them. - Escape output: Use
esc_attr()for input values,esc_html()for plain text, andesc_url()for URLs. - Use WordPress query APIs:
WP_Queryand its query parameters are preferable to building SQL statements manually for this use case. - Limit result counts: Returning 20 results per page avoids loading every matching vendor at once and supports pagination.
- Validate metadata: Store ages consistently as numeric values. If existing data contains words or inconsistent formats, clean those records before relying on numeric comparisons.
- Check category IDs: Replace the example IDs with actual category term IDs and ensure the selected taxonomy is associated with the custom post type.
For further details, see the official WordPress documentation for sanitize_text_field() and esc_attr().
Common Problems and How to Fix Them
1. The search returns no results
Check whether the metadata keys in your query exactly match the keys stored for the vendor posts. Confirm that the posts are published and that the submitted field values match the saved values. Temporarily test one condition at a time.
2. Category filtering does not work
Confirm that the category taxonomy is associated with the vendor post type. Verify the category IDs and ensure the vendors have actually been assigned to those categories. If you use a custom taxonomy, update the query accordingly.
3. Minimum and maximum age filters return unexpected results
Use 'type' => 'NUMERIC' for age comparisons and store ages as numbers. Also verify that the minimum age is not greater than the maximum age. The supplied template checks this range before running the query.
4. The form loses its selected values
The template repopulates the text and number fields and retains selected category checkboxes using the submitted query parameters. If you customize the form, preserve this behavior so visitors can refine a search without entering everything again.
5. The page becomes slow with many results
Avoid loading an unlimited number of posts on every request. Use a reasonable posts_per_page value, keep pagination enabled, and review your hosting resources and metadata structure if the query remains slow.
Frequently Asked Questions
1. Can WordPress search posts by custom fields?
Yes. You can use WP_Query with the meta_query parameter to retrieve posts based on custom-field values. You can filter by exact matches, numeric comparisons, and other supported comparison operators.
2. What is the difference between meta_query and tax_query?
meta_query filters posts by metadata stored in custom fields, such as country, state, price, or age. tax_query filters posts by taxonomy terms, such as categories, tags, or custom taxonomies.
3. Can I filter WordPress posts by multiple custom fields at once?
Yes. Add multiple conditions to the meta_query array. Use the AND relationship when all active conditions must match, or use OR when any matching condition is sufficient.
4. How can I search a numeric custom field between two values?
Add a minimum condition using >= and a maximum condition using <=. Set the comparison type to NUMERIC and ensure the saved metadata contains valid numeric values.
5. Does WP_Query work with custom post types?
Yes. Set the post_type argument to the registered custom post type's name, such as vendor. The metadata keys and taxonomy names must correspond to the data associated with that post type.
6. Do I need a plugin to build custom-field search filters?
No. You can build the functionality with a PHP page template and WordPress APIs. Plugins can simplify the creation and management of custom fields or provide ready-made filtering interfaces, but they are not required for this example.
7. Can I use Advanced Custom Fields (ACF) with WP_Query?
Yes. ACF field values are generally stored as WordPress post metadata, so you can query them using the corresponding metadata key. Confirm the actual saved key and value format, particularly for relationship, serialized, date, and other complex field types.
8. Is it safe to use GET parameters in a WordPress search form?
GET parameters are suitable for read-only searches, but you should validate and sanitize incoming values and escape them before displaying them in HTML. For actions that change data, additional security measures such as capability checks and nonce verification are necessary.
Conclusion
WordPress custom-field search filters make it possible to create flexible directories and searchable listings without writing custom SQL queries. By combining WP_Query, meta_query, and tax_query, you can filter vendors by location, numeric age ranges, and categories.
The key is to use the correct metadata keys, validate visitor input, handle numeric values with the right comparison type, and ensure the relevant taxonomies are registered for your custom post type. Start with the working page-template example above and adapt the fields, categories, and result layout to your website's requirements.
Last updated: