Price filtering is one of the most common requirements in WooCommerce search: show products between two values, find items below a sale threshold, or build a layered price-navigation experience. Elasticsearch range queries are designed for these operations, but the result depends on how WooCommerce prices are indexed and which price concept the store needs to expose.
What a price range query does
A range query matches documents whose value falls within a specified interval. Elasticsearch supports four primary operators:
gte: greater than or equal togt: greater thanlte: less than or equal tolt: less than
For example, the following query finds products with a current price from 50 through 100, including both boundaries:
{"query":{"range":{"price":{"gte":50,"lte":100}}}}
The field must be mapped as a numeric or date field for a numeric range query. A WooCommerce price stored as a keyword value can produce lexical comparisons rather than numeric comparisons, causing incorrect results such as a value of 100 being ordered before 20.
Use filter context for price constraints
Price constraints normally determine whether a product is eligible; they do not contribute to relevance scoring. Put them in the filter portion of a Boolean query rather than in must:
{"query":{"bool":{"filter":[{"term":{"status":"publish"}},{"range":{"price":{"gte":50,"lte":100}}}]}}}
Filter clauses are generally more appropriate for faceted catalog search because they do not affect relevance scores and can benefit from Elasticsearch’s filter execution optimizations. Full-text clauses such as product-name matching can remain in must or should while the price range stays in filter.
Mapping WooCommerce prices correctly
Choose a representation that matches the store’s currency and precision requirements. For a single-currency catalog using ordinary decimal prices, a scaled_float is often a practical option:
{"mappings":{"properties":{"price":{"type":"scaled_float","scaling_factor":100}}}}
With a scaling factor of 100, a price of 19.95 is indexed with two-decimal precision. An alternative is to store the smallest currency unit as an integer, such as 1995 cents, and query using integer bounds:
{"query":{"range":{"price_minor":{"gte":5000,"lte":10000}}}}
Integer minor units can make rounding behavior explicit, but the application must consistently convert user-facing values before constructing the query. Avoid changing the type of an existing field in place; Elasticsearch field types are fixed within an index. Create a new index and reindex the documents when a mapping correction is required.
Decide which WooCommerce price to index
WooCommerce can expose several price values, and they are not interchangeable:
- Regular price: the normal catalog price before a sale.
- Sale price: the promotional price when a valid sale is active.
- Current or display price: the amount the shopper is expected to see, often derived from sale and regular prices.
- Minimum and maximum variation prices: useful when a variable product represents a range of variation prices.
For a storefront price filter, index the effective display price or a clearly defined equivalent. If the catalog needs separate filters for regular and sale pricing, index separate numeric fields such as regular_price, sale_price, and current_price. Document whether the values include tax, exclude tax, include currency conversion, or reflect customer-specific pricing.
A product-level current_price is usually sufficient for simple products. Variable products require a deliberate choice: filtering on the minimum variation price may include a product whose other variations are outside the selected range, while filtering nested variation documents can provide more precise matching.
Building a price slider query
A range slider should send normalized numeric bounds to the application layer. The server should validate and convert those bounds before inserting them into the Elasticsearch request:
{"query":{"bool":{"filter":[{"range":{"current_price":{"gte":25,"lte":75}}}]}}}
Do not concatenate raw request parameters into a query string. Parse the values as numbers, reject non-finite values, apply sensible minimum and maximum limits, and define what happens when only one bound is supplied. A lower-only filter can use gte, while an upper-only filter can use lte:
{"query":{"range":{"current_price":{"gte":25}}}}
When prices are stored as integer minor units, convert the slider values before querying. For example, a user-facing range of 25.00 to 75.00 becomes 2500 through 7500 when the indexed currency unit is cents.
Combining price ranges with WooCommerce filters
Production catalog queries commonly combine price with stock, taxonomy, visibility, and text search:
{"query":{"bool":{"must":[{"multi_match":{"query":"running shoes","fields":["name^3","description"]}}],"filter":[{"term":{"stock_status":"instock"}},{"term":{"product_cat.slug":"footwear"}},{"range":{"current_price":{"gte":50,"lte":150}}}]}}}
Use term queries for exact values such as stock status and category identifiers, assuming those fields are mapped appropriately. Keep the price condition independent of the text query so relevance scoring remains focused on the shopper’s search terms.
Creating price buckets for navigation
A range aggregation can return counts for predefined price bands. This is useful for a price facet or for displaying how many matching products fall into each business-defined interval:
{"size":0,"aggs":{"price_ranges":{"range":{"field":"current_price","ranges":[{"to":25},{"from":25,"to":50},{"from":50,"to":100},{"from":100}]}}}}
Range bucket boundaries are lower-inclusive and upper-exclusive. In this example, a product priced at exactly 25 belongs to the 25–50 bucket, not the bucket below 25. Make that behavior clear in the user interface. If the frontend requires different labels or inclusive wording, generate labels in application code rather than relying on ambiguous bucket names.
For automatically calculated statistics, a stats aggregation can return the minimum, maximum, average, sum, and count:
{"size":0,"aggs":{"price_stats":{"stats":{"field":"current_price"}}}}
Run price aggregations after the catalog filters have been applied if the navigation should describe the current result set. For example, a category and stock filter should normally restrict the aggregation as well as the returned products.
Handling variable products
If each variation is indexed as its own document, a range query on the variation price is straightforward, but the response may contain variations rather than parent products. The application can group results by parent product, or the index can use nested variation objects.
With nested variations, the price condition must use a nested query so Elasticsearch evaluates fields from the same variation object:
{"query":{"nested":{"path":"variations","query":{"range":{"variations.price":{"gte":50,"lte":100}}}}}}
This prevents a false match where one variation supplies the qualifying price and another variation supplies a qualifying attribute. The exact query must match the index mapping; the nested clause is required only when variations is mapped as nested.
Operational checks for WooCommerce agencies
- Verify the field mapping with the index mapping API before diagnosing query behavior.
- Inspect representative documents, including simple, variable, sale, zero-price, and out-of-stock products.
- Define whether prices are tax-inclusive and whether customer or currency context changes the indexed value.
- Reindex products after changing the price representation or mapping.
- Keep price synchronization aligned with WooCommerce updates, scheduled sale start times, sale end times, stock changes, and bulk edits.
- Test boundary values such as 0, 9.99, 10, and 10.01 to catch rounding and inclusivity errors.
Price range queries are reliable when the index contains a stable numeric representation and the application defines exactly which WooCommerce price is being filtered. The query itself is simple; the important engineering work is maintaining consistent currency, precision, variation behavior, and synchronization throughout the catalog pipeline.