Welcome to the third episode of Building Ecommerce. This trilogy of videos will walk developers through building ecommerce search solutions in Coveo. If you missed the first two videos, the Part 1: Product Catalogs, Variants, and Groups is here and the Part 2: Configuring the Search Platform is here.
Now that your index is set up and your platform is configured, it’s time to build the user interface (UI). In our scenario, we are using a React frontend, supported by a Headless framework.
But before we can build, we must design the final pages.
Page Design
Our ecommerce search UI consists of multiple pages. In our scenario, the generic ecommerce store demo also powers product detail pages and listing pages. Those are normally powered by your own ecommerce search implementation.
Each page will have dedicated product recommendations, powered by our ML models. For each page, we’re using Query Suggestions and Automatic Relevance Tuning (ART) ML models.
Here’s a list of our pages, their ML models, and their content:
- Landing Page
- Search Results Page
- Product Detail Page
- Product Listing Page
- Cart Page
(* Are not implemented in the generic ecommerce store demo)
There are a lot of recommendation models available. You can embed them in the same way as we are doing in the generic ecommerce store.
If you are using the Headless framework or if you are using the Search API directly, both require you to send the proper analytics events.
React and Headless Frameworks
Using React to build pages is quite common nowadays. Developers have total control over the rendering and can create the components they need.
This is exactly what we did when we built the ecommerce generic store demo. The Headless framework helps us by providing actions and controllers for communication between components and our back end.
Configure Your Search Engine
To use the Headless framework, you first need to define a search engine. The engine is then used for the communication between our components and the Search API.
In the example below, you will also notice that we added the product catalog fields that we want to retrieve.
const EC_STANDARD_FIELDS = ['ec_brand', 'ec_category', 'ec_cogs', 'ec_description','ec_image', 'ec_images', 'ec_in_stock', 'ec_item_group_id', 'ec_name', 'ec_parent_id', 'ec_price', 'ec_product_id', 'ec_promo_price', 'ec_rating', 'ec_shortdesc', 'ec_skus', 'ec_thumbnails', 'ec_variant_sku', 'permanentid', 'urihash']; //We want to register the fields to return from the Search API const registerFields = (engine: SearchEngine | ProductRecommendationEngine) => { const fields = getFieldsFromConfig(); const fieldActions = loadFieldActions(engine); engine.dispatch(fieldActions.registerFieldsToInclude(fields)); return engine; }; //We build the config const buildConfig = (pipeline, searchHub, analyticsEnabled: boolean = true): SearchEngineOptions => ({ configuration: { organizationId: process.env.ORG_ID, accessToken: process.env.API_KEY, platformUrl: process.env.PLATFORM_URL || undefined, analytics: { enabled: analyticsEnabled, originLevel2: publicRuntimeConfig.searchTab }, search: { pipeline, searchHub, } } }); //Define the Engine export const headlessEngine = registerFields( buildSearchEngine(buildConfig(process.env.SEARCH_PIPELINE, 'MainSearch')) ) as SearchEngine;
Now that we’ve defined our search engine, we can use it in our different components.
Analytics
Although the Headless framework handles many analytic requests inherently, an ecommerce search implementation often requires additional metrics. Because the UI needs to send specific events to the Analytics API, we use the CoveoUA script. In our code, we are using the ‘CoveoAnalytics.ts’ script.
For example, here’s the ‘addToCart’ action:
const addToCart = (products: AnalyticsProductData[] | AnalyticsProductData) => { products = Array.isArray(products) ? products : [products]; products.forEach(product => { coveoua('ec:addProduct', product); }); coveoua('ec:setAction', 'add'); coveoua('send', 'event'); };
The above will make sure to add all of the products to the ‘CoveoUA’ event, set a specific action, and, finally, use the ‘Send’ to submit the request. More info here.
Define Your Machine Learning Models
Our ML models need more configuration, so we specify the searchHub to use. Every instance of an ecommerce search product recommendation requires defining a searchHub.
In the below example we also added a check to ensure that when the analytics are sent, no ‘searchQueryUid’ is passed in the event data, as it will be fetched from sessionStorage.
The Headless framework offers middleware calls to augment the requests sent.
// Recommendations Engine export const headlessEngine_Recommendations = (searchHub): ProductRecommendationEngine => { const config = buildConfig(process.env.SEARCH_PIPELINE, searchHub) as any; delete config.configuration.search; config.configuration.searchHub = searchHub; config.configuration.analytics.analyticsClientMiddleware = (eventName, eventData) => { if (!eventData.searchQueryUid) { eventData.searchQueryUid = sessionStorage.getItem('_r_searchQueryUid'); } return eventData; }; return registerFields(buildProductRecommendationEngine(config)) as ProductRecommendationEngine; };
Providing Product Suggestions in the Global Search Box
The ML Query Suggestion model provides an easy way to quickly enter queries. In some cases, showing product information in the Query Suggestion list is also a requirement. Doing so requires executing an actual query, parsing the results, and adding them to the Query Suggestion results.
Be careful! If you do this for every keystroke, you would quickly execute a whole bunch of queries. Unless you really need it, we don’t recommend showing product information in a Query Suggestion list.
In our scenario, the ‘AutoComplete’ is now configured using ‘Groups’:
groupBy={(option) => option.group ? option.group : 'Suggestions'}
This will ensure that the ML Query Suggestions are separated from the Product Suggestions.
In the ‘onInputChange’ event of the ‘AutoComplete,’ we can check for the actual input values:
onInputChange={async (_, newInputValue) => { this.headlessSearchBox.updateText(newInputValue); this.getValues(newInputValue); }}
The ‘getValues’ will (after a certain timeout) call our actual query.
getValues(newValue) { clearTimeout(this.handler); this.handler = setTimeout((newValue) => { this.getSearchAsYouTypeResults(newValue); }, mySearchAsYouTypeDelay); }
The ’getSearchAsYouTypeResults’ will execute the actual query against the Search API.
async getSearchAsYouTypeResults() { let q = this.headlessSearchBox.state.value; if (q.length > 2 && !this.props.router.query['fromTest']) { const searchActions = loadSearchActions(headlessEngineQS); const searchParActions = loadPaginationActions(headlessEngineQS); const queryActions = loadQueryActions(headlessEngineQS); const analyticsActions = loadSearchAnalyticsActions(headlessEngineQS); await headlessEngineQS.dispatch(queryActions.updateQuery({ q })); await headlessEngineQS.dispatch(searchParActions.registerNumberOfResults(3)); const res = await headlessEngineQS.dispatch(searchActions.executeSearch(analyticsActions.logInterfaceLoad())); const results = (res ?.payload as any) ?.response ?.results; let newState = []; if (results) { if (results ?.length) { results.forEach((product, index) => { let img = product.raw['ec_image']; let title = HighlightUtils.highlightString({ content: product.title, highlights: product.titleHighlights, openingDelimiter: '<strong>', closingDelimiter: '</strong>' }); newState.push({ highlightedValue: '<div class="QSImage" style="background-image: url(' + img + ')">' + title + '</div>', group: 'Products', rawValue: product.title, info: { url: product.clickUri, pid: product.raw['permanentid'] } }); }); } } this.setState({ otherSuggestions: newState }); this.updateState(); } }
The state will make sure that the Product Suggestions are added to the current state of the SearchBox.
async updateState() { let newState = [...this.headlessSearchBox.state.suggestions]; let newStateLength = newState.length; //Only execute query when actual query has changed //Add the otherSuggestions to the list let otherSuggestions = this.state.otherSuggestions; if (otherSuggestions) { if (otherSuggestions ?.length) { otherSuggestions.forEach((product, index) => { newState.push(product); }); } } }
This will result in an experience like:
This was the last part of the Building Ecommerce series. See you in the next episode of Whiteboard Wednesdays!
Code is available on github.
Special thanks to our Coveo Labs Team: Jerome Devost and Pranal Kuttappan!