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 a freelance WordPress developer and a contributing developer to the Genesis framework. For the past 14 years he has worked with attorneys, publishers, corporations, and non-profits, building custom websites tailored to their needs and goals.

Ready to upgrade your website?

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

Let's Talk

Reader Interactions

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. 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.

  3. 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.

Leave A Reply