Coveo Search Integration with AEM — Front end approaches

Sajalkumar Panda
Activate AEM
Published in
9 min readOct 17, 2023

In this blog post we are going to introduce the front end approaches & best practices to be followed while developing a Coveo search results page. This document is designed for any front end developer who uses search regularly and wants to understand the features and implementations of Coveo search. We also explain why Coveo search is different from any other search engine. By the end of this document, you will have developed a solid understanding of the basics of Coveo search, how we integrate Coveo search with AEM, hand shaking between these two systems and displaying the search results on an AEM web page.

Below we described the components of Coveo search and its features .

Various elements as part of the Coveo search page are given below and a screenshot of the actual page is given at end of description.

Facets:

We have separated the page into two sections, Left section has facets (which is used for refinements) and the right side has search results.

Facet is an interface that allows the user to search one or multiple values and filter the required field values and produce the results on the right side section.

Facets Features:

Facts search provides a list of filtered values or refined results as the end user requires.

We have multiple option searches like resource / year / type / tag. Based on this search we can get the desired results.

The values in a facet can be sorted from ascending, descending or alphanumeric order. We can also configure a custom sort as we require.

Right section has the results section. Which provides the list of content retrieved from the query.

Sorting:

We used three options for sorting the results as given below.

1.Relevance — By default all items in the result pages are ranked by relevance. The first item populated in the results section is considered as most relevant.

2. Date Ascending — from old record to new recorded date.

3. Date Descending — from new record to old record

Filter: A filter is used to narrow down the result set when the user querying the required inputs.

Pagination: We have an option to travers through the search results pages based on the ‘total number of pages’. Using pagination we can add option for user to select 1st, 2nd 3rd … pages or using ‘Previous’ & ‘Next’.

Additional Options:

List View — when you click on list view option, all queried results will display as list.

Card View — when you click on the card view, All queried results will display as cards.

Initially we are showing 10 records on the page. If you want to see other results navigate the page to next page using pagination.

Below given a standard view of developed Coveo search page with above said components

AEM Search results page from Coveo

Diving into implementation details

The approach was to separate the page into multiple sections. At the top of the page we have the search box where you can enter the search term.

On the left hand side we have navigation elements. The navigation is made up of facets which helps us to navigate, filter and refine results that are retrieved by the query.

And the result section provided the list of content returned for the query that was entered above in the context section .

AEM Search results page from Coveo

The integration between AEM to Coveo

Coveo provides a set of API’s which can be called from the AEM page. For every query searched by the user, we call the Coveo through authentication handshakes and retrieve the results. These results are rendered as part of the page load in AEM.

The initial step of configuration involves retrieving the authentication token from Coveo and add the token in the configuration Manager of AEM. For this we need to go to OSGI configuration -> AEM-[Project] — Global Site Search Box API Config & need to add the token, before querying the user search

Osgi Configuration for Coveo Search

Once the token has been configured in OSGI, the AEM back end code can retrieve the configuration details and start using it for authentications. When user start searching the required query, then an API call will be sent through the network from AEM and get the desired result in the response in the return API response from Coveo. Below given the screenshot.

Let us see how to customize the Coveo search results page

Coveo has a set of features that could be helpful for any brand, but because it is meant to serve a wide variety of business use cases, it is highly unlikely it will have all the functionality we will need to reach peak productivity.

However, custom software development is done by considering our business requirements in mind, which means it can include every requirement we need to succeed and it can easily integrate with business and scale as the brand expands.

How to invoke the Coveo Search using API and Payload:

The Coveo Endpoint is given below:

https://platform.cloud.coveo.com/rest/search/v2

Token:

Coveo team provides a trial token to authenticate the Coveo and play around with sample search results. In this blog post we will be using the trial token provided by Coveo for our sample code. In production environments, we will have to use the licensed token from Coveo.

Trial Token : xx564559b1–0045–48e1–953c-3addd1eexx

We will pass our token value through apiToken from the front end.

