Skip to content

New: AI agent integration via Model Context Protocol (MCP).Learn more

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.

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.

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.

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.

Product data reaches Algolia through two paths:

graph TD;
    ct["commercetools"] -- "subscription events" --> catalog["Catalog service"]
    catalog -- "real-time update" --> algolia["Algolia"]
    job["Full sync job"] -- "batch upsert" --> algolia
    job -- "paginate products" --> ct

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.

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.

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
}
}
}
}

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 are returned as part of the search response. Two types are supported:

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
}
}

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.

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.