Skip to main content

Search & Filtering

Muddy implements a dual search architecture combining FacetWP v4.3.3 for faceted filtering and Algolia v2.8.1 for instant search, providing customers with powerful product discovery tools.

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│ Customer Search Query │
└────────────┬────────────────────────────────┬───────────────┘
│ │
│ Instant Search │ Faceted Filtering
│ (Text queries) │ (Attributes, categories)
│ │
┌────────────▼───────────────┐ ┌──────────▼────────────────┐
│ Algolia Search │ │ FacetWP Engine │
│ - Full-text search │ │ - Price ranges │
│ - Typo tolerance │ │ - Categories │
│ - Instant results │ │ - Product attributes │
│ - Autocomplete │ │ - Custom taxonomies │
└────────────┬───────────────┘ └──────────┬────────────────┘
│ │
│ REST API │ WordPress Query
│ │
┌────────────▼────────────────────────────────▼───────────────┐
│ WordPress Product Database │
└─────────────────────────────────────────────────────────────┘

FacetWP v4.3.3

FacetWP provides faceted filtering for BigCommerce products with dynamic filter updates and clean URLs.

Installation & Configuration

wp plugin install facetwp --activate

License Activation:

Navigate to Settings → FacetWP → Settings and enter your license key.

Facet Configuration

Price Range Facet

// Facet settings (configured via admin)
[
'name' => 'price_range',
'label' => 'Price',
'type' => 'slider',
'source' => 'cf/bigcommerce_price',
'prefix' => '$',
'format' => '0,0.00',
'min' => 0,
'max' => 1000,
'step' => 10
]

Category Facet

[
'name' => 'product_category',
'label' => 'Category',
'type' => 'checkboxes',
'source' => 'tax/bigcommerce_category',
'parent_term' => 0,
'orderby' => 'count',
'order' => 'DESC',
'show_expanded' => 'yes'
]

Product Type Facet

[
'name' => 'product_type',
'label' => 'Product Type',
'type' => 'radio',
'source' => 'tax/product_type',
'orderby' => 'term_order',
'order' => 'ASC'
]

Brand Facet

[
'name' => 'brand',
'label' => 'Brand',
'type' => 'checkboxes',
'source' => 'tax/bigcommerce_brand',
'show_expanded' => 'no'
]

Size/Capacity Facet

[
'name' => 'capacity',
'label' => 'Weight Capacity',
'type' => 'checkboxes',
'source' => 'cf/product_capacity',
'orderby' => 'meta_value_num',
'order' => 'ASC'
]

In Stock Facet

[
'name' => 'availability',
'label' => 'Availability',
'type' => 'radio',
'source' => 'cf/bigcommerce_inventory',
'choices' => [
'in_stock' => 'In Stock',
'out_of_stock' => 'Out of Stock'
]
]

Template Integration

Products Archive Page

// archive-bigcommerce_product.php

get_header();
?>

<div class="product-archive">
<div class="container">
<div class="row">
<!-- Sidebar with facets -->
<aside class="col-lg-3 sidebar">
<div class="facets-wrapper">
<h3>Filter Products</h3>

<!-- Category facet -->
<div class="facet-group">
<?php echo facetwp_display( 'facet', 'product_category' ); ?>
</div>

<!-- Price range facet -->
<div class="facet-group">
<?php echo facetwp_display( 'facet', 'price_range' ); ?>
</div>

<!-- Brand facet -->
<div class="facet-group">
<?php echo facetwp_display( 'facet', 'brand' ); ?>
</div>

<!-- Availability facet -->
<div class="facet-group">
<?php echo facetwp_display( 'facet', 'availability' ); ?>
</div>

<!-- Reset button -->
<button class="facetwp-reset" onclick="FWP.reset()">Clear Filters</button>
</div>
</aside>

<!-- Products grid -->
<main class="col-lg-9">
<div class="archive-header">
<h1><?php echo get_the_archive_title(); ?></h1>

