@commercetools-demo/search-config-runtime
v0.1.0
Published
Framework-agnostic runtime that resolves commercetools search configuration and compiles Product Search requests.
Readme
@commercetools-demo/search-config-runtime
Framework-agnostic runtime for search configuration authored in the Merchant
Center. Reads a published configuration document from commercetools Custom
Objects, compiles a ProductSearchRequest, and applies the merchandising that
Product Search cannot express on its own.
No dependencies. Works in any JavaScript server tier.
Install
npm install @commercetools-demo/search-config-runtimeYour storefront's API client needs view_published_products (or
view_products) and view_key_value_documents.
Use
import {
createConfigLoader, compileSearchRequest, applyMerchandising,
} from '@commercetools-demo/search-config-runtime';
const loader = createConfigLoader({
apiUrl: process.env.CTP_API_URL!,
projectKey: process.env.CTP_PROJECT_KEY!,
getAccessToken: () => myTokenProvider(),
});
export async function search(term: string, page = 0) {
const resolver = await loader.getResolver('default');
if (!resolver) throw new Error('No published search configuration.');
const ctx = { locale: 'en-US', currency: 'USD', country: 'US' };
const config = resolver.resolve(ctx);
const compiled = compileSearchRequest(config, { query: term, page, ctx });
const response = await fetch(
`${process.env.CTP_API_URL}/${process.env.CTP_PROJECT_KEY}/products/search`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${await myTokenProvider()}`,
'Content-Type': 'application/json',
// Routing header for semantic and hybrid. compileSearchRequest supplies it.
...compiled.headers,
},
body: JSON.stringify(compiled.body),
}
);
const body = await response.json();
const { items } = applyMerchandising(body.results, compiled, {
// Needed only for boost and bury; pin and hide work without it.
accessor: (item, field) => readAttribute(item, field),
});
return { items, total: body.total };
}What it does, and what it cannot
Configuration splits into what commercetools applies and what this package applies.
| Concern | Where it happens |
| --- | --- |
| Search mode (lexical, semantic, hybrid) | userQuery on the request |
| Facets, sorting, pagination | Compiled into the request |
| Category and store scoping | query, so facet counts inherit it |
| Shopper refinements | postFilter, so facet counts stay stable |
| Price sorting scoped to the request currency | sort.filter on the price path |
| Boost in structured mode | Boosted expressions in query |
| Boost in userQuery modes, bury, pin, hide | Post-fetch, in applyMerchandising |
| Synonyms | Text substitution in userQuery.value — see below |
| Field weights, embedding attributes | commercetools support applies these. Export a request with buildTuningRequest |
Three limits are worth stating plainly:
- Boost blends with relevance; it does not partition. Scoring is
factor / (rank + 2), so a 1.6x boost nudges a product up a few places while a 6x boost can reach the top. Sorting by factor alone would put every boosted product above every unboosted one however irrelevant — an always-on 1.6x boost pushed the actual matches off page one before this was fixed. - Post-fetch reordering only sees the fetched window. Boost, bury, pin and
hide are all applied after the fetch, so the compiler over-fetches
(
pagination.merchandisingOverfetch) — but a product ranked below that window cannot be promoted into view.sort.filteris not an alternative: it selects which value to sort by within a multi-valued nested field, and pointing it at an unrelated field fails server-side. - Synonyms substitute, they do not expand.
userQueryrequires every term to match, so appending alternatives narrows a query to nothing: against a catalogue of dressers,armoirereturns 0 results and so doesarmoire dresser closet wardrobe. Each set's first term is canonical — the word the catalogue uses — and matched terms are rewritten onto it. Rewriting the text rather than the expressions is forced byuserQuerybeing opaque; putting alternatives in the structuredquerywould filter rather than broaden. - Semantic matching is product-level. In semantic or hybrid mode without a
variant-level filter,
markMatchingVariantsreports every variant as matched.
Configuration layering
A published bundle carries the whole profile chain rather than a pre-resolved
snapshot, because locale, currency, customer group, and channel vary per
request. resolveConfig merges root-first: each profile's base, then every
matching context override in declared order.
Two merge rules:
- Objects merge deeply.
- Arrays merge by
key, never by position. Setdisabled: trueto suppress an element inherited from a lower layer. Positional merging would make inheritance depend on authoring order.
Built-in defaults act as the lowest layer, so a profile that states
matching.fields patches the defaults by key rather than replacing them. To drop
a default field, set disabled: true or weight: 0.
Use resolveConfigVerbose to get provenance — which layer set each section —
for an effective-configuration view.
Analytics
createAnalyticsRecorder buffers events and folds them into bucket-sharded
daily aggregates. Sharding is the point: one daily document would serialise every
write in the fleet behind a single version. summarize sums the buckets.
Await flush() in serverless handlers, or buffered events are lost on freeze.
Only aggregate counts are stored, never shopper identifiers.
Testing
npm test