Search & Algolia
Evolve supports pluggable search providers behind a unified GraphQL
interface. The catalog service resolves productSearch queries by
delegating to the configured provider, either Algolia for dedicated
search infrastructure or commercetools' built-in Product Search API.
Pluggable providers
The search provider is selected through the SEARCH_ENGINE
environment variable:
if (searchEngine === "algolia") {
return new AlgoliaSearch().searchProducts(context, args);
}
if (searchEngine === "commercetools") {
return new CommercetoolsSearch().searchProducts(context, args);
}
Both providers implement the same SearchProvider base class, so the
GraphQL schema and frontend code are identical regardless of which
provider is active. Switching providers is a configuration change, not
a code change.
Algolia offers faster search responses, typo tolerance, synonym support, and configurable relevance ranking. It requires keeping a separate search index in sync with commercetools.
commercetools uses the commerce engine's built-in search with no additional infrastructure. It supports full-text search and faceting but with less control over relevance tuning.
Algolia index structure
Index naming
Each store gets its own set of Algolia indexes. The naming convention is:
{storeKey}_default # Default relevance sort
{storeKey}_price_asc # Price ascending
{storeKey}_price_desc # Price descending
{storeKey}_suggestions # Query suggestions
Store keys are lowercased. Sort-specific indexes are Algolia replicas that share the same data but apply different ranking strategies.
Record shape
Each Algolia record represents a product with all its variants:
{
objectID: "product-id",
name: { "en-GB": "Running Shoe", "nl-NL": "Hardloopschoen" },
slug: { "en-GB": "running-shoe", "nl-NL": "hardloopschoen" },
description: { "en-GB": "...", "nl-NL": "..." },
image: "https://cdn.example.com/product.jpg",
startingPrice: { currency: "EUR", centAmount: 9999 },
categoryPageId: ["shoes", "shoes > running"],
hierarchicalCategories: {
lvl0: "Shoes",
lvl1: "Shoes > Running",
},
variants: [
{
sku: "SHOE-42-BLACK",
price: { currency: "EUR", centAmount: 9999 },
attributes: { size: "42", color: "black" },
image: "https://cdn.example.com/shoe-black.jpg",
}
]
}
Only variants with a price in the store's currency are included. Localized fields only contain the languages configured for the store. Hierarchical categories enable Algolia's built-in hierarchical faceting.
Data synchronization
Product data reaches Algolia through two paths:
Full batch sync
The sync-algolia job iterates all products in commercetools using
cursor-based pagination, converts each to an Algolia record, and
calls saveObjects() in bulk. Products without a price in the
store's currency are deleted from the index. Run this job after
initial setup or to recover from drift.
Real-time event-driven updates
The catalog service subscribes to commercetools product events through the messaging system:
| Event | Action |
|---|---|
ProductPublished | Re-fetch product, upsert record with partialUpdateObjects |
ProductPriceDiscountsSet | Re-fetch product, upsert record |
ProductUnpublished | Delete record from all store indexes |
ProductDeleted | Delete record from all store indexes |
Updates always re-fetch the full product from commercetools rather than relying on the event payload. This ensures the Algolia record reflects the latest state even if events arrive out of order.
GraphQL search integration
The productSearch query is the primary search interface:
query {
productSearch(
searchTerm: "running shoes"
filters: [
{ key: "color", selections: ["black", "blue"] }
{ key: "price", min: 50, max: 150 }
]
sort: priceAscending
page: 1
pageSize: 24
) {
total
results {
name
slug
variants { sku, price { gross { centAmount, currencyCode } } }
}
facets {
key
label
... on OptionsFacet {
options { key, label, count, selected }
}
... on RangeFacet {
min
max
selectedMin
selectedMax
}
}
}
}
Multi-query pattern (Algolia)
When using Algolia, the search executes multiple queries in a single API call. The first query returns the matching products. Each active facet filter generates an additional query with all other filters applied. This returns the full set of facet options for that specific facet, even when the search is filtered. This provides the "X results available if you remove this filter" counts that users expect.
Facets
Facets are returned as part of the search response. Two types are supported:
Options facets
String or boolean attributes with discrete values. Each option includes a count of matching products:
... on OptionsFacet {
key # "color"
label # "Color"
options {
key # "black"
label # "Black"
count # 42
selected # true/false
}
}
Range facets
Numeric attributes (typically price) with min/max boundaries:
... on RangeFacet {
key # "price"
label # "Price"
min # 10.00
max # 500.00
selectedMin # 50.00 (current filter)
selectedMax # 150.00 (current filter)
}
Price values are converted from centAmounts to currency units (divided by 100) in the facet response.
Facet configuration (commercetools)
When using the commercetools search provider, facet configuration can
be stored per category in a custom field (facetConfig). This allows
different categories to show different facets. When no category-level
configuration exists, a default schema is used.