Why the WooCommerce-to-Elasticsearch mapping matters
A WooCommerce product contains much more than a name and price. It may include SKUs, descriptions, taxonomies, attributes, stock state, sale dates, images, custom fields, and multiple variations. Elasticsearch can search and filter this data efficiently, but only when the document structure reflects how customers use the catalogue.
For agencies, the mapping should be designed before indexing the first product. A reliable design prevents common problems such as prices being indexed as text, category filters behaving inconsistently, and variation attributes being matched across different variants.
Start with a stable product document
Each Elasticsearch document should represent one searchable WooCommerce product unless the project specifically requires one document per variation. Include a stable identifier that connects the document to the WooCommerce product and a URL that can be used by the frontend.
A practical product document might contain these fields:
{
"product_id": 1842,
"sku": "SHOE-001",
"name": "Trail Running Shoe",
"description": "A lightweight running shoe for mixed terrain.",
"permalink": "https://example.com/product/trail-running-shoe/",
"status": "publish",
"catalog_visibility": "visible",
"product_type": "variable",
"categories": [
{"id": 12, "slug": "footwear", "name": "Footwear"},
{"id": 18, "slug": "running-shoes", "name": "Running Shoes"}
],
"brands": [
{"id": 7, "slug": "acme", "name": "Acme"}
],
"attributes": [
{"slug": "pa_material", "name": "Material", "values": ["Mesh"]}
],
"price": 89.99,
"regular_price": 109.99,
"sale_price": 89.99,
"on_sale": true,
"stock_status": "instock",
"stock_quantity": 24,
"average_rating": 4.7,
"review_count": 86,
"images": [
{"src": "https://example.com/uploads/trail-running-shoe.jpg", "alt": "Trail running shoe"}
],
"variations": [
{"id": 1843, "sku": "SHOE-001-42", "size": "42", "color": "Black", "price": 89.99, "stock_status": "instock"}
],
"updated_at": "2025-02-10T14:30:00Z"
}
Use WooCommerce IDs as numeric fields and preserve SKUs and slugs as exact values. A SKU may contain leading zeroes or punctuation, so it should not be treated as a number.
Define explicit field types
Do not rely on Elasticsearch dynamic mapping for a production catalogue. Dynamic mapping can turn a field into an unsuitable type after one unexpected value is indexed. Define the important fields explicitly.
{
"mappings": {
"dynamic": "strict",
"properties": {
"product_id": {"type": "long"},
"sku": {"type": "keyword"},
"name": {
"type": "text",
"fields": {
"keyword": {"type": "keyword", "ignore_above": 256}
}
},
"description": {"type": "text"},
"permalink": {"type": "keyword", "index": false},
"status": {"type": "keyword"},
"catalog_visibility": {"type": "keyword"},
"product_type": {"type": "keyword"},
"price": {"type": "scaled_float", "scaling_factor": 100},
"regular_price": {"type": "scaled_float", "scaling_factor": 100},
"sale_price": {"type": "scaled_float", "scaling_factor": 100},
"on_sale": {"type": "boolean"},
"stock_status": {"type": "keyword"},
"stock_quantity": {"type": "integer"},
"average_rating": {"type": "half_float"},
"review_count": {"type": "integer"},
"updated_at": {"type": "date"},
"categories": {
"properties": {
"id": {"type": "long"},
"slug": {"type": "keyword"},
"name": {"type": "keyword"}
}
},
"brands": {
"properties": {
"id": {"type": "long"},
"slug": {"type": "keyword"},
"name": {"type": "keyword"}
}
},
"attributes": {
"type": "nested",
"properties": {
"slug": {"type": "keyword"},
"name": {"type": "keyword"},
"values": {"type": "keyword"}
}
},
"images": {
"properties": {
"src": {"type": "keyword", "index": false},
"alt": {"type": "text"}
}
},
"variations": {
"type": "nested",
"properties": {
"id": {"type": "long"},
"sku": {"type": "keyword"},
"size": {"type": "keyword"},
"color": {"type": "keyword"},
"price": {"type": "scaled_float", "scaling_factor": 100},
"stock_status": {"type": "keyword"}
}
}
}
}
}
The text type is appropriate for full-text search. The keyword type is appropriate for exact matching, filtering, sorting, and aggregations. A multi-field such as name.keyword supports both product-name search and exact sorting or duplicate detection.
Normalize WooCommerce values before indexing
WooCommerce commonly exposes prices as strings, dates in different formats, and stock quantities that may be empty when stock management is disabled. Normalize these values in the integration layer rather than making every Elasticsearch query compensate for them.
For example:
- Convert
"89.99"to the numeric value89.99. - Convert WooCommerce date values to ISO 8601 timestamps.
- Store
nullfor an unavailable stock quantity instead of using an arbitrary zero. - Convert taxonomy term slugs to lowercase and keep display names separately.
- Exclude products that are not published or are hidden from the catalogue, unless the index serves an administrative search interface.
- Use the effective current price in
price, while retaining regular and sale prices for display and range filters.
Currency handling needs an explicit decision. If an index serves one currency, a scaled_float with a scaling factor of 100 is usually suitable. For multi-currency stores, index a currency code and either maintain separate price fields per currency or create separate documents per currency context. Do not compare values from different currencies in one range aggregation.
Model taxonomies for filtering
Categories, brands, and product tags are normally filterable exact values. Store a stable term ID or slug in a keyword field. The display name can be stored alongside it for filter labels.
A filter for the running-shoes category can then use a term query:
{
"term": {
"categories.slug": "running-shoes"
}
}
If the category hierarchy is important, store a separate field containing ancestor slugs, such as category_ancestors: ["footwear", "sport"]. This allows a query for a parent category to include all descendants without requiring a hierarchy query at request time.
For multilingual stores, do not overwrite one language’s category name with another. Keep language-specific display fields or use separate localized indices, depending on the search architecture.
Treat attributes as structured data
WooCommerce attributes can be global taxonomies or product-specific attributes. Store the attribute slug and values consistently so filters do not depend on display text.
A simple structure works when the application only filters one attribute at a time. Use nested when the relationship between an attribute and its values must be preserved. For example, an attribute document can contain slug: pa_color and values: ["Black", "Blue"].
Variation data should also be nested when multiple conditions must apply to the same variation. Without nested mapping, a query for size: 42 and color: Red could match a product that has size 42 in one variation and Red in another.
A same-variation filter can be written as:
{
"nested": {
"path": "variations",
"query": {
"bool": {
"filter": [
{"term": {"variations.size": "42"}},
{"term": {"variations.color": "Red"}},
{"term": {"variations.stock_status": "instock"}}
]
}
}
}
}
If the business only needs product-level filtering and does not need variation-level stock or price logic, flattening selected variation values may be simpler. The correct choice depends on the catalogue and checkout flow.
Decide whether products or variations are the documents
One document per product
This approach is useful for category pages and product search. A parent product contains its variations, and the frontend opens the product page after the customer selects options.
Advantages include fewer documents, straightforward product-level relevance, and simple result rendering. The trade-off is that variation filters and availability checks require nested queries.
One document per variation
This approach is useful when every size, colour, or configuration must appear as an independent searchable result. Each document includes the parent product ID and variation-specific data.
Advantages include simple filtering and sorting by variation price or stock. The trade-off is duplicate product results, so the application may need collapse, grouping, or a post-processing step to display one product card per parent product.
Do not mix the two approaches accidentally. A product index should have a documented identity rule, such as product_id as the document ID or product_id-variation_id for variation documents.
Build search fields separately from display fields
Searchable content and display content have different requirements. Product names, short descriptions, and selected attributes usually belong in text fields. URLs, IDs, stock states, and slugs usually do not need full-text analysis.
A copy_to field can provide a single search target while retaining the original fields for display and filtering:
{
"mappings": {
"properties": {
"name": {"type": "text", "copy_to": "search_text"},
"sku": {"type": "keyword", "copy_to": "search_text"},
"description": {"type": "text", "copy_to": "search_text"},
"search_text": {"type": "text"}
}
}
}
For SKUs, consider a separate exact SKU query rather than applying normal language analysis. Customers often search for punctuation-sensitive values such as SHOE-001.
Remove HTML from WooCommerce descriptions before indexing, or use a dedicated HTML-to-text transformation. Retain the original HTML outside the indexed search field if it is needed for product rendering.
Keep indexing synchronized with WooCommerce
The mapping is only useful if documents remain current. Product creation, updates, deletion, stock changes, price changes, taxonomy changes, and variation updates should all trigger an indexing event.
Use the WooCommerce product ID as the Elasticsearch document ID. This makes updates idempotent and prevents duplicate documents when a webhook or queue message is retried. Send the complete normalized document when practical rather than applying many small partial updates that can leave stale nested data behind.
For large catalogues, use a queue and bulk indexing. A typical bulk operation should:
- Load the WooCommerce product and its variations.
- Apply visibility and stock rules.
- Normalize prices, dates, taxonomies, and attributes.
- Generate the complete Elasticsearch document.
- Submit batches using the Bulk API.
- Record rejected items and retry transient failures.
- Use a delete action when a product is permanently removed or should no longer be searchable.
When the mapping changes incompatibly, create a new index, populate it, validate representative queries, and switch an alias to the new index. Avoid changing field types in place; Elasticsearch does not allow a field to change from keyword to text or from text to a numeric type within the same index.
Validate the mapping with real catalogue queries
Before releasing the index, test the queries used by the storefront and merchandising team:
- Product-name and SKU searches.
- Category, brand, and attribute filters.
- Price ranges and sorting.
- In-stock-only filtering.
- Sale-product filtering.
- Same-variation size and colour filtering.
- Products with no SKU, no stock quantity, or no sale price.
- Products containing accented characters, HTML, or duplicate taxonomy labels.
Also inspect the mapping with the Elasticsearch mapping API and review the index’s field count. Avoid indexing every WooCommerce custom field automatically. High-cardinality or unpredictable metadata can create mapping growth and unnecessary storage. Whitelist custom fields that have a clear search, filter, or display purpose, and map unknown metadata as flattened or exclude it when appropriate.