Vespa can provide fast keyword, filtering, ranking, and semantic retrieval for a WooCommerce catalog, but it needs a deliberate feed design. The integration should turn each WooCommerce product into a stable Vespa document, keep the document schema aligned with the search experience, and update Vespa whenever catalog data changes.
Choose the Vespa document model
A practical starting point is one Vespa document per searchable WooCommerce product. Use the WooCommerce product ID as the stable identifier rather than the product name or slug. This keeps updates and deletes deterministic even when a product is renamed.
For variable products, agencies usually choose one of two models:
- Parent-product documents: Store the variable product as one document with aggregated attributes such as available sizes, colors, and price range.
- Variation documents: Store each variation as a separate document when customers need to search or filter individual SKUs, prices, or inventory quantities.
The correct choice depends on the storefront behavior. A product-listing experience generally benefits from parent documents, while SKU-level search, inventory lookup, and B2B catalogs often require variation documents.
Define a schema for search and filtering
The Vespa schema should reflect the fields used by queries and ranking. Text fields can be indexed for full-text search, while numeric and categorical fields should usually be attributes for efficient filtering and sorting.
schema product {
document product {
field product_id type string {
indexing: attribute | summary
}
field title type string {
indexing: index | summary
index: enable-bm25
}
field description type string {
indexing: index | summary
}
field sku type string {
indexing: attribute | summary
}
field price type double {
indexing: attribute | summary
}
field stock_quantity type int {
indexing: attribute | summary
}
field stock_status type string {
indexing: attribute | summary
}
field category_ids type array<string> {
indexing: attribute | summary
}
field brand type string {
indexing: attribute | summary
}
field permalink type string {
indexing: summary
}
field image_url type string {
indexing: summary
}
field updated_at type long {
indexing: attribute | summary
}
}
fieldset default {
fields: title, description, sku
}
rank-profile lexical inherits default {
first-phase {
expression: bm25(title) + bm25(description)
}
}
}
Do not put every WooCommerce field into Vespa by default. Product metadata such as internal notes, order counts, or administrative flags may be unnecessary for search. Keeping the document focused reduces feed size and makes schema changes easier to manage.
Map WooCommerce data to Vespa fields
WooCommerce exposes product data through PHP objects, the REST API, and database tables. For a WordPress plugin or a custom integration running inside WordPress, wc_get_products() and WC_Product provide a safer abstraction than reading post metadata directly.
A product-to-document mapping might look like this:
$document = [
'fields' => [
'product_id' => (string) $product->get_id(),
'title' => wp_strip_all_tags($product->get_name()),
'description' => wp_strip_all_tags($product->get_short_description() . ' ' . $product->get_description()),
'sku' => (string) $product->get_sku(),
'price' => (float) $product->get_price(),
'stock_quantity' => (int) ($product->get_stock_quantity() ?? 0),
'stock_status' => (string) $product->get_stock_status(),
'category_ids' => array_map('strval', wc_get_product_term_ids($product->get_id(), 'product_cat')),
'permalink' => get_permalink($product->get_id()),
'image_url' => wp_get_attachment_image_url($product->get_image_id(), 'full') ?: '',
'updated_at' => time()
]
];
Normalize values before feeding them. Prices should use a numeric representation, category IDs should have a consistent type, and HTML from descriptions should be removed or converted into text. If the store supports multiple currencies, do not overwrite one currency with another. Use separate currency fields or a clearly defined price document model.
Send a document through the Vespa Document API
The Document API accepts a document at a URL containing the namespace, document type, and document ID. A stable ID such as woo-12345 makes the operation idempotent: sending the same product again replaces the existing document.
PUT https://vespa.example.com/document/v1/catalog/product/docid/woo-12345
Content-Type: application/json
{
"fields": {
"product_id": "12345",
"title": "Trail Running Shoes",
"description": "Lightweight shoes for technical trails",
"sku": "TRAIL-001",
"price": 129.99,
"stock_quantity": 18,
"stock_status": "instock",
"category_ids": ["42", "57"],
"permalink": "https://shop.example.com/product/trail-running-shoes/",
"image_url": "https://shop.example.com/uploads/trail-running-shoes.jpg",
"updated_at": 1710000000
}
}
For production traffic, send the request from a worker or integration service rather than making the customer-facing request wait for Vespa. Authenticate the feed endpoint using the deployment’s chosen method, set connection and request timeouts, and record the response body and HTTP status for failed operations.
Feed products from a WordPress integration
A small PHP client can use WordPress’s HTTP API to send a document. The URL should be built from a configured Vespa endpoint, and the document ID should be URL-encoded.
function feed_product_to_vespa(WC_Product $product) {
$vespa_base = rtrim(get_option('vespa_feed_url'), '/');
$doc_id = 'woo-' . $product->get_id();
$url = $vespa_base . '/document/v1/catalog/product/docid/' . rawurlencode($doc_id);
$payload = [
'fields' => [
'product_id' => (string) $product->get_id(),
'title' => wp_strip_all_tags($product->get_name()),
'description' => wp_strip_all_tags($product->get_short_description()),
'sku' => (string) $product->get_sku(),
'price' => (float) $product->get_price(),
'stock_quantity' => (int) ($product->get_stock_quantity() ?? 0),
'stock_status' => (string) $product->get_stock_status(),
'permalink' => get_permalink($product->get_id()),
'updated_at' => time()
]
];
$response = wp_safe_remote_request($url, [
'method' => 'PUT',
'timeout' => 15,
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . get_option('vespa_feed_token')
],
'body' => wp_json_encode($payload)
]);
if (is_wp_error($response)) {
throw new RuntimeException($response->get_error_message());
}
$status = wp_remote_retrieve_response_code($response);
if ($status < 200 || $status >= 300) {
throw new RuntimeException(wp_remote_retrieve_body($response));
}
}
Keep the feed token and endpoint outside product data and avoid logging authorization headers. A queue-backed worker is preferable to synchronous hooks because catalog updates can happen during admin requests, imports, checkout operations, or scheduled stock changes.
Handle WooCommerce lifecycle events
Use WooCommerce hooks to enqueue Vespa operations rather than performing a full feed for every request. Useful triggers include product saves, stock changes, price changes, status changes, and term changes. The exact hook strategy depends on the WooCommerce version and the integration’s requirements, so the worker should reload the product by ID before creating the Vespa document.
add_action('woocommerce_update_product', function ($product_id) {
as_enqueue_async_action('my_feed_product_to_vespa', [
'product_id' => (int) $product_id
]);
});
add_action('my_feed_product_to_vespa', function ($args) {
$product = wc_get_product($args['product_id']);
if (!$product) {
return;
}
if ($product->get_status() !== 'publish') {
delete_product_from_vespa($product->get_id());
return;
}
feed_product_to_vespa($product);
});
Action Scheduler, which is commonly available in WooCommerce installations, can provide retries and background execution. Make the job idempotent and include exponential backoff for temporary failures. Do not retry permanent schema or authentication errors indefinitely.
Delete products that are no longer searchable
Unpublished, trashed, or permanently deleted products should not remain in the search index. The Document API delete operation uses the same document ID as the feed operation.
DELETE https://vespa.example.com/document/v1/catalog/product/docid/woo-12345
Some stores intentionally retain discontinued products for historical or internal search. In that case, feed a status field and filter it at query time instead of deleting the document. This also allows a product to be reactivated without rebuilding its document.
Run an initial catalog feed
For the first deployment, process the catalog in pages rather than loading every product into one PHP request. A typical import should:
- Read a bounded page of published products.
- Transform each product into the Vespa document shape.
- Send documents in controlled batches or concurrent requests.
- Record successful and failed document IDs.
- Retry transient failures after the page has completed.
- Verify document counts and representative search queries.
For large catalogs, use a dedicated importer or service that reads the WooCommerce REST API or an exported feed. Control concurrency so the importer does not overload WordPress, the Vespa endpoint, or the network between them. Keep a checkpoint such as the last processed product ID or updated timestamp, but still provide a full rebuild mode for schema migrations and recovery.
Keep updates consistent
WooCommerce and Vespa are separate systems, so an update can succeed in WordPress while the feed request fails. Store an outbox record or queue job containing the product ID and event timestamp. The worker can then retry until the operation succeeds. Include the WooCommerce modified timestamp in the document, and have the worker reload current data before feeding it; this prevents an older queued event from overwriting newer product information.
For bulk imports, compare a source-side export with Vespa’s expected document IDs. Missing documents, stale timestamps, and unexpected deletions are more useful reconciliation checks than simply counting successful HTTP requests.
Validate the feed before connecting search
Test the feed with products covering simple, variable, out-of-stock, discounted, uncategorized, and HTML-heavy descriptions. Confirm that:
- Product IDs remain unchanged across updates.
- Numeric prices sort correctly and support range filters.
- Category and attribute filters match the intended values.
- Descriptions do not contain unwanted markup or private data.
- Unpublished and deleted products disappear or receive the intended status.
- Variation IDs do not collide with parent-product IDs.
- Retrying the same feed request does not create duplicate documents.
Once the document feed is reliable, Vespa queries can add lexical ranking, structured filters, and later semantic features without changing the WooCommerce source integration.