const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiToken}`,
};

We have some other dependencies also to fetch the proper data from API. Below given additional information passed as part of API call.

const payload = {
q: searchString,
facets: this.facets,
format: 'json',
enableWordCompletion: true,
firstResult: this.firstResult,
numberOfResults: parseInt(numPerPage, 10),
sortCriteria: this.sortCriteria,
cq: this.source
};

The details of API Parameters are given below,

q — The query string which was entered by a user (we got the value from URL query parameters or from Search Input field).

facets: This helps to generate facets. Which facets value we need from API response. We will discuss more details on the facet section.

firstResult: Depicts the initial item in a list. For e.g. in results 1st page we need values from 0–10 the firstResult is 0 ; for 2nd page results are from 11–20 & the firstResult value is 11.

numberofResults: The total number of results on a page.

SortCriteria: We need to define ascending/Descending here.

const method = 'POST';

In the API response we get back the result, facets and total data count. Depending on the response we can populate DOM of search results HTML.

How to invoke the Pagination in Coveo:

We introduced a pagination function, from where we can populate required pages and can show data based on that.

Need to pass four parameters in this function

totalItems: Total number of results. We will get this from the API response.

currentactivePage: defaults to 1 which means first page or else we can pass the data depending on click.

pageSize: How many menu numbers of results we need to show in every page. We are going to pick the value from the dialog.

maxPages: How many pages we want to show at a glance in the pagination portion.

paginate = (totalItems, currentactivePage, pageSize, maxPages) => {
let jumpToPrevious = '';
let jumpToNext = '';
let startPage;
let endPage;
let currentPage = currentactivePage;
// calculate total pages
totalPages = Math.ceil(totalItems / pageSize);
// ensure current page isn't out of range
if (currentPage < 1) {
currentPage = 1;
} else if (currentPage > totalPages) {
currentPage = totalPages;
}
if (totalPages <= maxPages) {
// total pages less than max so show all pages
startPage = 1;
endPage = totalPages;
} else {
// total pages more than max so calculate start and end pages
const maxPagesBeforeCurrentPage = Math.floor(maxPages / 2);
const maxPagesAfterCurrentPage = Math.ceil(maxPages / 2) - 1;
if (currentPage <= maxPagesBeforeCurrentPage) {
// current page near the start
startPage = 1;
endPage = maxPages;
} else if (currentPage + maxPagesAfterCurrentPage >= totalPages) {
// current page near the end
startPage = totalPages - maxPages + 1;
endPage = totalPages;
} else {
// current page somewhere in the middle
startPage = currentPage - maxPagesBeforeCurrentPage;
endPage = currentPage + maxPagesAfterCurrentPage;
}
}
// calculate start and end item indexes
const startIndex = (currentPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);
// create an array of pages to ng-repeat in the pager control
const pages = Array.from(Array((endPage + 1) - startPage).keys()).map((i) => startPage + i);
// return object with all pager properties required by the view
const pageDom = pages
.map((page) => {
if (this.activePage === page) {
return `<li class="${componentName}__pager-list-item active"><a href="javascript:void(0)" class="${componentName}__active-page" ${componentName}-data-page="${page}" aria-label="Page ${page}">${page}</a></li>`;
}
return `<li class="${componentName}__pager-list-item"><a href="javascript:void(0)" class="${componentName}__active-page" ${componentName}-data-page="${page}">${page}</a></li>`;
}).join('');
jumpToPrevious = this.activePage !== 1 ? `<li class="${componentName}__pager-list-item"><a href="javascript:void(0)" class="${componentName}__jump-first" aria-label="Page 1"><i class="fa-solid fa-chevrons-left"></i></a></li>
<li class="${componentName}__pager-list-item"><a href="javascript:void(0)" class="${componentName}__left-icon" aria-label="Previous Page"><i class="fa-solid fa-chevron-left"></i></a></li>` : `<li class="${componentName}__pager-list-item disabled"><a href="javascript:void(0)" class="${componentName}__jump-first" aria-label="Page 1"><i class="fa-solid fa-chevrons-left"></i></a></li>
<li class="${componentName}__pager-list-item disabled"><a href="javascript:void(0)" class="${componentName}__left-icon" aria-label="Previous Page"><i class="fa-solid fa-chevron-left"></i></a></li>`;
jumpToNext = this.activePage === totalPages ? `<li class="${componentName}__pager-list-item disabled"><a href="javascript:void(0)" class="${componentName}__right-icon" aria-label="Next Page"><i class="fa-solid fa-chevron-right"></i></a></li>
<li class="${componentName}__pager-list-item disabled"><a href="javascript:void(0)" class="${componentName}__jump-last" aria-label="Page End"><i class="fa-solid fa-chevrons-right"></i></a></li>` : `<li class="${componentName}__pager-list-item"><a href="javascript:void(0)" class="${componentName}__right-icon" aria-label="Next Page"><i class="fa-solid fa-chevron-right"></i></a></li>
<li class="${componentName}__pager-list-item"><a href="javascript:void(0)" class="${componentName}__jump-last" aria-label="Page End"><i class="fa-solid fa-chevrons-right"></i></a></li>`;
const listOfPage = `${jumpToPrevious} ${pageDom} ${jumpToNext}`;
return {
totalItems,
currentPage,
pageSize,
totalPages,
startPage,
endPage,
startIndex,
endIndex,
pages,
listOfPage
};
}

How to invoke Facets in Coveo:

In earlier section we briefly discussed this. To define the facets, we have to pass required facets value.

Payload:

Response:

On getting data from API response we need to pass the value.

updateFacetsAsRequired = (responseFacets) => {
const payloadFacets = this.facets;
const facetMap = new Map();
payloadFacets.forEach((facet) => {
const key = facet.facetId;
facetMap.set(key, facet.facetLabel);
});
let facetsResponse = responseFacets;
facetsResponse = facetsResponse.map((facets) => {
const key = facets.facetId;
return ({
…facets,
facetLabel: facetMap.get(key) || '',
});
});
facetMap.clear();
this.renderFacets(facetsResponse);
}
this.facets = this.cmpConfig.facetsList !== 'null' ? JSON.parse(this.cmpConfig.facetsList) : [];

this.facets = the facets we declared on dialog or predefined facets.

renderFacets() : Passing the value by this function we can create DOM as required.

Onclick:

While clicking on the facets, we need to pass the selected/clicked value through API so that we can get the filtered result.

Payload:

Response:

Final result:

When we load the API response to search results HTML, it looks as below.

Generic Best Practices Followed

  • We have written the code in a readable way so that other developers can easily understand, enhancement can be done in future easily.
  • We have avoided writing redundant code / logic and simplified the code with well structured way.
  • Maintained extensive code comments for the functionality explaining for what purpose we have written, so that everyone can debug the code easily.
  • We have followed the BEM standards for the CSS.
  • We have done the code avoiding lint issues with the css as well as js and followed the UI Standards.

References:

Coveo API Documentation: https://docs.coveo.com/en/105/build-a-search-ui/api-key-authentication

Coveo API Swagger Documentation: https://platform.cloud.coveo.com/docs?urls.primaryName=Search%20API

--

--