Advanced Search using SearchWP and FacetWP

I recently collaborated with Emergent Order to design and build a new website for Mountain States Legal Foundation, a non-profit providing legal services.

One of the key goals for the redesign was improved site search. Users need to easily find information about the cases they care about.

We leveraged SearchWP for relevant search results, indexing of documents, and generating related content listings throughout the site. We used FacetWP for advanced filtering on the search results page.

Here are some tips and code snippets I used to implement the advanced site search.

SearchWP

We use SearchWP on every site because it provides two valuable features:

  1. It expands the data used for indexing.
    While the standard WordPress search only looks at the title and content fields, SearchWP lets you index metadata, taxonomies, and more. You can also control which content types can be included in the index and the weighting of different factors when determining relevance.
  2. It delivers relevant search results.
    The standard WordPress search finds any post with a keyword match in the title or content and returns those results with the most recent first. The most recent article often isn’t the most relevant.

Include documents in search

When setting up SearchWP, we added “Media” as one of the post types to index, and limited the file type to “All Documents”.

If you check “Transfer weight to parent”, the post or page containing the document will be displayed instead of the document itself, which is a really nice feature.

In this particular case we wanted to expose the documents themselves. You’ll need to ensure your theme displays ‘attachment’ posts correctly in the loop.

Here’s the archive partial we used for documents in search results. We’re also including the source page as a clickable subtitle in the results.

<?php
/**
 * Attachment archive partial
 *
 * @package      MountainStatesLegal2019
 * @author       Bill Erickson
 * @since        1.0.0
 * @license      GPL-2.0+
**/
$subtitle = $subtitle_url = false;
$parent = wp_get_post_parent_id( get_the_ID() );
if( !empty( $parent ) && 'case' == get_post_type( $parent ) ) {
	$subtitle = get_the_title( $parent );
	$subtitle_url = get_permalink( $parent );
}
echo '<article class="post-summary type-document">';
	if( !empty( $subtitle ) )
		echo '<h5 class="small"><a href="' . esc_url_raw( $subtitle_url ) . '">' . $subtitle . '</a></h5>';
	echo '<h2 class="entry-title"><a href="' . get_permalink() . '">' . get_the_title() . '</a></h2>';
	echo '<div class="entry-meta"><span class="entry-type">Document</span></div>';
echo '</article>';

Search Metrics

We wanted to provide editors with insight into how users are interacting with the site search. SearchWP Metrics is perfect for this. It lets you monitor the amount of site searches, popular search terms, average ranking of clicked result, and more.

The “No Result Searches” can be a tool to determine new content that needs to be written, updates to existing content so it better matches common search terms, or search synonyms you can add to direct those searches to ones with results.

If you enable Live Search results as the user types, use this code to track metrics on those as well.

Customize Metabox

When you have the SearchWP Related addon installed, it adds a “SearchWP Related Content” metabox to all post types.

The code below makes the following changes:

  • Limits it to just the post, case, and press_release post type
  • Sets the priority to ‘low’ so it doesn’t appear above important metaboxes
  • Changes the title to “Related News”
/**
 * SearchWP Related on specific post types
 * @link https://www.billerickson.net/code/searchwp-related-on-specific-post-types/
 */
function ea_swp_related_excluded_post_types( $exclude = array() ) {
	$allowed = array( 'post', 'case', 'press_release' );
	$all = array_keys( get_post_types() );
	return array_diff( $all, $allowed );
}
add_filter( 'searchwp_related_excluded_post_types', 'ea_swp_related_excluded_post_types' );

/**
 * SearchWP Related, metabox priority
 *
 */
function ea_swp_metabox_priority( $priority ) {
	return 'low';
}
add_filter( 'searchwp_related_meta_box_priority', 'ea_swp_metabox_priority' );

/**
 * SearchWP Related, metabox title
 *
 */
function ea_swp_metabox_title( $title ) {
	return 'Related News';
}
add_filter( 'searchwp_related_meta_box_title', 'ea_swp_metabox_title' );