<!-- Result count -->
<div class="result-count">
<?php echo facetwp_display( 'counts' ); ?>
</div>

<!-- Sort dropdown -->
<div class="sort-dropdown">
<?php echo facetwp_display( 'sort' ); ?>
</div>
</div>

<!-- Products grid (FacetWP template) -->
<div class="facetwp-template">
<?php
if ( have_posts() ) :
echo '<div class="products-grid grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">';

while ( have_posts() ) : the_post();
get_template_part( 'template-parts/content', 'product-card' );
endwhile;

echo '</div>';
else :
echo '<p>No products found matching your filters.</p>';
endif;
?>
</div>

<!-- Pagination -->
<div class="facetwp-pagination">
<?php echo facetwp_display( 'pager' ); ?>
</div>
</main>
</div>
</div>
</div>

<?php
get_footer();

Sort Options

// Configure sort options
add_filter( 'facetwp_sort_options', function( $options, $params ) {
$options = [
'default' => [
'label' => 'Default',
'query_args' => [
'orderby' => 'menu_order',
'order' => 'ASC'
]
],
'price_asc' => [
'label' => 'Price: Low to High',
'query_args' => [
'orderby' => 'meta_value_num',
'meta_key' => 'bigcommerce_price',
'order' => 'ASC'
]
],
'price_desc' => [
'label' => 'Price: High to Low',
'query_args' => [
'orderby' => 'meta_value_num',
'meta_key' => 'bigcommerce_price',
'order' => 'DESC'
]
],
'name_asc' => [
'label' => 'Name: A to Z',
'query_args' => [
'orderby' => 'title',
'order' => 'ASC'
]
],
'date_desc' => [
'label' => 'Newest First',
'query_args' => [
'orderby' => 'date',
'order' => 'DESC'
]
]
];

return $options;
}, 10, 2 );

Custom Indexing

// Index custom product attributes
add_filter( 'facetwp_indexer_post_facet', function( $params, $class ) {
if ( 'bigcommerce_product' === $params['post_id'] ) {
$product_id = $params['post_id'];

// Index custom attributes
$capacity = get_post_meta( $product_id, 'product_capacity', true );
if ( ! empty( $capacity ) ) {
$params['facet_value'] = $capacity;
$params['facet_display_value'] = $capacity . ' lbs';
}
}

return $params;
}, 10, 2 );

Performance Optimization

// Cache facet counts
add_filter( 'facetwp_cache_lifetime', function( $seconds ) {
return 3600; // 1 hour
} );

// Preload facet data
add_action( 'wp_footer', function() {
if ( is_post_type_archive( 'bigcommerce_product' ) ) {
?>
<script>
// Preload facets on page load
document.addEventListener('DOMContentLoaded', () => {
if (typeof FWP !== 'undefined') {
FWP.preload_data = <?php echo json_encode( FWP()->facet->preload() ); ?>;
}
});
</script>
<?php
}
} );

Algolia v2.8.1

Algolia provides instant search with typo tolerance, autocomplete, and advanced relevance tuning.

Installation & Setup

wp plugin install wp-search-with-algolia --activate

Configuration:

Navigate to Algolia Search → Settings and configure:

Application ID: XXXXXXXX
Search-Only API Key: xxxxxxxxxxxxx
Admin API Key: xxxxxxxxxxxxx (for indexing)
Index Prefix: muddy_
Number of Results: 20

Index Configuration

Product Index Settings

// Indexable attributes
add_filter( 'algolia_post_bigcommerce_product_settings', function( $settings ) {
$settings['attributesToIndex'] = [
'unordered(post_title)',
'unordered(content)',
'unordered(taxonomies.bigcommerce_category)',
'unordered(taxonomies.bigcommerce_brand)',
'unordered(meta.bigcommerce_sku)'
];

$settings['customRanking'] = [
'desc(is_featured)',
'desc(popularity)',
'asc(price)'
];

$settings['attributesForFaceting'] = [
'taxonomies.bigcommerce_category',
'taxonomies.bigcommerce_brand',
'price',
'in_stock'
];

return $settings;
} );

