Zen Electron aims to provide a powerful and intuitive search experience for its customers. Their key business requirements for the search results page are:
- Full-text Search: Customers should be able to search across product 
nameanddescriptionto find relevant items. - Comprehensive Sorting: Results should be sortable by:
- Relevance (
score) - Newest products (
createdAtdescending) - Oldest products (
createdAtascending) - Highest Price (
variants.prices.centAmountdescending) - Lowest Price (
variants.prices.centAmountascending) 
 - Relevance (
 - Faceted Navigation: Customers need to refine search results using facets for:
Brand(for example, "Sony", "Samsung")Price(for example, "$0-$50", "$51-$100")Category(for example, "Laptops", "Smartphones")
 - Store-Specific Results: As Zen Electron operates with multiple Stores, search results must be filtered to show only products available in the customer's current store.
 - Tailored Product Data & Pricing:
- Product information (like names, descriptions) should be localized based on the store's configured locales.
 - Prices displayed must be the best-matching prices for the customer's context (currency, country, customer group, channel), derived from the store's settings.
 - The API response should return reduced product fields (Product Projections) relevant to the store's locale and pricing context to optimize performance.
 
 - URL Parameter Integration: The search state (query, sort, filters) should be reflectable in and controllable via URL parameters for shareability and bookmarking.
 
Mapping requirements to Product Search API queries
Let's break down how each requirement translates into an API query.
1. Base query structure and store filtering
stores field in the main query object. The storeKey would typically be derived from the user's session or the domain they are accessing.productProjectionParameters to ensure we get localized product data and the correct prices based on the store context.storeProjection: storeKey: This parameter ensures that product data (like names, descriptions) is returned in the locales configured for the specifiedstoreKey. It also influences which attributes and prices might be considered "visible" or "relevant" for that store.- Price Selection Parameters (
priceCurrency,priceCountry,priceCustomerGroup,priceChannel): These are crucial for getting the correct price. For Zen Electron, these values would often be derived from thestoreKey's configuration (for example, a store for "AU" would havepriceCurrency: 'AUD'andpriceCountry: 'AU'). If a customer is part of a specific customer group or if prices are channel-specific, those IDs would also be passed. 
{
  "query": {
    "filter": [
      {
        "exact": {
          "field": "stores",
          "value": "0ed7d63b-0b10-4688-91b8-2fb28e9a957e" // Store ID
        }
      }
    ]
  },
  "productProjectionParameters": {
    "staged": false, // Show current product data
    "storeProjection": "zen-electron-store-key",
    "priceCurrency": "AUD", // Example, derived from store or user context
    "priceCountry": "AU", // Example, derived from store or user context
    "priceCustomerGroup": "optional-customer-group-id", // If applicable
    "priceChannel": "optional-channel-id" // If applicable
  },
  "limit": 20,
  "offset": 0
  // Sorting and facets will be added here
}
- The 
query.filterensures results are from the specified store. productProjectionParameters.storeProjectiontailors localized attributes (name, description) based on the store's languages.productProjectionParametersfor price selection ensure themasterVariant.priceorvariant.pricein the returned Product Projection reflects the most relevant price.- By using 
productProjectionParameters, theresultsarray in the response will contain enrichedProductProjectionobjects, not just IDs. This reduces the need for subsequent API calls to fetch full product details, but leads to higher response times. 
2. Full-text search
name and description. You achieve this by using the fullText simple expression. We'll combine these with an or compound expression if a match in either field is desired. The language for fullText should match one of the store's locales.// Adding full-text search to the query object
{
  "query": {
   "and": [ // Combines store filter with full-text search
      {
       "exact": { // Store filter remains
        "field": "stores",
        "value": "your-zen-electron-store-id"
       },
    },
    { // Full-text search part
     "or": [ // Match in name OR description
      {
       "fullText": {
        "field": "name",
        "language": "en-AU",
        "value": "user search term",
        "caseInsensitive": true
       }
      },
      {
       "fullText": {
        "field": "description",
        "language": "en-AU",
        "value": "user search term",
        "caseInsensitive": true
       }
      }
     ]
    }
   ]
  }
}
- The main 
queryis now anandto combine the mandatory store filter with the user's full-text search. - The full-text part uses an 
orso products matching the "user search term" in eithernameordescription(for the specifiedlanguage) are returned. caseInsensitive: trueis generally a good practice for user-facing search.- The 
language(for example, "en-AU") should be dynamically set based on the active store/locale. 
3. Sorting
sort parameter in the ProductSearchRequest takes an array of SearchSorting objects.// Example "sort" array for different options.
"sort": [
  // By Relevance (Score) - default if no sort is provided, or explicitly:
  // { "field": "score", "order": "desc" }
  // By Newest (createdAt descending)
  // { "field": "createdAt", "order": "desc" }
  // By Oldest (createdAt ascending)
  // { "field": "createdAt", "order": "asc" }
  // By Highest Price (variants.prices.centAmount descending)
  // { "field": "variants.prices.centAmount", "order": "desc", "mode": "max" }
  // By Lowest Price (variants.prices.centAmount ascending)
  { "field": "variants.prices.centAmount", "order": "asc", "mode": "min" }
]
Only pass one of the options at a time, based on what the customer has selected. For example the default sort would be:
"sort": [
  { "field": "score", "order": "desc" }
]
And if they selected highest to lowest price, it would be:
"sort": [
  { "field": "variants.prices.centAmount", "order": "desc", "mode": "max" }
]
Let’s look at the default sort score in the context of the whole query that we have built so far.
"query": {
  "and": [ // Combines store filter with full-text search
    {
      "filter": [ // Store filter remains
        {
          "exact": {
            "field": "stores",
            "value": "your-zen-electron-store-key"
          }
        }
      ]
    },
    { // Full-text search part
      "or": [ // Match in name OR description
        {
          "fullText": {
            "field": "name",
            "language": "en-AU",
            "value": "user search term",
            "caseInsensitive": true
          }
        },
        {
          "fullText": {
            "field": "description",
            "language": "en-AU",
            "value": "user search term",
            "caseInsensitive": true
          }
        }
      ]
    }
  ],
"sort": [
  { "field": "score", "order": "desc", "mode": "max" }
]
}
score: Sorts by relevance, typically used only with full-text search.createdAt: Sorts by product creation date.variants.prices.centAmount: Sorts by price.mode: "min"is used for "lowest price" to consider the minimum price among variants if a product has multiple.mode: "max"is used for "highest price" to consider the maximum price among variants.
- The specific sort object would be chosen based on user selection from the frontend.
 