FacetWP

FacetWP is the simplest tool for filtering and sorting content in WordPress. We use it often for filtering results on archive pages (example), but it can also be used for search results.

Add labels to FacetWP filters

When you setup your FacetWP filters (known as “facets”), you specify a label for each. FacetWP doesn’t display these labels on the frontend, but the JavaScript code below adds the label above the facet.

jQuery(function($){

	// FacetWP Labels
	// @link https://www.billerickson.net/advanced-search-with-searchwp-and-facetwp/#facetwp-labels
	$(document).on('facetwp-loaded', function() {
		$('.facetwp-facet').each(function() {
			var $facet = $(this);
			var facet_name = $facet.attr('data-name');
			var facet_label = FWP.settings.labels[facet_name];

			if ($facet.closest('.facet-wrap').length < 1) {
				$facet.wrap('<div class="facet-wrap"></div>');
				$facet.before('<h5 class="facet-label">' + facet_label + '</h5>');
			}
		});
	});
});

Filter by year

When you create a facet, you can select from many different types and data sources. I used the “Slider” type and “Post Date” as the data source.

The Post Date source uses a UNIX timestamp, so I had to customize the indexer to use the year for this facet.

My facet name was filter_by_year, so change that in the code below to match your facet name. Once you’ve added this code, click “Re-index” to update the data.

/**
 * FacetWP, Filter by Year
 */
add_filter( 'facetwp_index_row', function( $params, $class ) {
	if ( 'filter_by_year' == $params['facet_name'] ) {
		$raw_value = $params['facet_value'];
		$params['facet_value'] = date( 'Y', strtotime( $raw_value ) );
		$params['facet_display_value'] = $params['facet_value'];
	}
	return $params;
}, 10, 2 );

Hide empty facets

Some of the filters we’ve added only apply to certain content types. For instance, the “Open” and “Closed” status at the bottom is a taxonomy on the ‘case’ content type.

FacetWP displays the filter regardless of whether there’s anything to filter. The code below hides the facet if it’s empty. Note: this assumes each facet is inside of its own .widget container.

jQuery(function($){

  // Hide empty facets
  // @link https://www.billerickson.net/advanced-search-with-searchwp-and-facetwp/#facetwp-hide-empty
  $(document).on('facetwp-loaded', function() {
    $.each(FWP.settings.num_choices, function(key, val) {
      var $parent = $('.facetwp-facet-' + key).closest('.widget');
      (0 === val) ? $parent.hide() : $parent.show();
    });
  });
});

If a user submitted an empty search form, WordPress listed all the site’s content and the filters appeared in the sidebar. Selecting any filter caused the 404 page to load as a listing in the content area.

I added the following to functions.php to redirect empty search queries to a Search page that contained a search form and no FacetWP filters.

/**
 * Empty search redirect
 * https://www.billerickson.net/advanced-search-with-searchwp-and-facetwp/#empty-search
 */
function ea_empty_search_redirect( $query ) {
	if( $query->is_main_query() && ! is_admin() && $query->is_search() && empty( $query->query['s'] ) ) {
		wp_redirect( home_url( 'search' ) );
		exit;
	}
}
add_action( 'pre_get_posts', 'ea_empty_search_redirect' );

I’m using pre_get_posts instead of template_redirect so I can target only the main query. When I used template_redirect I ran into an issue with FacetWP filters not working.

Customize FacetWP Pagination

FacetWP uses JavaScript to refresh results so you can’t use the standard WordPress pagination. You can either use the shortcode [facetwp pager="true"] or the function facetwp_display( 'pager' );.

It’s a good idea to add pagination scrolling so users are taken to the top of the results after navigating to a new page.

You can customize the markup of the pagination function using the facetwp_pager_html filter. Here’s what I used on the site referenced above to match our standard pagination used elsewhere:

/**
 * FacetWP pagination
 *
 */
