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
Autocomplete Search Bar
// 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
] );
}
} );
Popular Searches Report
// 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;
}
}
Touch-Friendly Search
// 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
} );