Table of contents :

Building Your First WooCommerce Elasticsearch Query

100-days-of-elasticsearch-woocommerce-009

Table of contents :

Your first Elasticsearch query for WooCommerce should answer a specific storefront question, such as: “Which published products match a customer’s search for waterproof hiking boots?” Start with a small, readable query and add filters only when the business requirement is clear.

Understand the indexed product document

Elasticsearch searches the document that was indexed, not the WooCommerce database directly. The exact field names depend on your indexing pipeline or search extension. A product document might contain fields like name, description, sku, categories, attributes, stock_status, price, status, and visibility.

Before writing a query, inspect a document and its mapping:

GET wc_products/_mapping

GET wc_products/_search
{
  "size": 1,
  "query": {
    "match_all": {}
  }
}

Replace wc_products with the index used by your environment. The mapping tells you whether a field is searchable text, an exact-value keyword, a number, a date, or another type. This matters because a full-text field and an exact filter field are queried differently.

Make a basic full-text query

A match query analyzes the customer’s words before searching. It is a useful first step for product names and descriptions:

GET wc_products/_search
{
  "size": 20,
  "query": {
    "match": {
      "name": "waterproof hiking boots"
    }
  }
}

Elasticsearch analyzes the phrase according to the field’s analyzer and scores products based on relevance. The size parameter limits the number of hits returned. It does not change the total number of matching documents.

Search several WooCommerce fields

Customers usually expect a search term to match more than the product name. A multi_match query searches several fields and allows you to give important fields more weight. The caret in name^4 boosts matches in the product name relative to the other fields.

GET wc_products/_search
{
  "size": 20,
  "query": {
    "multi_match": {
      "query": "waterproof hiking boots",
      "fields": [
        "name^4",
        "short_description^2",
        "description",
        "sku"
      ],
      "type": "best_fields"
    }
  }
}

Only include fields that exist in your mapping. If sku is mapped as a keyword, it may be better handled as an exact lookup or included through a dedicated SKU query rather than treated as ordinary prose.

Add WooCommerce filters with a bool query

A storefront search normally needs business rules in addition to text relevance. Use bool to combine them:

GET wc_products/_search
{
  "from": 0,
  "size": 20,
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "waterproof hiking boots",
            "fields": ["name^4", "short_description^2", "description"]
          }
        }
      ],
      "filter": [
        { "term": { "status": "publish" } },
        { "term": { "visibility": "visible" } },
        { "term": { "stock_status": "instock" } },
        { "range": { "price": { "lte": 180 } } }
      ]
    }
  }
}

Place relevance-bearing clauses such as match and multi_match in must or should. Put yes-or-no constraints in filter. Filter clauses do not contribute to the relevance score and can be cached, which is generally more efficient for catalog restrictions.

Whether a product is visible, published, or in stock may be represented differently by your integration. Confirm the actual values in indexed documents before deploying the query. Do not assume that WooCommerce’s database values and the Elasticsearch document structure are identical.

Filter by category or attribute

Exact taxonomy and attribute filtering usually requires a keyword field. For example, if categories are indexed as a flat keyword array:

GET wc_products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "trail jacket",
            "fields": ["name^4", "description"]
          }
        }
      ],
      "filter": [
        { "term": { "categories.slug": "mens-jackets" } },
        { "term": { "attributes.color.keyword": "blue" } }
      ]
    }
  }
}

The path depends on the document shape. If a category is stored as categories: ["mens-jackets"], query categories directly. If it is stored as objects with slug and name, use the mapped subfield. If attributes are mapped as nested, use a nested query so that the attribute name and value remain associated with the same attribute object.

GET wc_products/_search
{
  "query": {
    "nested": {
      "path": "attributes",
      "query": {
        "bool": {
          "filter": [
            { "term": { "attributes.name.keyword": "color" } },
            { "term": { "attributes.value.keyword": "blue" } }
          ]
        }
      }
    }
  }
}

Sort without losing relevance

For a relevance-ranked result set, omit sort; Elasticsearch returns results by relevance score by default. For a price sort, use a numeric field and define how documents with no price should be handled:

GET wc_products/_search
{
  "size": 20,
  "query": {
    "bool": {
      "must": {
        "match": { "name": "hiking boots" }
      },
      "filter": [
        { "term": { "status": "publish" } }
      ]
    }
  },
  "sort": [
    { "price": { "order": "asc", "missing": "_last" } },
    { "_id": { "order": "asc" } }
  ]
}

Do not sort on an analyzed text field. Use a numeric price field or a keyword subfield. The secondary _id sort gives pagination a more stable tie-breaker when many products have the same price.

Call the query from a WooCommerce integration

An agency might execute the request from a server-side WordPress integration rather than exposing Elasticsearch credentials to the browser. The following PHP example uses WordPress HTTP functions and sends the query to an internal Elasticsearch endpoint:

$body = [
    'size'  => 20,
    'query' => [
        'bool' => [
            'must' => [
                [
                    'multi_match' => [
                        'query'  => sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) ),
                        'fields' => [ 'name^4', 'short_description^2', 'description' ],
                    ],
                ],
            ],
            'filter' => [
                [ 'term' => [ 'status' => 'publish' ] ],
                [ 'term' => [ 'visibility' => 'visible' ] ],
            ],
        ],
    ],
];

$response = wp_remote_post(
    'https://search.internal.example/wc_products/_search',
    [
        'headers' => [ 'Content-Type' => 'application/json' ],
        'body'    => wp_json_encode( $body ),
        'timeout' => 5,
    ]
);

if ( is_wp_error( $response ) ) {
    // Fall back to the normal WooCommerce search path.
}

$data = json_decode( wp_remote_retrieve_body( $response ), true );

Keep authentication, endpoint credentials, and query construction on the server. Validate and limit user-controlled values such as page size, sort direction, category slugs, and price ranges. A production integration should also define a timeout and a fallback path so a search cluster outage does not make the storefront unusable.

Inspect and test the response

Each result appears in the hits.hits array. The document is in _source, while _score shows the relevance score when the query is score-based:

{
  "hits": {
    "total": { "value": 42, "relation": "eq" },
    "hits": [
      {
        "_id": "1234",
        "_score": 8.71,
        "_source": {
          "product_id": 1234,
          "name": "Waterproof Hiking Boots",
          "price": 149.99
        }
      }
    ]
  }
}

Test queries with real customer phrases, misspellings, empty searches, unpublished products, out-of-stock products, products with missing prices, and products containing multiple attributes. Use _search in a development index first, compare results with the intended WooCommerce catalog rules, and verify that reindexing produces the fields your query expects.

Trending posts
You might also like