WooCommerce search often begins with a database query over product titles, SKUs, descriptions, and taxonomy terms. That approach can work for a small catalog, but it becomes harder to tune when an agency is managing large catalogs, multilingual content, frequent inventory updates, and merchandising requirements. Vespa provides a dedicated search layer that can index WooCommerce product content and rank matching documents in real time.
This implementation focuses on full-text product search: matching a shopper’s words against indexed product fields and returning the most relevant products. Filtering, personalization, semantic retrieval, and recommendations can be added later without replacing the product-search foundation.
Choose the WooCommerce fields to index
Start by defining which fields contain searchable language and which fields are primarily used for filtering or display. A typical product document might include:
- title: the product name and important descriptive terms
- description: the long description, with HTML removed or normalized before indexing
- short_description: concise merchandising copy
- sku: useful for exact or partial identifier searches
- categories: category names and paths
- attributes: searchable values such as material, size, color, or compatibility
- product_id: the WooCommerce identifier used to synchronize and link results
Do not send the complete WooCommerce REST response directly to the index. Build a deliberate search document instead. This keeps the schema stable, avoids indexing unnecessary markup, and makes it easier to control which fields affect ranking.
Define a Vespa product schema
The following schema gives Vespa indexed text fields for full-text matching, summary fields for returned content, and attributes for identifiers and filtering.
schema product {
document product {
field product_id type string {
indexing: attribute | summary
}
field title type string {
indexing: index | summary
index: enable-bm25
}
field short_description type string {
indexing: index | summary
index: enable-bm25
}
field description type string {
indexing: index | summary
index: enable-bm25
}
field sku type string {
indexing: attribute | summary
}
field categories type array<string> {
indexing: index | attribute | summary
index: enable-bm25
}
}
fieldset default {
fields: title, short_description, description, categories
}
rank-profile text inherits default {
first-phase {
expression: bm25(title) + 0.7 * bm25(short_description) + 0.4 * bm25(description) + 0.5 * bm25(categories)
}
}
}
The exact weights should be validated with representative queries. Product titles usually deserve more influence than long descriptions because titles are concise and generally reflect stronger purchase intent. Categories can help match broad searches, but they should not overwhelm a direct title match.
For larger catalogs, consider separate fields for normalized attributes rather than concatenating everything into one description. A shopper searching for a “waterproof hiking jacket” should match the relevant product language, while a filter for size or color should use structured fields instead of relying only on full-text ranking.
Prepare and feed WooCommerce products
A synchronization process should convert each WooCommerce product into a Vespa document. It can use the WooCommerce REST API for an initial import and webhooks or scheduled change detection for ongoing updates.
Before feeding content, remove HTML tags from descriptions, decode entities, normalize whitespace, and preserve meaningful text such as headings or list items. A product with variations may require a design decision: index the parent product for a simple catalog experience, or index each purchasable variation when size, color, stock, or SKU must affect the search result.
const productDocument = {
fields: {
product_id: String(product.id),
title: product.name,
short_description: stripHtml(product.short_description),
description: stripHtml(product.description),
sku: product.sku || "",
categories: product.categories.map(category => category.name)
}
};
await vespaFeed.putDocument(`id:shop:product::${product.id}`, productDocument);
Use the WooCommerce product ID as the stable Vespa document key. When a product is deleted, send a document deletion as part of the synchronization flow. Otherwise, discontinued products can remain searchable even though they no longer exist in WooCommerce.
Run a full-text query
A basic Vespa query can send the shopper’s text to userQuery() and select the custom text ranking profile.
GET https://search.example.com/search/?yql=select%20*%20from%20product%20where%20userQuery()&query=waterproof%20hiking%20jacket&ranking=text&hits=24
In production code, construct query parameters with a URL encoder rather than concatenating raw user input into the URL. Keep the Vespa endpoint behind a server-side service or controlled proxy when credentials, tenant isolation, rate limiting, or response shaping are required.
The returned summary fields can be used to render search results directly, or the service can use the returned product IDs to retrieve current price, stock, and purchase links from WooCommerce. If freshness is important, index frequently changing sellable data carefully and define which system is authoritative for each field. Search text and product availability do not always have the same update cadence.
Handle exact identifiers separately
Full-text ranking is appropriate for natural-language searches, but SKU searches often require exact behavior. A search service can detect identifier-like input and add a structured condition, or expose a separate lookup path.
select * from product where sku contains "ABC-1042" or userQuery()
Use query construction rules that safely encode values and avoid treating arbitrary shopper input as trusted query syntax. For partial SKU matching, index an appropriate n-gram or normalized field rather than assuming that a standard text field will behave like a substring database search.
Use query and ranking controls for agency requirements
Agencies commonly need different search behavior for different stores or catalogs. Keep those differences in configuration where possible:
- Use field weighting to prioritize titles, brands, or curated product copy.
- Use structured filters for product status, catalog visibility, stock, price ranges, categories, and attributes.
- Use a separate rank profile for a brand-focused storefront or a B2B catalog.
- Apply business rules after relevance has been established, such as a controlled boost for in-stock products.
- Log the query, result IDs, response time, and zero-result status for evaluation.
A rank profile should remain explainable. If a product is promoted because it matches the title, category, and description, the team should be able to inspect that behavior. Avoid hiding important merchandising logic in application code where it cannot be tested consistently across pagination and deployments.
Test relevance with real WooCommerce queries
Create a relevance set from actual store searches rather than testing only invented examples. Include spelling variations, singular and plural forms, brand names, SKUs, category terms, attribute combinations, and queries that should return no results.
- Collect representative queries from WooCommerce analytics and site-search logs.
- Record the products that merchants consider acceptable results for each query.
- Compare title-only, weighted full-text, and filtered searches.
- Measure zero-result rate, click-through rate, add-to-cart rate, and response latency.
- Review false positives caused by generic description text or unrelated category names.
- Version schema and rank-profile changes so relevance regressions can be rolled back.
For a multi-client agency, store these test cases outside the application deployment and run them whenever a schema, feed transformation, or ranking profile changes. Search quality is part of the store’s product experience, not just an infrastructure concern.
Keep the synchronization path reliable
Use an initial full feed followed by incremental updates. Webhooks can reduce propagation delay, while a periodic reconciliation job catches failed deliveries, missed events, and manual data changes. The synchronization service should be idempotent: feeding the same WooCommerce product more than once should produce the same document state.
Monitor feed failures separately from query failures. Useful operational signals include the age of the oldest unapplied product update, the number of WooCommerce products missing from Vespa, document deletion failures, zero-result searches, and the percentage of queries that fall back to a slower path.
For stores with multiple languages, decide whether each language should be stored in separate fields, separate document versions, or separate content feeds. Configure language-aware processing and ranking deliberately; indexing translated and untranslated text into one undifferentiated field can produce confusing matches.