Skip to main content

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:

EventAction
ProductPublishedRe-fetch product, upsert record with partialUpdateObjects
ProductPriceDiscountsSetRe-fetch product, upsert record
ProductUnpublishedDelete record from all store indexes
ProductDeletedDelete 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.