Product modeling
In 2025, commercetools introduced architectural changes designed to streamline how product data is structured and retrieved. The focus shifted from "per-variant" data management to a more centralized "Product-level" approach, significantly reducing data redundancy and API payload sizes.
These updates are critical for developers looking to optimize storefront performance, especially for large catalogs with high variant counts.
Key enhancements
- Product-level Attributes: historically, Attributes like
brand,manufacturer, ormaterialhad to be replicated across every variant using theSameForAllconstraint. You can now define Product Attributes directly at the Product level. This centralization ensures that common data is stored only once, leading to faster updates and leaner API responses. - Attribute filtering for Product Projections: a new
filter[attributes]query parameter allows you to explicitly include or exclude specific Attributes in the API response. This is a game-changer for mobile performance, where reducing the JSON payload size is vital for quick page loads. - Updated Query Predicate format: to enhance indexing efficiency, Query Predicates for Attributes now require the Attribute
name. This change allows the platform to route queries more effectively, preventing performance degradation as your Attribute count grows.
search-color and color-code):import { ctpClientHTTPAPI, projectKey } from "./BuildClient";
import { createApiBuilderFromCtpClient } from "@commercetools/platform-sdk";
const apiRoot = createApiBuilderFromCtpClient(ctpClientHTTPAPI).withProjectKey({
projectKey,
});
async function getFilteredAttributes() {
await apiRoot
.productProjections()
.withKey({ key: "comfort-coffee-mug" })
.get({
queryArgs: {
// Include only 'search-color' and 'color-code'
"filter[attributes]": ["search-color", "color-code"],
},
})
.execute()
.then((response) => {
console.log(
"Filtered attributes: \n" +
JSON.stringify(response.body.masterVariant.attributes, null, 2)
);
});
}
await getFilteredAttributes();
Filtered attributes:
[ {
"name" : "search-color",
"value" : {
"key" : "white",
"label" : {
"de-DE" : "Weiß",
"en-US" : "White",
"en-GB" : "White"
}
}
}, {
"name" : "color-code",
"value" : "#FFFFFF"
} ]
As you can see by our result, when trying to display all Attributes of the master variant, only the ones included in our filter are available. This not only improves readability but also can help in reducing the payload size of the response we get from the HTTP API.
Product Search
- Distinct Facets: group results by Attributes like
brand,color, ormaterial. - Ranges Facets: counts Products that have values within a specified range.
- Count Facets: counts the number of Products (or Product Variants).
- Stats Facets: dynamically calculate the
min,max, andaveragevalues for numeric fields, such as prices or weights, across the entire search result set.
Last year also introduced several features designed to reduce "no results found" pages and improve conversion rates:
- Fuzzy Search: built-in tolerance for typos. If a customer searches for "coffe mug," the engine automatically identifies the intent for "coffee mug."
- Discounted price filtering: you can now filter specifically for products where
discountedis not null. This allows for the creation of high-performance "Sale" landing pages that update automatically as promotions are activated.
Product Search's speed and efficiency stem from its ID-based nature: it returns only the IDs of matching products, requiring a subsequent call to the GraphQL API for detailed product information. The good news is that Product Search is now directly supported within the GraphQL API, allowing you to achieve everything in a single request!
fullText query would likely return no results (unless, by chance, "tarditional" products exist in the catalog!).fuzzy query feature to tolerate such mistakes. Furthermore, we can enrich the request to also fetch statistical data on the EUR prices for the German market for every matched product. Crucially, we will restrict the results to products without discounted prices. The most significant benefit is that the GraphQL API allows us to achieve all of this in one consolidated request.
Before trying this example make sure that Product Search is activated in your Project.// Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example.
// GraphQL query for fuzzy product search with price statistics facet
const searchQuery = `
query FuzzySearchWithGermanPriceStats {
productsSearch(
query: {
and: [
{
fuzzy: {
field: "name"
value: "tarditional"
level: 2
language: "en-US"
}
}
{
exact: {
field: "variants.prices.discounted"
value: false
}
}
]
}
facets: {
stats: {
name: "priceStatistics"
field: "variants.prices.centAmount"
filter: {
and: [
{
exact: {
field: "variants.prices.currencyCode"
value: "EUR"
}
}
{
exact: {
field: "variants.prices.country"
value: "DE"
}
}
]
}
}
}
) {
results {
product {
key
}
}
facets {
... on ProductSearchFacetResultStats {
name
count
sum
min
max
mean
}
}
}
}
`;
// Execute the GraphQL query and print the results
async function runFuzzyProductSearch(): Promise<void> {
try {
const result = await apiRoot
.graphql()
.post({ body: { query: searchQuery } })
.execute();
// Log the product keys and price statistics from the search response
console.log("Fuzzy search result:");
console.log(JSON.stringify(result.body.data, null, 2));
} catch (error) {
console.error("Error executing product search:", error);
}
}
// Run the search
await runFuzzyProductSearch();
Fuzzy search result:
{
"productsSearch" : {
"results" : [ {
"product" : {
"key" : "traditional-three-seater-sofa"
}
}, {
"product" : {
"key" : "traditional-l-seater-sofa"
}
} ],
"facets" : [ {
"name" : "priceStatistics",
"count" : 2,
"sum" : 599800.0,
"min" : 239900.0,
"max" : 359900.0,
"mean" : 299900.0
} ]
}
}
The single API request performed exactly as intended. The response included data for two “traditional”, non-discounted products, along with statistics on their prices in EUR for the German market. These enhancements are expected to significantly simplify the development of product listing and search pages.
Product Projection Search
productProjectionSearch query in GraphQL often targets specific variant Attributes (for example, "red shirt, size medium"). While the search itself correctly returns the parent product, the variants and allVariants fields on that product traditionally returned all variants, regardless of the search criteria.onlyMatching: Boolean argument to the variants and allVariants fields within the ProductProjection type.onlyMatching: true, you offload the filtering responsibility to the commercetools API. The response is cleaner and more efficient, as it only includes the Product Variants that matched your original productProjectionSearch query.// Ensure all necessary imports are included and the API client is instantiated as demonstrated in the first example.
/**
* GraphQL query utilizing the 2025 search enhancements.
* 1. 'markMatchingVariants: true' flags variants that meet the filter criteria.
* 2. 'allVariants(onlyMatching: true)' filters the results on the server-side,
* returning only the specific SKU(s) that match the 'blue' color filter.
*/
const searchQuery = `
query FindBlueSofa {
productProjectionSearch(
text: "sofa"
locale: "en-US"
filters: {model: {value: {path: "variants.attributes.search-color.key", values: "blue"}}}
markMatchingVariants: true
) {
results {
key
allVariants(onlyMatching: true) {
sku
}
}
}
}
`;
/**
* Executes the search and logs the filtered variant data.
*/
async function onlyMatchingVariantSearch(): Promise<void> {
try {
const result = await apiRoot
.graphql()
.post({ body: { query: searchQuery } })
.execute();
// Log the refined data structure (showing only matching SKUs)
console.log("Search result (Filtered to Matching Variants):");
console.log(JSON.stringify(result.body.data, null, 2));
} catch (error) {
// Catch network or query syntax errors
console.error("Error executing product search:", error);
}
}
// Invoke the search function
await onlyMatchingVariantSearch();
Search result:
{
"productProjectionSearch" : {
"results" : [ {
"key" : "nala-two-seater-sofa",
"allVariants" : [ {
"sku" : "NTSS-02"
}, {
"sku" : "NTTS-04"
} ]
} ]
}
}
As we can see in the result, only variants matching our search criteria were returned.
onlyMatching: true feature in ProductProjectionSearch is a significant developer quality-of-life improvement. By moving the filtering logic from your client application back to the high-performance commercetools API, you can drastically simplify your front-end code, reduce the burden on your application server, and ensure faster, leaner data payloads for a better customer experience.