Faceted navigation is essential in WooCommerce stores, but product filters should not accidentally change the order of otherwise relevant results. A customer searching for “running shoes” should still see the products most related to that phrase after selecting a size, brand, or price range.
Separate relevance from eligibility
Elasticsearch uses query context to calculate relevance scores and filter context to decide whether a document is eligible. Full-text clauses such as match and multi_match belong in the scoring portion of the request. Exact constraints such as stock status, product category, brand, price, and attributes belong in a bool.filter array.
Clauses in filter context do not contribute to the _score. They narrow the result set while leaving the text relevance calculation to the search query.
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "running shoes",
"fields": ["name^4", "short_description", "description"]
}
}
],
"filter": [
{ "term": { "stock_status": "instock" } },
{ "term": { "brand.keyword": "Acme" } },
{ "range": { "price": { "gte": 80, "lte": 180 } } }
]
}
}
}
In this example, the text query determines which products best match “running shoes”. The stock, brand, and price conditions only determine which matching products are allowed to remain in the result set.
Use exact field types for WooCommerce filters
WooCommerce filters usually represent exact values, so the Elasticsearch mapping must support exact matching. Product attributes, brands, visibility flags, stock status, and taxonomy identifiers are commonly mapped as keyword fields. Prices and numeric custom fields should use numeric types such as double, integer, or scaled_float.
{
"properties": {
"stock_status": { "type": "keyword" },
"brand": {
"properties": {
"slug": { "type": "keyword" },
"name": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }
}
},
"price": { "type": "scaled_float", "scaling_factor": 100 },
"attributes": {
"properties": {
"pa_size": { "type": "keyword" },
"pa_color": { "type": "keyword" }
}
}
}
}
Do not use an analyzed text field for a strict facet. A query against brand may be analyzed and may match partial terms, while brand.keyword can represent one exact brand value. The correct field name depends on the index mapping generated by the agency’s synchronization process.
Choose filter, must, and post_filter deliberately
filter: Use for constraints that should not affect relevance, including catalog visibility, stock, taxonomy terms, attributes, and numeric ranges.must: Use for clauses that should contribute to matching and scoring, such as full-text product search.post_filter: Use when search hits should be filtered but facet aggregations should describe the broader result set before the selected facet is applied.
post_filter is particularly useful for WooCommerce layered navigation. Suppose a customer selects the “red” color facet. The product hits should contain only red products, but the color aggregation may still need to show other available colors so the customer can change the selection. A normal query filter applies before aggregations and can make selected facets appear artificially narrow.
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "running shoes",
"fields": ["name^4", "description"]
}
}
],
"filter": [
{ "term": { "stock_status": "instock" } },
{ "term": { "brand.keyword": "Acme" } }
]
}
},
"post_filter": {
"term": { "attributes.pa_color": "red" }
},
"aggs": {
"colors": {
"terms": { "field": "attributes.pa_color" }
}
}
}
The exact aggregation strategy depends on the desired user interface. If every selected filter must remain applied when calculating every other facet, use filtered aggregations or a dedicated aggregation filter for each facet. Document this behavior because it affects both the Elasticsearch request and the WooCommerce filter component.
Handle multiple values and attribute combinations correctly
Multiple values within one facet normally use a terms query, which behaves like OR. For example, selecting medium or large should match either size. Multiple different facets normally become separate entries in the filter array, which behaves like AND.
{
"query": {
"bool": {
"filter": [
{ "terms": { "attributes.pa_size": ["m", "l"] } },
{ "terms": { "attributes.pa_color": ["black", "navy"] } }
]
}
}
}
Product attributes need extra care when a product contains arrays of objects. If each object represents a related attribute pair, use a nested mapping and a nested query. Otherwise, Elasticsearch can match the size from one object and the color from another, creating a combination that does not exist in the product data.
{
"query": {
"bool": {
"filter": [
{
"nested": {
"path": "variations",
"query": {
"bool": {
"filter": [
{ "term": { "variations.size": "m" } },
{ "term": { "variations.color": "red" } }
]
}
}
}
}
]
}
}
}
Keep ranking stable while filters change
Moving a facet from must into filter prevents that facet from adding score, but the result order can still change because the eligible product set has changed. A highly relevant product that was previously ranked below several other products may become the first result after those products are excluded. That is expected and is different from the filter itself changing the score.
If the business requires a secondary ordering, define it explicitly. For example, sort first by _score, then by a stable product field such as popularity or product ID. A stable tie-breaker prevents products with equal scores from moving unpredictably between pages.
{
"sort": [
{ "_score": "desc" },
{ "popularity": "desc" },
{ "product_id": "asc" }
]
}
WooCommerce implementation checks
- Build the full-text search clauses separately from the selected filter clauses.
- Translate slugs and IDs into exact values that match the Elasticsearch mapping.
- Apply stock, visibility, and exclusion rules consistently to search, category pages, and autocomplete.
- Use
post_filteror filtered aggregations when the facet counts must ignore one or more selected facets. - Confirm that variation-level filtering uses the correct parent-product and nested-document strategy.
- Test empty filters, invalid values, multiple values, price boundaries, pagination, and zero-result responses.
Measure the effect with realistic searches
Agencies should compare ranking before and after each filter is applied. Use representative WooCommerce queries, inspect _score values, and run the Elasticsearch _explain API against selected products when a ranking change is unexpected. Also verify aggregation counts independently; correct product hits with incorrect facet counts still produce a poor shopping experience.
The core design is simple: let full-text clauses answer “how relevant is this product?” and let filter clauses answer “is this product allowed in the current catalog view?” Keeping those responsibilities separate makes search behavior easier to tune, cache, test, and explain to store owners.