Custom Ranking

// Add custom ranking attributes
add_filter( 'algolia_post_bigcommerce_product_shared_attributes', function( $attributes, $post ) {
$attributes['price'] = (float) get_post_meta( $post->ID, 'bigcommerce_price', true );
$attributes['in_stock'] = (int) get_post_meta( $post->ID, 'bigcommerce_inventory', true ) > 0;
$attributes['is_featured'] = (int) get_post_meta( $post->ID, 'bigcommerce_featured', true );
$attributes['popularity'] = (int) get_post_meta( $post->ID, 'views_count', true );

return $attributes;
}, 10, 2 );

Search UI

// Header search form
<form role="search" class="search-form">
<input type="search"
id="algolia-search-input"
class="search-field"
placeholder="Search products..."
autocomplete="off"
name="s">
<button type="submit" class="search-submit">
<svg><!-- Search icon --></svg>
</button>
</form>

<script>
// Initialize Algolia autocomplete
const search = instantsearch({
indexName: 'muddy_bigcommerce_product',
searchClient: algoliasearch(
'<?php echo esc_js( ALGOLIA_APP_ID ); ?>',
'<?php echo esc_js( ALGOLIA_SEARCH_KEY ); ?>'
)
});

// Configure autocomplete widget
search.addWidgets([
instantsearch.widgets.searchBox({
container: '#algolia-search-input',
placeholder: 'Search products...',
autofocus: true,
showSubmit: false,
showReset: false
}),

instantsearch.widgets.hits({
container: '#search-results',
templates: {
item(hit) {
return `
<a href="${hit.permalink}" class="search-result-item">
<img src="${hit.images.thumbnail.url}" alt="${hit.post_title}">
<div class="search-result-content">
<h3>${hit._highlightResult.post_title.value}</h3>
<p class="price">$${hit.price}</p>
</div>
</a>
`;
}
}
}),

instantsearch.widgets.pagination({
container: '#search-pagination'
})
]);

search.start();
</script>

Search Results Page

// search.php template

get_header();
?>

<div class="search-page">
<div class="container">
<h1>Search Results for: <?php echo get_search_query(); ?></h1>

<div id="algolia-search-wrapper">
<!-- Search refinements -->
<div class="search-sidebar">
<h3>Refine Results</h3>

<!-- Category refinement -->
<div id="category-refinement"></div>

<!-- Price range -->
<div id="price-refinement"></div>

<!-- Brand refinement -->
<div id="brand-refinement"></div>

<!-- Clear refinements -->
<div id="clear-refinements"></div>
</div>

<!-- Search results -->
<div class="search-results">
<div id="search-stats"></div>
<div id="search-hits"></div>
<div id="search-pagination"></div>
</div>
</div>
</div>
</div>

<script>
// Configure Algolia search page
const search = instantsearch({
indexName: 'muddy_bigcommerce_product',
searchClient: algoliasearch('<?php echo ALGOLIA_APP_ID; ?>', '<?php echo ALGOLIA_SEARCH_KEY; ?>'),
routing: true
});

search.addWidgets([
// Search stats
instantsearch.widgets.stats({
container: '#search-stats',
templates: {
text: '{{nbHits}} products found in {{processingTimeMS}}ms'
}
}),

// Category refinement
instantsearch.widgets.refinementList({
container: '#category-refinement',
attribute: 'taxonomies.bigcommerce_category',
limit: 10,
showMore: true
}),

// Price range
instantsearch.widgets.rangeSlider({
container: '#price-refinement',
attribute: 'price',
tooltips: {
format(value) {
return '$' + Math.round(value);
}
}
}),

// Brand refinement
instantsearch.widgets.refinementList({
container: '#brand-refinement',
attribute: 'taxonomies.bigcommerce_brand'
}),

// Clear refinements
instantsearch.widgets.clearRefinements({
container: '#clear-refinements'
}),

// Results
instantsearch.widgets.hits({
container: '#search-hits',
templates: {
item: productTemplate
}
}),

// Pagination
instantsearch.widgets.pagination({
container: '#search-pagination'
})
]);

