Typesense Indexing Patterns for Headless WooCommerce
Why Typesense Over Algolia or Elasticsearch
For most headless WooCommerce projects, the search engine choice comes down to three options. Algolia is the easy answer — and the expensive one, with per-search pricing that scales painfully past 50k operations/month. Elasticsearch is powerful but operationally heavy: a JVM, a cluster, and a learning curve that's hard to justify for product search. Typesense lands in the middle — open-source, single-binary, sub-50ms queries on millions of documents, and a pricing model (self-hosted or Typesense Cloud) that doesn't penalize success.
On a recent project syncing 12k WooCommerce products, the entire Typesense Cloud bill came to $19/month. The equivalent Algolia bill would have been north of $400.
Schema Design: Flatten, Don't Nest
Typesense supports nested fields, but querying and faceting against flat fields is dramatically faster. The pattern I use: flatten WooCommerce's taxonomy and meta structure into a denormalized document per product.
{
id: '1234',
name: 'Soy Wax Candle — Vanilla',
price: 29.95,
on_sale: false,
in_stock: true,
categories: ['Candles', 'Soy Wax', 'Vanilla'],
category_ids: [12, 45, 78],
attributes_scent: ['Vanilla', 'Sweet'],
attributes_size: ['200ml'],
permalink: '/product/soy-wax-candle-vanilla/',
image: 'https://cdn.example.com/...',
updated_at: 1747094400
}
Faceting on categories and attributes_* as string arrays gives instant filter counts. The updated_at Unix timestamp drives incremental sync.
Incremental Sync via WP Hooks
A full reindex of 12k products takes ~90 seconds. Doing that on every product save is unacceptable. Instead, hook into WooCommerce's save events and push only the changed document:
add_action('woocommerce_update_product', function($product_id) {
$product = wc_get_product($product_id);
$doc = build_typesense_doc($product);
wp_remote_post(TYPESENSE_URL . '/collections/products/documents?action=upsert', [
'headers' => ['X-TYPESENSE-API-KEY' => TYPESENSE_KEY],
'body' => wp_json_encode($doc),
'timeout' => 5,
]);
}, 20, 1);
Deletes hook into before_delete_post with a guard that the post type is product. Stock changes hook into woocommerce_product_set_stock separately — they fire on every order and you want the lightest possible payload there.
The Full-Reindex Safety Net
Hooks miss things. A bulk SQL update, a WP-CLI import, a database restore — none of these fire the save action. The pattern: a nightly WP-Cron job that walks the product catalog and upserts any document where the WordPress post_modified is newer than Typesense's updated_at.
This catches drift without rebuilding the index from scratch. On the 12k-product site, the nightly reconciliation processes ~50 documents on average and finishes in under 4 seconds.
Search-as-You-Type with InstantSearch
Typesense ships a drop-in adapter for Algolia's react-instantsearch. The Next.js side becomes trivial:
import { TypesenseInstantSearchAdapter } from 'typesense-instantsearch-adapter';
const adapter = new TypesenseInstantSearchAdapter({
server: { apiKey: process.env.NEXT_PUBLIC_TS_SEARCH_KEY, nodes: [...] },
additionalSearchParameters: {
query_by: 'name,categories,attributes_scent',
query_by_weights: '4,2,1',
},
});
The search-only API key is scoped to read-only on the products collection — safe to ship to the browser. The admin key, which can write and create collections, lives only on the server.
The Gotchas
Collection aliases for zero-downtime reindexes. When the schema changes, you can't mutate fields on a live collection. Build a new collection (products_v2), reindex into it, then swap the alias atomically. The frontend always queries products, which points to whichever version is current.
Variable products need a strategy. Indexing each variation as its own document explodes the index size and confuses search results. The cleaner approach: index the parent product with a variation_prices range and a variation_attributes array. Variation-level filtering happens client-side on the result.
Multi-currency means multi-field. If the storefront ships in USD, AUD, and EUR, store price_usd, price_aud, price_eur as separate fields. Sorting and filtering by price requires a single numeric field; you can't sort by a per-locale lookup.
When Not to Use Typesense
If the catalog is under 500 products and the WooCommerce default search (with FibSearch or similar) hits acceptable latency, the operational overhead isn't worth it. Typesense earns its place when search becomes a primary UX surface — instant results, faceted refinement, typo tolerance — not just a fallback when navigation fails.