Faceting
facets array in the ProductSearchRequest.// "facets" array for Brand, Price, and Category
"facets": [
  { // Brand Facet (Distinct Facet on a text attribute)
    "distinct": {
      "name": "brandFacet",
      "field": "variants.attributes.brand.key", // Assuming 'brand' is an enum
      "fieldType": "enum",
      "limit": 10, // Show top 10 brands
      "missing": "N/A" // Bucket for products without a brand
    }
  },
  { // Price Facet (Ranges Facet on a number field)
    "ranges": {
      "name": "priceFacet",
      "field": "variants.prices.centAmount", // Price in cents
      "ranges": [
        { "key": "0-50", "to": 5000 },       // Up to $49.99
        { "key": "50-100", "from": 5000, "to": 10000 }, // $50.00 to $99.99
        { "key": "100-200", "from": 10000, "to": 20000 },// $100.00 to $199.99
        { "key": "200-plus", "from": 20000 } // $200.00 and above
      ]
    }
  },
  { // Category Facet (Distinct Facet on a keyword field)
    "distinct": {
      "name": "categoryFacet",
      "field": "categories", // Faceting on category IDs
      "limit": 20,
      "missing": "No Category"
    }
  }
]
brandFacet: Adistinctfacet onvariants.attributes.brand.key. ThefieldTypemust match the attribute type (for example,enum,text).limitcontrols how many brand values are returned at maximum.priceFacet: Arangesfacet onvariants.prices.centAmount. Each object inrangesdefines a price bucket. Thekeyis a user-friendly name for the UI.fromis inclusive,tois exclusive. Values are in cents.categoryFacet: Adistinctfacet oncategories.id. This will return category IDs and their counts. The frontend/BFF would need to map these IDs to category names.
facets array with results for each requested facet, showing keys and counts. Example Facet Result Snippet:"facets": [
  {
    "name": "brandFacet",
    "buckets": [
      { "key": "Sony", "count": 25 },
      { "key": "Samsung", "count": 18 }
    ]
  },
  {
    "name": "priceFacet",
    "buckets": [
      { "key": "0-50", "count": 15 },
      { "key": "50-100", "count": 30 }
    ]
  }
  // ... categoryFacet results
]
Facet filtering (AND vs OR Logic using postFilter)
postFilter.postFilter applies to the results of the main query after facets have been calculated.// Example "postFilter" when user selects Brand "Sony" AND Price Range "$50-$100"
"postFilter": {
  "and": [
    { // Brand filter
      "exact": {
        "field": "variants.attributes.brand.key",
        "fieldType": "enum",
        "value": "Sony"
      }
    },
    { // Price range filter
      "range": {
        "field": "variants.prices.centAmount",
        "gte": 5000, // Greater than or equal to 5000 cents
        "lt": 10000  // Less than 10000 cents
      }
    }
  ]
}
- 
AND logic (between different facets): When a user selects values from different facets (for example, Brand "Sony" AND Price "$50-$100"), these conditions are combined with an
andoperator within thepostFilteras shown above. The results will only include products that are "Sony" AND fall within the "$50-$100" price range. - 
OR logic (within the same facet): If Zen Electron allows selecting multiple values within the same facet (for example, Brand "Sony" OR Brand "Samsung"), this is also handled in the
postFilter. 
// Example "postFilter" for Brand "Sony" OR Brand "Samsung"
"postFilter": {
  "or": [
    {
      "exact": {
        "field": "variants.attributes.brand.key",
        "fieldType": "enum",
        "value": "Sony"
      }
    },
    {
      "exact": {
        "field": "variants.attributes.brand.key",
        "fieldType": "enum",
        "value": "Samsung"
      }
    }
  ]
}
postFilter would become a nested structure:// Brand ("Sony" OR "Samsung") AND Price Range ("$50-$100")
"postFilter": {
  "and": [
    { // OR condition for brands
      "or": [
        { "exact": { "field": "variants.attributes.brand.key", "fieldType": "enum", "value": "Sony" } },
        { "exact": { "field": "variants.attributes.brand.key", "fieldType": "enum", "value": "Samsung" } }
      ]
    },
    { // Price range filter
      "range": { "field": "variants.prices.centAmount", "gte": 5000, "lt": 10000 }
    }
  ]
}
URL parameters integration
zen-electron.com/en-AU/search?q=laptop&sort=price_asc&brand=Dell&price=500-1000&category=electronics-cat-idThe BFF would parse these parameters:
q=laptop: Populates thevaluein thefullTextexpressions.sort=price_asc: Determines thesortobject (for example,{ "field": "variants.prices.centAmount", "order": "asc", "mode": "min" }).brand=Dell: Adds anexactfilter forvariants.attributes.brand.keywith value "Dell" to thepostFilter. If multiple brands are selected (for example,brand=Dell,Sony), they'd form anorcondition.price=500-1000: Adds arangefilter forvariants.prices.centAmount(for example,gte: 50000, lt: 100000) to thepostFilter.category=electronics-cat-id: Filter forcategories.idwith the value "electronics-cat-id" to thepostFilter. Using the category ID is very convenient for building the query, but might be less ideal for SEO or for the customer to read it. So you will most likely want to use the category name. In this case, you will need to have a means of mapping the name to ID. We have looked at a similar scenario earlier in the module.
The BFF constructs the complete JSON request body based on these URL parameters and sends it to the Product Search API. When facet options or sort orders are changed on the frontend, the URL is updated and a new API request is made via the BFF.
This approach ensures that search states are bookmarkable, shareable, and support browser back/forward navigation.
Cache search results for enhanced user experience
To further improve performance and user experience, Zen Electron should implement a caching strategy for its search/ listing page. Frequent or common search queries, especially those combined with popular filter sets, can benefit significantly from caching.
How Zen Electron can implement caching
- BFF layer caching: The Backend For Frontend (BFF) is an ideal place to implement caching. When the BFF receives a request derived from URL parameters, it can first check a cache (for example, Redis, Memcached) for a stored response corresponding to that exact query (including search term, filters, sort order, pagination, and store context).
- If a valid cached response exists, it's returned directly to the frontend, bypassing the Composable Commerce API.
 - If not, the BFF queries the Product Search API, stores the response in the cache with an appropriate Time-To-Live (TTL), and then returns it to the frontend.
 
 - CDN caching (for highly common, less dynamic queries): For very popular, non-personalized search landing pages (for example, a search for "best sellers" without user-specific filters), responses could potentially be cached at the CDN level for even faster delivery. This is more applicable if the results don't change frequently.
 
Why caching benefits Zen Electron
- Improved Perceived Performance & UX: Cached responses are served much faster, leading to quicker page load times for users, especially for subsequent visits or common searches. This significantly enhances the user experience.
 
Cache invalidation strategy
Implementing a cache invalidation strategy is crucial for Zen Electron. This could be a simple Time-To-Live (TTL)-based, where entries automatically expire after a set period (for example, 5-15 minutes).
Caching is most effective when there is a high cache hit ratio. Frequently accessed category pages are likely to achieve high hit ratios. Highly specific queries, such as full-text searches or results refined by many user-selected filters, will have lower hit ratios.
By integrating these Product Search API features with a well-considered caching strategy, Zen Electron can develop a robust, performant, and user-friendly search results page, ensuring it meets business needs and provides an optimal user experience