Developer Introduction
What is ECS?
Bizzkit Ecommerce Search (ECS) is a multi-host search platform that ingests product, content, and behavioral data from external systems (PIM, CMS, Builder.io, Google Analytics, and more) and provides fast, AI-enhanced search capabilities to storefronts and back-office applications.
Think of ECS as the search brain of a Bizzkit-powered ecommerce site: it takes raw product data from PIM, editorial content from CMS and Builder.io, and commercial signals from analytics — then makes all of that searchable, filterable, and rankable in real time.
High-Level Data Flow
graph TD
subgraph Sources["External Data Sources"]
PIM
CMS
Builder["Builder.io"]
GA4["GA4 (Google)"]
BAIA["BAIA (AI)"]
ERP
end
subgraph Courier["Courier (Ingestion Hub)"]
CourierAPI["Courier API (on-demand)"]
CourierRunner["Courier Runner (scheduled)"]
end
subgraph ECSCore["ECS Core (Search Platform)"]
AdminAPI["Admin API (config, ingest)"]
SearchHost["Search Host (read-only queries)"]
JobRunner["Job Runner (index rebuilds)"]
subgraph Infra["Shared Infrastructure"]
ES["Elasticsearch (search index)"]
SQL["SQL Server"]
Redis["Redis (cache)"]
ASB["Azure Service Bus (messaging)"]
end
AdminAPI --> Infra
SearchHost --> Infra
JobRunner --> Infra
end
subgraph Consumers
Storefront["Storefront (React, Next.js)"]
AdminUI["Admin UI (ECS config)"]
OtherProducts["Other Bizzkit Products (via C#/TS SDKs)"]
CustomerSolution["Customer Solution"]
end
PIM --> Courier
CMS --> Courier
Builder --> Courier
GA4 --> Courier
BAIA --> Courier
ERP --> ECSCore
CourierAPI --> ECSCore
CourierRunner --> ECSCore
ECSCore --> Storefront
ECSCore --> AdminUI
ECSCore --> OtherProducts
ECSCore <--> CustomerSolution
Essential Concepts
Segments
A segment allows a customer to model their different markets, languages, and assortments as separate search configurations. Every piece of data — products, SKUs, configurations, business rules, synonyms — is scoped to a segment.
A segment typically represents a combination of market, language, and channel. For example:
| Segment ID | Meaning |
|---|---|
b2c-dk-en |
B2C site, Denmark, English |
b2b-se-sv |
B2B site, Sweden, Swedish |
When you call any ECS API, you specify which segment you're operating on via the segmentId parameter.
Products & SKUs
- Product — A logical grouping of variants (e.g., "Running Shoe Model X").
- SKU (Stock Keeping Unit) — The actual sellable item with a specific price, size, color, and stock status (e.g., "Running Shoe Model X, Black, Size 42").
A single product can have many SKUs. In search results, ECS returns products with their matching SKUs grouped together.
Fields
Fields are the data attributes available on products and SKUs. Each field has a type that determines whether it can be searched, filtered, or used as a facet.
| Field | Type | Searchable | Filterable / Facet |
|---|---|---|---|
| Product Name | Predefined | Yes | No |
| Product Number | Predefined | Yes | Filter only |
| Product ID | Predefined | Yes | Filter only |
| Short Description | Predefined | Yes | No |
| Long Description | Predefined | Yes | No |
| Alternative Search Words | Predefined | Yes | No |
| SKU Name | Predefined | Yes | No |
| SKU Number | Predefined | Yes | Filter only |
| SKU EAN | Predefined | Yes | Filter only |
| SKU ID | Predefined | Yes | Filter only |
| String Attribute | Dynamic (per SKU) | Yes | Yes |
| Number Attribute | Dynamic (per SKU) | No | Yes |
| Interval Attribute | Dynamic (per SKU) | No | Yes |
| Categories | Predefined | Yes | Yes |
| Category IDs | Predefined | No | Yes |
| Price | Predefined | No | Yes |
| Price Group | Predefined | No | No |
| In Stock | Predefined | No | Yes |
| Stock | Predefined | No | No |
| Searchable IDs | Predefined | No | Yes |
| Product Metadata | Dynamic (per SKU) | No | No |
| SKU Metadata | Dynamic (per SKU) | No | No |
| Media | Predefined | No | No |
| Rating | Predefined | No | No |
| Savings | Predefined | No | No |
- Searchable fields are matched against the user's search phrase during keyword search. Which fields are searchable is configurable per segment — the table above shows the defaults. Fields can also be marked as quick-searchable, meaning they are included in quick search (typeahead) queries — by default only Product Name and SKU Name.
- Filterable fields can be used in search request filters and can appear as facets. Which fields are exposed as allowed filters is controlled by publication configuration. Fields marked Filter only (Product Number, Product ID, SKU Number, SKU EAN, SKU ID) can be added as allowed filters for service-level filtering but cannot be used as customer-facing facets.
- Dynamic fields (String Attribute, Number Attribute, Interval Attribute) are created from the ingested product data — their names come from PIM attribute definitions.
Parameters
Parameters are inputs to the commercial sort scoring system. They are not searchable and not filterable — their sole purpose is to influence how products are ranked in search results.
A parameter takes a raw input value from either an external source (e.g., analytics data like sales count or click-through rate) or a field already ingested into ECS (e.g., a number attribute like rating). The input value is then normalized into a score between 0 and 1, which feeds into the commercial sort calculation.
The normalization strategy depends on the parameter type:
| Parameter Type | Normalization Strategy | Example Use Case |
|---|---|---|
| Term | Matches a string value against configured partitions. If the value matches a partition, the score is 1; otherwise 0. | Brand = "Premium" → boost |
| Range | Checks whether a numeric value falls within a configured range. If it does, the score is 1; otherwise 0. | Price between 100–500 → boost |
| Rank | Ranks all SKUs by a numeric value using interpolation across clusters, producing a continuous score between 0 and 1. | Sales count → top sellers ranked higher |
| AI Ranking | Similar to Rank but uses AI-generated signals for scoring. | AI relevance signal → boost |
Each parameter has partitions — named segments within the parameter that define how values map to commercial boost. For example, a Term parameter for "brand" might have partitions for "Premium" and "Economy", each contributing differently to the sort score.
Scopes
A scope defines a named configuration for the Search Host. It controls which fields are returned, which facets are shown, and default result counts. Critically, every scope has a scope type that tells ECS what the user's intent is.
Scope Types
Each scope is assigned a ScopeType that represents the user intent behind the request:
| Scope Type | Intent |
|---|---|
FullSearch |
User is on a search results page (typed a search phrase) |
Browse |
User is on a category or overview page (no search phrase) |
QuickSearch |
User is typing in the search box (quick search/suggestions) |
Details |
User is viewing a product detail page |
Basket |
User is on the basket/cart page |
Checkout |
User is on the checkout page |
OrderConfirmation |
User is on the order confirmation page |
Bands |
Request is for a page widget (e.g., recommendations, related products) |
MachineSearch |
Request is system-to-system, not triggered by an end user |
Why Scope Type Matters for Tracking
Using the correct scope and scope type is essential for tracking and analytics to work correctly. ECS's event tracking system uses the scope type to capture user intent and build analytics such as:
- Findability metrics — measuring whether a search (
FullSearch) led to a product view (Details) and eventually a purchase (OrderConfirmation). If the wrong scope type is used, ECS cannot connect these steps in the user journey. - Search funnel analysis — tracking the path from search → browse → details → checkout. Each step must use its corresponding scope type so the analytics pipeline can reconstruct the user's session.
- Popular search phrases — only requests with
FullSearchscope type are counted as real searches. If aBrowserequest is miscategorized asFullSearch, it inflates search phrase statistics.
Common mistakes:
| Mistake | Impact |
|---|---|
Using FullSearch for category pages |
Category views counted as searches, inflating search metrics |
Using Browse for actual search queries |
Real searches invisible in findability reports |
Using FullSearch for quick search/typeahead |
Every keystroke counted as a full search |
Not using Details for product page loads |
Findability cannot detect if a search led to a product view |
Rule of thumb: Match the scope type to the page the user is on, not to the type of data you're fetching.
Common Scope Configurations
| Scope ID | Scope Type | Use Case |
|---|---|---|
full-search |
FullSearch |
Search results page (products + content + suggestions) |
browse |
Browse |
Category browsing (no text query) |
quick-search |
QuickSearch |
Quick search/suggestions while typing |
details |
Details |
Product detail page (fetch related data) |
Publications
ECS uses a draft → publish workflow for configuration changes:
- Draft: Administrators configure business rules, synonyms, parameters, etc. in the Admin UI.
- Publish: A snapshot of all configuration is created and pushed live.
- Live: Search queries use the published snapshot. The previous publication remains available as a rollback target.
This means configuration changes are not applied immediately — they require an explicit publish step. This is important to understand when integrating, as changes to business rules or synonyms won't affect search results until they're published.
Targets
Targets are user-defined conditions that allow integrators to pass contextual information from the storefront into the search request. ECS uses targets as additional conditions when resolving business rules, making it possible to customize search behavior based on context that only the calling application knows about.
A target is defined in the Admin UI with an ID, a name, and a set of predefined values. For example:
| Target ID | Name | Values |
|---|---|---|
weather |
Weather | sunny, raining, snowing |
gender |
Gender | male, female |
campaign |
Campaign | holiday, summer-sale, black-friday |
When the storefront makes a search request, it can include targets in the targets field:
These target values are then used as conditions in business rule matching. A business rule conditioned on weather = raining would only be selected as the best fit when the search request includes that target value. This enables scenarios like:
- Weather-based merchandising — Boost rain jackets when the storefront knows it's raining at the user's location.
- Campaign-specific ranking — Show different pinned products or facets during a holiday campaign vs. a summer sale.
- Gender-targeted facets — Display different filter options depending on the audience segment.
Targets are entirely customer-defined — ECS does not prescribe what targets should exist. The storefront team decides which contextual dimensions are relevant and configures them in the Admin UI. The integrating application is then responsible for passing the appropriate target values in each search request.
Business Rules
Business rules are conditional configurations that modify search behavior based on context. Each business rule has a type that determines what aspect of the search it controls:
| Business Rule Type | What It Controls |
|---|---|
| Facets | Which facet filters appear in the search results (e.g., color, size, brand) |
| Parameter Set | Which parameters are used and how they are partitioned |
| Pinned Products | Specific products forced to the top of results in a defined order |
| Relevance Boost | Which fields are boosted and by how much (e.g., boost title over description) |
| Affinities | Personalization weights based on user affinity data |
| Semantic Search Boost | Whether AI vector embeddings are used for meaning-based matching |
Conditions and Best-Fitting Matching
Business rules support inheritance through a best-fitting matching algorithm. Each business rule can be configured with conditions — such as which scope is used, which category the user is browsing, whether a search phrase is present, or custom target values (e.g., device type, campaign).
When a search request comes in, ECS evaluates the conditions from the request against the available business rules independently for each business rule type. For each type, it finds the business rule whose conditions best match the current request:
- Candidate selection — ECS finds all business rules of a given type whose conditions are fully satisfied by the incoming request.
- Best fit — Among the candidates, the rule that matched the most conditions wins. This means a more specific rule (e.g., one conditioned on scope + category + phrase) will beat a less specific one (e.g., conditioned on scope only).
- Fallback — If no business rule's conditions are met, a fallback rule (one with no conditions) is used, if one exists. This acts as the default configuration.
This means you can set up a general default business rule for each type and then create more specific overrides for particular contexts — without duplicating the full configuration. For example:
| Rule Name | Type | Conditions | Effect |
|---|---|---|---|
| Default facets | Facets | (none — fallback) | Show color, size, brand, price |
| Shoes category facets | Facets | Category = "Shoes" | Show color, size, brand, price + shoe width |
| Default relevance | Relevance Boost | (none — fallback) | Standard field weights |
| Holiday campaign relevance | Relevance Boost | Target: campaign = "holiday" | Boost "gift" and "seasonal" fields |
In this example, a search request browsing the "Shoes" category would use the "Shoes category facets" rule for facets (more specific match) but the "Default relevance" rule for relevance boosting (no specific relevance rule for shoes). Each business rule type is resolved independently.
Affinities
Affinities are ECS's personalization mechanism. They allow search results to be boosted based on how well a product's attributes align with a user's preferences or an audience's profile. The core idea is simple: products are tagged with affinity values (e.g., "this product is 0.8 vegan"), and users or audiences are also tagged with affinity values (e.g., "this user is 0.6 interested in vegan"). When these overlap, the product gets a ranking boost.
Affinities are organized by data sources — each source represents a distinct origin of affinity data (e.g., Google Analytics, a recommendation engine, manually curated segments). A business rule of type Affinities controls which data sources are active and their relative weight.
Item Affinities
Every product/SKU in ECS can have item affinities — numerical values (0 to 1) representing how strongly the product is associated with specific attributes. These are ingested alongside product data, typically from PIM or a recommendation engine.
For example, a product might have:
| Attribute | Affinity Value |
|---|---|
vegan |
0.8 |
color_yellow |
0.4 |
sustainable |
0.9 |
User Affinities
User affinities represent an individual user's preferences. They are ingested per user (identified by userId) and map the same attributes to preference scores. User affinities are typically derived from behavioral data — what the user has browsed, purchased, or interacted with.
When a search request includes a userId, ECS looks up that user's affinities and computes a personalized boost for each product:
boost = item affinity × user affinity × weight
where the weight comes from the active Affinities business rule for the data source.
User affinities have a time-to-live (30 days) — they expire automatically, reflecting that user preferences change over time.
Audience Affinities
Audience affinities represent the preferences of a group of users rather than an individual. An audience is defined in the Admin UI with an ID (e.g., male, returning-customers, high-spenders) and a set of affinity values per data source — similar to user affinities, but shared across all members of the audience.
When a search request includes audienceIds, ECS applies the audience's affinity profile to boost results. This is useful when individual user-level data isn't available but you know which audience segment the user belongs to.
Audiences are flexible beyond demographic segments — they can also model physical locations. For example, an audience can represent a physical shop (e.g., shop-copenhagen), where the affinity values reflect the products that shop actually has in stock. A customer visiting the Copenhagen shop would have audienceIds: ["shop-copenhagen"] passed in their search requests, causing ECS to boost products available in that shop's storage. In this model, "audience" effectively means "the context of someone visiting this shop."
The scoring works the same way as user affinities:
boost = item affinity × audience affinity × weight
Both user affinities and audience affinities can be active simultaneously — their boosts are additive.
How Affinities Flow into Search
- Item affinities are ingested with product data and stored per SKU.
- User affinities are ingested per user via the Admin API and indexed in a dedicated user affinity index in Elasticsearch.
- Audience affinities are ingested via the Admin API.
- At search time, the active Affinities business rule determines which data sources are used and their weights.
- For each product in the result set, the affinity score is calculated by multiplying item affinities with user/audience affinities and the configured weight.
- The affinity boost is added to the product's commercial sort score, influencing the final ranking.
Facets & Filters
- Facets are the filter navigation you see on a search results page (e.g., "Color: Red (12), Blue (8), Green (3)"). ECS returns facets alongside search results so the UI can render filter options with counts.
- Filters are applied in search requests to narrow results. There are three types:
- String filter — Exact value match (e.g.,
"COLOR": {"values": ["Black"]}). - Range filter — Numeric range (e.g., price between 100 and 500).
- Price range filter — Specialized price filtering with currency awareness.
- String filter — Exact value match (e.g.,
Filters sent in a search request are validated against a published allowed filter list. Only filters matching an allowed field name are applied. This prevents callers from filtering on arbitrary internal fields.
Domain Filters
A domain filter is a filter that is applied to the Elasticsearch query as a hard constraint — it restricts which documents are considered without affecting facet counts. Domain filters come from three sources:
- Scope configuration — Filters configured directly on a scope (e.g.,
inStock: true), baked into the published configuration. These are always applied regardless of what the caller sends. - Authenticated filters — Filters extracted from a signed JWT token (see below).
- Regular filters on non-facet fields — When a caller sends a regular filter on a field that is not currently used as a facet, that filter is also treated as a domain filter.
The distinction between a domain filter and a facet filter matters because of how facet counts are calculated. When a filter targets a field that is also a facet, ECS must compute how the filter affects the facet values and their counts — the facet still needs to show the user which values are available and how many results each value would produce if selected. A domain filter, by contrast, simply restricts the result set without this facet-count calculation overhead.
For example, a scope for a B2B storefront might have a domain filter inStock: true so that out-of-stock products never appear, no matter what filters the caller includes. Because inStock is configured as a domain filter on the scope, it silently constrains results without appearing in the facet navigation.
How Authenticated Filters Become Domain Filters
ECS supports authenticated search via a signed JWT token passed in the authenticationToken field of the search request. This token is created server-side (using the Bizzkit.Sdk.Search.Authenticated SDK) and can contain filters embedded as claims.
When ECS receives a search request with an authentication token:
- The token is validated against the segment's signing keys.
- Filters from the token's claims are extracted as authenticated filters.
- Authenticated filters bypass the allowed filter list — they are not subject to the same allowlist validation as regular filters.
- Authenticated filters are merged with domain filters and regular filters to form the full set of domain filters applied to the Elasticsearch query.
This mechanism is used when a backend system needs to enforce filters that the frontend cannot tamper with. For example:
- A B2B portal issues a token with
priceGroup: "wholesale"so the customer only sees wholesale prices. - A customer-specific token restricts search results to a particular assortment or catalog subset.
The key difference between filter types:
| Filter Type | Source | Validated Against Allowlist | Treated As Domain Filter | Can User Override? |
|---|---|---|---|---|
| Regular filter (facet field) | Search request body | Yes | No (affects facet counts) | Yes |
| Regular filter (non-facet field) | Search request body | Yes | Yes | Yes |
| Scope domain filter | Scope configuration | No (preconfigured) | Yes | No |
| Authenticated filter | JWT token claims | No (trusted) | Yes | No |
Suggestions
Suggestions are typeahead entries shown as the user types in the search box, providing a fast path to relevant results. Suggestions are generated by a suggestion generator that you configure in the Admin UI.
The suggestion generator lets you select which fields are used to produce suggestion candidates. While it is possible to include product names, this often doesn't work well in practice — generating a unique suggestion for every product creates too many low-value entries. Instead, suggestions tend to work best when built from broader fields like categories and brands, which group many products under a single, high-value suggestion.
For example, typing "run" would suggest "Running shoes" (a category) or "Nike" (a brand) rather than listing individual product names like "Nike Air Zoom Pegasus 40 Black Size 42".
Language Dictionary
ECS models search language understanding through four separate dictionary concepts, each representing a distinct aspect of how natural language works. This is an intentional design choice — rather than mirroring how Elasticsearch technically handles text analysis, ECS models how a language actually works.
| Concept | What It Does | Example |
|---|---|---|
| Synonyms | Maps words with the same meaning so either term finds the same results | "car" ↔ "automobile" |
| Hypernyms | Maps specific terms to broader categories (is-a relationships) | "denim" → "pants", "espresso" → "coffee" |
| Irregular Words | Handles irregular forms that standard stemming cannot resolve | "mice" → "mouse", "children" → "child" |
| Misspellings | Maps known common misspellings to their correct form | "recieve" → "receive", "definately" → "definitely" |
These four dictionaries are independently managed — each serves a different linguistic purpose and is configured separately in the Admin UI. Together, they allow search to understand the nuances of language rather than relying solely on Elasticsearch's built-in text analysis.
Did You Mean
Did You Mean is a separate query-time feature that provides automatic spelling correction. If a search for "pnts" finds no results, ECS suggests "pants". Unlike the language dictionary entries, Did You Mean corrections are generated from the indexed product data rather than being manually curated.
Commercial Sort
Commercial sort allows ranking products based on commercial parameters like profit margin, popularity scores, or manually assigned weights — beyond pure text relevance. This is configured per segment and business rule.
Content Search
ECS doesn't just search products — it also indexes editorial content (articles, pages) from CMS and Builder.io. A unified search request can return both products and content in a single response, making it suitable for building search experiences that blend products with related editorial content.
How Keyword Search Works
Understanding the text analysis pipeline is important for troubleshooting why a search matches (or doesn't match) certain products. When product and content data is indexed, the text goes through a multi-step analysis pipeline that transforms raw text into searchable tokens. The same pipeline is applied to search queries at query time, ensuring that indexed text and query text are compared in the same normalized form.
The Text Analysis Pipeline
Why This Matters
- Order matters. The pipeline processes text sequentially — each step feeds into the next. For example, misspellings are corrected before stemming, so that "recieve" is first corrected to "receive" and then stemmed normally.
- The Language Dictionary concepts map directly to pipeline steps. The Synonyms, Hypernyms, Irregular Words, and Misspellings you configure in the Admin UI are applied at specific points in this pipeline. This is why they are modeled as separate concepts — they operate at different stages of text analysis.
- Both index-time and query-time. The same pipeline is used when indexing documents and when processing search queries, ensuring that the tokens generated from a query can match the tokens stored in the index.
- Troubleshooting searches. If a search term doesn't match as expected, use the diagnostics feature in the Admin UI — it shows the output of each pipeline step, so you can see exactly how a term was transformed at every stage. This makes it straightforward to identify whether a term was removed as a stop word (step 7), stemmed to an unexpected root (step 11), or missing a synonym mapping (step 14).
Diagnostics
The diagnostics feature is one of the most useful tools available to developers working with ECS. It is accessible from the Admin UI and provides deep visibility into how ECS processes search phrases and matches them against products. Instead of guessing why a search returns certain results (or no results), diagnostics lets you inspect every step of the process.
Analyze: Understand How a Search Phrase Is Processed
The Analyze tool takes a search phrase and runs it through the entire text analysis pipeline, returning the output of each step. For every pipeline step, you can see:
- Which tokens exist after the step has been applied.
- Whether a token was affected by the step (changed, expanded, or removed) or passed through unaffected.
- The final set of "must match" terms — the tokens that a product document must contain to be considered a match.
For example, analyzing the phrase "Skincare product with low allegies" would show:
| Step | Output | What Happened |
|---|---|---|
| HTML Stripping | Skincare product with low allegies |
No change (no HTML) |
| Tokenization | skincare, product, with, low, allegies |
Split into tokens |
| Lowercase | skincare, product, with, low, allegies |
All already lowercase after tokenization |
| Misspellings | skincare, product, with, low, allergies |
allegies corrected to allergies |
| Ignored Words | skincare, product, low, allergies |
with removed (stop word) |
| Language Stemmer | skincar, product, low, allergi |
Stemmed to root forms |
| Synonyms | skincar, product, low, allergi |
No synonyms configured |
| (final) | → Must match: skincar AND product AND low AND allergi |
These are the tokens used for matching |
This makes it immediately clear why a product might not be found — perhaps the misspelling wasn't in the dictionary, a term was unexpectedly removed as a stop word, or stemming produced an unexpected root form.
Match: Understand Why a Product Does or Doesn't Match
The Match tool goes a step further — given a search phrase and a specific product or SKU identifier, it shows:
- Field-level matching — For each searchable field on the product (name, description, category, etc.), it highlights which parts of the field value matched the search tokens and which didn't. This lets you see exactly where in the product data a match was found (or why it was missed).
- Term-level matching — For each token that the search query was analyzed into, it shows whether that token was found in the product. If any required token is missing, that explains why the product doesn't appear in results.
- Filter exclusion — If filters were applied, it shows whether the product was excluded by a filter rather than by text matching. This distinguishes between "the product doesn't match the search phrase" and "the product matches but was filtered out."
When to Use Diagnostics
| Scenario | Which Tool | What to Look For |
|---|---|---|
| A search phrase returns no results | Analyze | Check if important tokens are being removed as stop words or stemmed unexpectedly |
| A specific product doesn't appear in results | Match | Check which terms matched and which didn't — is a field missing data? |
| A product appears when it shouldn't | Match | Check which fields are producing unexpected matches |
| A synonym or misspelling correction isn't working | Analyze | Check the specific pipeline step to see if the dictionary entry was applied |
| Results changed after a language dictionary update | Analyze | Compare the pipeline output before and after to see how tokens changed |
| A product shows in search but not when filters applied | Match | Check if the filter exclusion flag indicates filter-based removal |
Playground
The Playground is an interactive tool in the Admin UI that lets both developers and daily users see exactly which business rules are in effect for a given search context and how they shape the search results. Where diagnostics focuses on text analysis and token matching, the Playground focuses on business rule resolution and result ranking.
What the Playground Shows
When you open the Playground, you define a set of conditions — the same conditions that a real search request would carry (scope, category, search phrase, targets, etc.). The Playground then:
- Resolves business rules — For each business rule type (Facets, Parameter Set, Pinned Products, Relevance Boost, Affinities, Semantic Search Boost), it shows which specific rule was selected as the best fit and whether the match is exact or a fallback. This makes the inheritance model fully transparent.
- Previews search results — Executes a search with the resolved business rules applied, showing the actual product results in a grid view.
- Explains scoring — For each product in the results, it breaks down the commercial sort score: which parameters contributed, what the normalized values were, and how the partition weights affected the final ranking.
- Shows affinity influence — If personalization (affinities) is active, it shows how user affinity data affected the result ordering.
Why the Playground Matters
The Playground bridges the gap between configuration and outcome. Without it, understanding why a particular search returns certain results in a specific order requires mental simulation of the best-fitting algorithm across multiple business rule types. With the Playground, you can:
- Verify business rule inheritance — Confirm that a more specific rule (e.g., for a particular category + scope) correctly overrides the default fallback rule.
- Test before publishing — The Playground works with draft configuration, so you can preview how upcoming changes will affect search results before publishing them live.
- Debug unexpected ranking — If products appear in an unexpected order, the score explanation reveals exactly which parameters and weights produced that ranking.
- Collaborate with non-developers — Product managers and merchandisers can use the Playground to validate that their business rule changes produce the intended results, without needing developer assistance.
Architecture Overview
Deployable Hosts
ECS is composed of five deployable services split across two subsystems:
| Host | Subsystem | Purpose |
|---|---|---|
| Admin API | ECS Core | Configuration, ingestion endpoints, business rules, publication management |
| Search Host | ECS Core | Read-only search queries, facets, suggestions, SKU export (optimized for high query throughput) |
| Job Runner | ECS Core | Background jobs: index rebuilds, cleanup, suggestion generation |
| Courier API | Courier | Data ingestion hub — coordinates data intake from PIM, CMS, Builder.io, GA4, BAIA |
| Courier Runner | Courier | Background scheduled ingestion tasks (daily syncs) |
graph LR
subgraph ECSCore["ECS Core"]
AdminAPI["Admin API (config, ingest, business rules, publish)"]
SearchHost["Search Host (search, facets, suggestions, SKU export)"]
JobRunner["Job Runner (index rebuild, cleanup, suggestion gen)"]
end
subgraph Courier["Courier"]
CourierAPI["Courier API (webhook receiver, on-demand ingest)"]
CourierRunner["Courier Runner (scheduled daily sync jobs)"]
end
Technology Stack
| Technology | Role |
|---|---|
| Elasticsearch | Primary search index (products, content, suggestions, spelling dictionaries) |
| SQL Server | Relational store for ingested data, configurations, business rules (EF Core) |
| Redis | Multi-layer cache for published configurations and frequently accessed data |
| Azure Service Bus | Asynchronous messaging for event-driven processing |
Integrating with ECS
Which API Do I Need?
| I want to... | Use this API | Authentication |
|---|---|---|
| Search for products/content on a storefront | Search Host | Anonymous or JWT |
| Export SKUs for data processing | Search Host | Anonymous or JWT |
| Configure segments, business rules, synonyms | Admin API | OAuth2 (token) |
| Push product/content data into ECS | Admin API | OAuth2 (token) |
| Coordinate ingestion from PIM/CMS/Builder.io/GA4 | Courier API | OAuth2 (token) |
Available SDKs
ECS provides generated SDK clients so you don't need to call the REST APIs directly.
C# SDKs
| NuGet Package | Interface / Factory | Purpose |
|---|---|---|
Bizzkit.Sdk.Search |
ISearchClient / SearchClientFactory |
Search queries (unauthenticated) |
Bizzkit.Sdk.Search.Preview |
ISearchClient / SearchClientFactory |
Preview API features |
Bizzkit.Sdk.EcommerceSearch |
ISearchAdminstrationClient / SearchAdminstrationClientFactory |
Admin operations (OAuth2) |
Bizzkit.Sdk.EcommerceSearch.Courier |
ICourierClient / CourierClientFactory |
Courier ingestion (OAuth2) |
TypeScript SDKs
| Import Path | Purpose |
|---|---|
frontend/lib/sdk/ecs-search/v25/ |
Search Host client (read-only) |
frontend/lib/sdk/ecs-admin/v25/ |
Admin API client |
npm Package
The @bizzkit/ecommerce-search npm package (from ecommercesearch/frontend-search-client/) provides a standalone TypeScript client for building storefront search UIs. It exports:
Client— Search Host clientPreviewClient— Preview Search Host clientAdmin/PreviewAdmin— Admin API types
Authentication
- Search Host — Can be called without authentication (anonymous) for public storefronts. Optionally, a JWT
authenticationTokensigned with aSearchSigningKeycan be passed in the request body to unlock restricted features (e.g., authenticated filters, debug explanations). - Admin API & Courier API — Require OAuth2 bearer tokens. The C# SDKs handle token management automatically via the factory pattern (
SearchAdminstrationClientFactory,CourierClientFactory).
The Unified Search Request
The unified search is the central concept of the Search Host. Rather than making separate calls for products, content, suggestions, and facets, the unified search endpoint (POST /search) returns all result types in a single response. This is the endpoint your storefront should call for virtually every search-related page.
Why "Unified"?
In a typical search UI, a single user action (typing a query, clicking a category) requires multiple types of data:
- Products with their matching SKUs
- Content (editorial articles, pages)
- Suggestions (typeahead completions)
- Did You Mean corrections (if the phrase didn't match well)
- Facets (filter options with counts)
- Related Tags (related search phrases)
- Popular Searches (shown when the search box is empty)
The unified search request fetches all of these in one round trip. What gets returned is controlled by the scope — each scope defines how many products, content items, suggestions, did-you-mean alternatives, and related tags to return. This means the same endpoint serves both a full search results page and a quick search dropdown — the scope configuration determines the response shape.
Request Flow
When the Search Host receives a unified search request, it goes through these steps:
- Scope resolution — The published scope configuration is loaded based on
segmentId+scopeId. This determines default result counts, domain filters, fields to return, and scope type. - Authentication — If an
authenticationTokenis present, it's validated and its claims are extracted as authenticated filters. - Business rule matching — The request context (scope, category, phrase, targets) is evaluated against all configured business rules to find the best-fitting rule for each type (facets, relevance boost, parameter set, etc.).
- Parallel sub-searches — ECS executes multiple searches in parallel against Elasticsearch:
- Product search (with commercial sort scoring, filters, facets)
- Content search (with relevance boosting)
- Suggestion search
- Did You Mean search
- Related tag search
- Response assembly — All results are combined into a single unified response.
Response Structure
The unified search response contains:
| Response Field | Description |
|---|---|
products |
Matched products, each with their nested SKUs and field values |
totalProducts |
Total product count matching the query (approximate above 3,000) |
totalSkus |
Total SKU count (if enabled on scope via countTotalSkus) |
facets |
Filter options with counts, ordered as configured in the matching Facets business rule |
suggestions |
Typeahead suggestions matching the phrase |
didYouMean |
Spelling correction alternatives |
relatedTags |
Related search phrases derived from the query |
aiRelatedTags |
AI-generated related search phrases |
content |
Matched editorial content items from CMS |
popularSearches |
Popular search phrases (only when phrase is empty and scope has this enabled) |
originalPhrase |
The phrase as submitted by the caller |
usedPhrase |
The phrase actually used for search (may differ if Did You Mean auto-corrected it) |
explanation |
Score breakdown per product (only with authenticated search + explain enabled) |
action |
Redirect action (e.g., redirect to a specific category page instead of showing results) |
How Scope Controls the Response
The scope determines what gets returned. If a scope has numberOfSuggestions: 0, no suggestions are fetched or returned — the sub-search is skipped entirely. This is why different scopes exist for different pages:
| Page | Scope Type | Products | Content | Suggestions | Did You Mean | Facets |
|---|---|---|---|---|---|---|
| Search results | FullSearch |
12 | 5 | 5 | 3 | Yes |
| Category browse | Browse |
24 | 0 | 0 | 0 | Yes |
| Quick search | QuickSearch |
5 | 3 | 5 | 0 | No |
| Product detail | Details |
1 | 0 | 0 | 0 | No |
Numbers are examples — they are configurable per scope.
Making a Search Request
The primary search endpoint is:
Minimal Request
Only segmentId and scopeId are required:
Full Request Example
| Field | Required | Description |
|---|---|---|
segmentId |
Yes | Which segment to search in (e.g., "b2c-dk-en") |
scopeId |
Yes | Which scope configuration to use (e.g., "full-search", "browse") |
phrase |
No | The search query text (max 255 chars). Omit for browse/category pages |
numberOfProducts |
No | How many products to return (max 1000, default set on scope) |
numberOfContent |
No | How many content items to return (max 1000) |
offsetProducts |
No | Number of products to skip (for pagination) |
sort |
No | Sorting criteria — first element wins (default: relevance desc) |
filters |
No | Key-value filter criteria (string or range filters) |
userId |
No | User ID for personalized boosting (affinity-based) |
pinnedIds |
No | Product/SKU IDs to pin at the top of results (max 200) |
targets |
No | Context values for business rule matching |
forceSearch |
No | Skip search optimizations like did-you-mean redirect |
Exporting SKUs
For bulk data export (e.g., feed generation), use the SKU export endpoint:
This endpoint supports cursor-based pagination using an opaque pageToken. Request the first page without a token, then use the pageToken from the response to fetch the next page. Continue until no more pages are returned.
Courier Ingestion Flow
Understanding how data gets into ECS via the courier is important for integration planning:
graph TD
PIM -- "Webhook (real-time)" --> Courier
CMS -- "Webhook / Daily" --> Courier
Builder["Builder.io"] -- "Webhook / Daily" --> Courier
GA4 -- "Daily sync" --> Courier
BAIA -- "Daily sync" --> Courier
subgraph Courier["Courier"]
CourierAPI["Courier API (on-demand / webhook triggers)"]
CourierRunner["Courier Runner (scheduled daily sync jobs)"]
end
Courier -- "SDK calls (Ingest API)" --> AdminAPI["ECS Core (Admin API)"]
How It Works
- Courier connects to data sources — PIM (product catalog), CMS (content pages), Builder.io (content pages), GA4 (analytics), BAIA (AI/image data).
- Data is transformed — The Courier maps external data models into ECS's ingestion format.
- Ingested via Admin API — The Courier uses the ECS SDK (
ISearchAdminstrationClientFactory) to push data into ECS Core. - Event chain triggers indexing — When product data is ingested, it triggers a chain of internal update events
- Elasticsearch index is updated — The Job Runner processes the rebuild messages and updates the Elasticsearch index.
- Search Host serves fresh data — Once indexed, the data is immediately available via the Search Host.
Why This Matters for Integrators
- Data is eventually consistent. After a product is updated in PIM, there is a short delay (seconds to minutes) before it appears in search results, because the data must flow through the Courier → Ingest → Index pipeline.
- You typically don't need to call the Ingest API directly. The Courier handles standard PIM/CMS data ingestion automatically. Direct Ingest API usage is only needed for custom data sources not covered by the Courier.
Performance Considerations
When integrating with ECS, keep these principles in mind to ensure a fast and reliable search experience.
Search Host Is Read-Optimized
The Search Host is designed for high query throughput — it's the endpoint your storefront will call on every search, every category page load, and every keystroke during quick search. It has higher concurrency limits than the Admin or Courier APIs.
The Admin API has lower concurrency limits because it handles configuration and ingestion — heavier, less frequent operations.
Use Cursor-Based Pagination for Exports
For the SKU export endpoint or any large result set, always use the cursor-based pageToken pattern instead of offset-based pagination with large offsets.
Why? Deep offset pagination (e.g., offsetProducts: 50000) becomes increasingly expensive in Elasticsearch because the system must count and skip all preceding results.
| Approach | Performance at Page 1 | Performance at Page 500 |
|---|---|---|
Offset (offsetProducts) |
Fast | Very slow |
Cursor (pageToken) |
Fast | Still fast |
Use Bulk Operations for Ingestion
If you're pushing data into ECS via the Admin API, use the bulk endpoints rather than making individual calls per product or SKU. Sending 10,000 individual API calls is orders of magnitude slower (and more likely to hit rate limits) than sending batches.
Configure Scopes to Fetch Only What You Need
Scopes control which fields are returned in search results. Setting up a scope to return only the fields your UI actually needs has a direct impact on performance — less data means faster serialization, smaller payloads, and reduced Elasticsearch _source filtering overhead.
Additionally, fetching SKU data requires additional lookups beyond the base product query. If your scope doesn't need SKU-level details (e.g., a quick search dropdown that only shows product names), configure the scope to not include underlying SKUs. This avoids the extra work of resolving and returning SKU data for every product in the result set.
| Scope Purpose | Fields Needed | Include SKUs? | Why |
|---|---|---|---|
| Quick search | Product name, image | No | Only showing quick suggestions — SKUs are unnecessary overhead |
| Search results page | Name, price, image, color | Yes (limited) | Need representative SKU data for display |
| Category browse | Name, price, image | Yes (limited) | Similar to search results |
| Product detail page | All fields | Yes (all) | Full product information required |
Takeaway: Don't use a single "fetch everything" scope for all use cases. Create purpose-built scopes that match the data requirements of each page or component.
Only Ingest What Has Changed
A common bad practice is re-sending the entire product catalog on every sync, regardless of whether anything has actually changed. This wastes bandwidth and puts unnecessary load on the Ingest API.
ECS does have its own built-in change tracking — when data is ingested, ECS compares the incoming data against what's already stored and will not forward unchanged data to Elasticsearch for reindexing. So redundant ingestion won't cause redundant index rebuilds. However, this doesn't eliminate the cost of the ingestion call itself: the data still needs to be serialized, sent over the network, received, deserialized, and compared. At scale, this adds up.
Integrating solutions should track their own changes and only push data that has actually been modified since the last successful ingestion. This applies to all data types: products, SKUs, content, user affinities, and item affinities.
| Approach | Bandwidth | Ingest API Load | Elasticsearch Indexing |
|---|---|---|---|
| Full catalog on every sync | High | High | Only actual changes (ECS filters) |
| Only changed items | Minimal | Minimal | Only actual changes |
How to implement this:
- Use webhooks where available. PIM already provides real-time webhooks that fire only when a product changes — the Courier uses these by default. If you're building a custom integration, prefer webhooks or change feeds over full exports.
- Maintain a change timestamp. Before each sync, record the current timestamp. On the next sync, only query for items modified after that timestamp.
- Compare before sending. If your data source doesn't support change tracking, compute a hash or version of each item and only send items whose hash differs from the last ingestion.