search.start();
</script>

<?php
get_footer();

Reindexing

# Manual reindex via WP-CLI
wp algolia reindex --post-type=bigcommerce_product

# Schedule automatic reindexing (cron)
wp cron event schedule algolia_reindex hourly

Automatic Reindexing:

// Reindex product on update
add_action( 'bigcommerce/import/product/updated', function( $product_id ) {
if ( function_exists( 'algolia_index_post' ) ) {
algolia_index_post( $product_id );
}
} );

// Schedule daily full reindex
if ( ! wp_next_scheduled( 'algolia_daily_reindex' ) ) {
wp_schedule_event( strtotime( '3:00 AM' ), 'daily', 'algolia_daily_reindex' );
}

add_action( 'algolia_daily_reindex', function() {
if ( function_exists( 'algolia_reindex_post_type' ) ) {
algolia_reindex_post_type( 'bigcommerce_product' );
}
} );

Search Analytics

Track Search Queries

// Log search queries for analytics
add_action( 'pre_get_posts', function( $query ) {
if ( ! is_admin() && $query->is_search() && $query->is_main_query() ) {
$search_term = get_search_query();

// Log to database
global $wpdb;
$wpdb->insert( $wpdb->prefix . 'search_log', [
'search_term' => $search_term,
'results_count' => $query->found_posts,
'timestamp' => current_time( 'mysql' )
] );

// Track in analytics
do_action( 'suma_analytics_track_event', 'search/performed', [
'search_term' => $search_term,
'results_count' => $query->found_posts
] );
}
} );
// Get top search terms
function get_popular_searches( $limit = 10 ) {
global $wpdb;

return $wpdb->get_results( $wpdb->prepare(
"SELECT search_term, COUNT(*) as count
FROM {$wpdb->prefix}search_log
WHERE timestamp > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY search_term
ORDER BY count DESC
LIMIT %d",
$limit
) );
}

Mobile Optimization

Responsive Facets

/* Mobile-friendly facet styling */
@media (max-width: 768px) {
.facets-wrapper {
position: fixed;
top: 0;
left: -100%;
width: 80%;
height: 100vh;
background: white;
z-index: 999;
transition: left 0.3s ease;
overflow-y: auto;
padding: 2rem;
}

.facets-wrapper.open {
left: 0;
}

.mobile-filter-toggle {
display: block;
width: 100%;
padding: 1rem;
background: #2a7e54;
color: white;
border: none;
font-weight: 600;
cursor: pointer;
}
}
// Mobile search experience
if (window.innerWidth < 768) {
// Larger touch targets
document.querySelectorAll('.facetwp-checkbox').forEach(checkbox => {
checkbox.style.minWidth = '44px';
checkbox.style.minHeight = '44px';
});

// Close facets on selection (mobile)
document.addEventListener('facetwp-loaded', () => {
document.querySelectorAll('.facetwp-facet').forEach(facet => {
facet.addEventListener('click', () => {
if (window.innerWidth < 768) {
document.querySelector('.facets-wrapper').classList.remove('open');
}
});
});
});
}

Troubleshooting

FacetWP Not Updating

# Clear FacetWP cache
wp facetwp index

# Rebuild index
wp facetwp rebuild

Algolia Sync Issues

# Clear Algolia cache
wp cache flush

# Force reindex
wp algolia reindex --force --post-type=bigcommerce_product

Performance Issues

// Optimize facet queries
add_filter( 'facetwp_preload_url_vars', function( $url_vars ) {
// Disable facet preloading on initial page load
return [];
} );

// Reduce Algolia records per page
add_filter( 'algolia_posts_per_page', function() {
return 12; // Reduce from default 20
} );