add_filter( 'facetwp_pager_html', function( $output, $params ) {
    $output = '';
    $page = $params['page'];
    $total_pages = $params['total_pages'];

    if( $total_pages === 1 )
        return;

    if ( $page > 1 ) {
        $output .= '<a class="facetwp-page prev" data-page="' . ($page - 1) . '">Previous</a>';
    }

    $show = 5;
    $start = min( 1, $page - 2 );
    $end = min( $page + 2, $total_pages );
    for( $i = $start; $i <= $end; $i++ ) {
        $class = ea_class( 'facetwp-page', 'active', $i === $page );
        $output .= '<a class="' . $class . '" data-page="' . $i . '">' . $i . '</a>';
    }

    if( $end < $total_pages ) {
        $output .= '<span class="page-numbers dots">…</span>';
        $output .= '<a class="facetwp-page" data-page="' . $total_pages . '">' . $total_pages . '</a>';
    }


    if ( $page < $total_pages && $total_pages > 1 ) {
        $output .= '<a class="facetwp-page next" data-page="' . ($page + 1) . '">Next</a>';
    }

    return $output;
}, 10, 2 );

Include attachments in search results

Even though I had set SearchWP to index attachments, FacetWP wasn’t returning them in the search results. The code below tells FacetWP to include attachments in its query:

/**
 * FacetWP, Index attachments
 *
 */
add_filter( 'facetwp_indexer_query_args', function( $args ) {
    $args['post_status'] = array( 'publish', 'inherit' );
    return $args;
});
/**
 * FacetWP Query Args
 *
 */
add_filter( 'facetwp_query_args', function( $args ) {
	$args['post_status'] = 'any';
	return $args;
}, 10 );

List total number of found results

You can use facetwp_display( 'counts' ); or [facetwp counts="true"] to display the number of posts matching the current faceted search.

You can customize the output using the facetwp_result_count filter.

/**
 * FacetWP, result count
 *
 */
add_filter( 'facetwp_result_count', function( $output, $params ) {
    $output = 'Showing ' . $params['lower'] . '-' . $params['upper'] . ' of ' . $params['total'] . ' results';
    return $output;
}, 10, 2 );

Sort results

You can display a dropdown to reorder the results using facetwp_display( 'sort' ); or [facetwp sort="true"].

The placeholder when nothing has been selected is “Sort by”, which I changed to “Relevance” since SearchWP is delivering relevancy-based results.

/**
 * FacetWP, change sort label
 *
 */
add_filter( 'facetwp_sort_options', function( $options, $params ) {
    $options['default']['label'] = 'Relevance';
    return $options;
}, 10, 2 );

Use singular name in post type filter

We have a “Filter by Type” facet for filtering by post type. This uses the post type name which is typically plural, but the client wanted the singular form used in the filter. Also, we wanted to change “Media” to “Document” since that’s the only type of media we’re indexing.

You can use the facetwp_index_row filter, and then check the facet name before making changes. In my case, our facet name is filter_by_type.

/**
 * FacetWP, singular name for post types
 *
 */
add_filter( 'facetwp_index_row', function( $params, $class ) {
    if ( 'filter_by_type' == $params['facet_name'] ) {
		$labels = array(
			'Pages'           => 'Page',
			'Cases'           => 'Case',
			'News'            => 'News Post',
			'Press Releases'  => 'Press Release',
			'Media'           => 'Document',
		);
		if( array_key_exists( $params['facet_display_value'], $labels ) )
			$params['facet_display_value'] = $labels[ $params['facet_display_value'] ];
    }
    return $params;
}, 10, 2 );

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson

Comments

  1. John Edward says

    I don’t think this scales. Should have setup an elasticsearch server. If this only a product placement I get it. Else…

    With thousands of posts and online users querying MySQL the server load will go nuts.

    • Bill Erickson says

      The vast majority of WordPress websites aren’t large enough to run into scaling issues. SearchWP was designed to work on sites with up to 20,000 posts. Once you get beyond that, you’re correct that elastic search is a better option. But even the native WordPress search would have issues at that scale – it’s not unique to this plugin.

  2. Hans says

    Thanks Bill, I didn’t yet know facetwp and will try it. It seems to offer some nice features too.

  3. Joe G. says

    I’m using both FacetWp and SearchWp on a large Woocommerce store and stumbled on your blog. Are you sure that the search results are returning through facetWP and maintianing their sort by relevance status? Mine is returning the relevant results but reverting to sorted by product title. Any tips from your experience?

    • Bill Erickson says

      You might try contacting FacetWP support for help. It took a bit of troubleshooting for us to get both plugins to play well together. They can look at the debugging output from FacetWP to help you fix this.

  4. Fabio Venni says

    Is there an obvious way of getting the applied filters, search string and sorting options to display as a title string? something like “12 results matching “your search” sorted by date and filtered by type”

    • Bill Erickson says

      I’m not sure. You might try reaching out to FacetWP support to see if they have an answer for that. Please share if you find anything out.

  5. Thorsten says

    Hi, I took a closer look at the States Legal Foundation website and I’m asking myself how you achieved it to get FacetWP working with the search results on the search results page? I tried it, but … no chance.

    • Bill Erickson says

      FacetWP lets you filter the results of the current query on the page, so it should work with search results out of the box.

      Every customization I made is listed here so if I ever need to build something like it again, I can reference this post.

      If it’s not working for you, I recommend reaching out to FacetWP support. They are excellent and quick to help troubleshoot your specific issues.

      • Thorsten says

        Thanks for your answer. Yes, I thought it would work out of the box, too, but for me it won’t work. Yes maybe I’ll reach out the support …

  6. John Giftakis says

    Hi, great article. Is there also a way to put a checkbox in each filtered result in order to bundle the selected and use them for other purposes?
    i.e. let’s say that each CPT searched is a Question and after I used FacetWP to filter them I want to select some of those to make a composite item name Quiz and export it or save the array selected.
    What do I need to know to achieve that?

  7. Laurel says

    Greetings Bill! I was looking at your article and it was very helpful. One question I had while looking at your FacetWP Custom Pagination, could you explain ea_class() used around line 21?

    • Bill Erickson says

      It’s a helper function I have in my starter theme (see here). It lets you add a second optional class if some condition is true.

      Ex: echo ea_class( 'home-page', 'active', is_front_page() ); is equivalent to:

      $classes = [ 'home-page' ];
      if( is_front_page() )
      	$classes[] = 'active';
      echo join( ' ', $classes );
      
  8. Kaz says

    Bill, would you care to share how to build a custom search page with these two tools together? I started with SearchWP and used their documentation to build a template page, but then when came time to add the Facet stuff I didn’t really understand how to combine them as the query for SearchWP is custom like so:

    “`
    // Perform the search.
    $search_results = [];
    $search_pagination = ”;
    if (!empty($search_query) && class_exists(‘\\SearchWP\\Query’)) {
    $searchwp_query = new \SearchWP\Query($search_query, [
    ‘engine’ => ‘custom’, // The Engine name.
    ‘fields’ => ‘all’, // Load proper native objects of each result.
    ‘page’ => $search_page,
    ]);

    $search_results = $searchwp_query->get_results();

    $search_pagination = paginate_links(array(
    ‘format’ => ‘?swppg=%#%’,
    ‘current’ => $search_page,
    ‘total’ => $searchwp_query->max_num_pages,
    ));
    }

    “`

    how do you tell one to use the other and vice-versa?

    • Bill Erickson says

      I think SearchWP was modifying the main search results query – it wasn’t a custom query. And since we were using the main query on the page, FacetWP just automatically worked with it.

      You could try adding 'facetwp' => true to the custom SearchWP query, but my guess is the SearchWP one isn’t a wrapper for WP_Query, so that won’t work.

      • Kaz says

        thanks for the reply and sorry for the formatting, can’t figure out how to edit that. understood, gonna try a different way. thanks!