Understanding Search Query With Pagination Using GraphQL In XM Cloud
Hello Everyone, In this blog we will see how we can implement search query using GraphQL and implement pagination.GraphQL uses cursor based system to handle pagination whenever we execute a search query. We will see this in action.
Let's understand this with a common use-case which every developers encounters. We are having Blog pages or Article pages. We want to get all blogs or articles based on some conditions. The blogs or articles should be sorted based on published date. At first, we just want to show n number of items from the result. Then on load more, we need to show the remaining items.
Let's jump quickly into action. We will open GraphQL IDE and dive into writing the query. I am using local XM Cloud.
Required thing to execute query.
- GraphQL IDE : "https://xmcloudcm.localhost/sitecore/api/graph/edge/ide"
- Api Key: We can get it from Sitecore instance under mentioned path. Path: /sitecore/system/Settings/Services/API Keys.
In above image, it gives a glimpse of how the IDE looks. We need to pass sc_Apikey in Headers as shown.
Let's see the GraphQL Query.
query {
pageResults: search(
where: {
AND: [
{
name: "_path"
value: "A1B4CCF8-E196-4AF5-9AAA-6B3E324FED65"
operator: CONTAINS
}
{
name: "_hasLayout",
value: "true"
}
{
name: "_language",
operator: EQ,
value: "fr-FR"
}
{
name: "_templates"
operator: CONTAINS
value: "acd3828a-d5b4-41fa-8a1e-1030e28b0c9a"
}
{
name: "_path",
operator: NCONTAINS,
value: "0DB3B3C6-94B0-4170-A4E9-AE056EAB6377"
}
{
OR : [
{
name: "tags",
operator: CONTAINS,
value: "21067420-61FB-4A67-99A5-D68D41CEBB0E"
}
{
name: "tags",
operator: CONTAINS,
value: "5108022D-D444-4EE4-8BE9-860FAC9E8363"
}
]
}
]
}
orderBy: { name: "date", direction: DESC }
first: 3
after: ""
) {
total
pageInfo {
endCursor
hasNext
}
results {
...Article
}
}
}
fragment Article on BrandAppRoute {
id
language {
name
}
url {
path
}
tags {
id
jsonValue
}
}
In the above GraphQL query, we have used all the condition that GraphQL supports. We have added path parameters to check the id should be there in path using contains property. We are also checking which item has Layout assigned to it. If we want to include any template in query.Also we are using Fragments to get what results, we need to show.You can also add your own field from template(ex. Tag field). See the below output results.
In the above result query, just focus on pageInfo object. We are having two properties, hasNext and endCursor. Now at start, we have seen, that for implementing pagination, GraphQL uses cursor based logic.We passed a number to first parameter passed in Query. The number passed to first parameter will be the results return at first. Then we check, the hasNext property value and if it is true, then pass endCursor value to after parameter as shown in second screenshot.
Now let's jump and see the next js logic to implement the same.
Step 1: We created a searchContentPages.js which will perform the search logic.
import { type ApolloQueryResult, gql } from '@apollo/client';
import graphQLClient from 'lib/graphql-request/backend';
import * as Types from 'temp/schema';
export type SearchPagesQuery = {
pageResults?: Types.Maybe<{
results?: Types.Maybe<Array<Types.Maybe<SearchPagesFragment>>>;
pageInfo?: Types.Maybe<PageInfo>;
}>;
};
export type SearchResult={
results?: Types.Maybe<Array<Types.Maybe<SearchPagesFragment>>>;
}
export type PageInfo = {
endCursor: string;
hasNext: boolean;
};
export type SearchPagesFragment = Pick<Types.BrandAppRoute, 'id'> & {
language: Pick<Types.ItemLanguage, 'name'>;
url: Pick<Types.ItemUrl, 'path'>;
tags?: Types.Maybe<Pick<Types.MultilistField, 'id' | 'jsonValue'>>;
};
const searchContentPages = async (
rootFolderId: string,
tagIds: string[],
pageId = '',
language = 'en',
numberOfPages?: number
): Promise<SearchResult | null> => {
let tagFilter = '';
if (tagIds?.length) {
const predicates = tagIds
.map((tag) => `{ name: "tags", operator: CONTAINS, value: "${tag}" }`)
.join('\n');
tagFilter = `{
OR: [${predicates}]
}`;
}
const pageIdFilter =
pageId !== rootFolderId
? '{ name: "_path", operator: NCONTAINS, value: "' + pageId + '" }'
: '';
const searchquery = gql`
query SearchPages(
$rootFolderId: String!
$language: String!
$numberOfPages: Int
$after:String
) {
pageResults: search(
where: {
AND: [
{ name: "_language", operator: EQ, value: $language }
{ name: "_hasLayout", value: "true" }
{
name: "_templates"
operator: CONTAINS
value: "acd3828a-d5b4-41fa-8a1e-1030e28b0c9a"
}
${pageIdFilter}
${tagFilter}
{ name: "_path", operator: CONTAINS, value: $rootFolderId }
]
}
orderBy: { name: "date", direction: DESC }
first: $numberOfPages
after: $after
) {
pageInfo {
endCursor
hasNext
}
results {
...Article
}
}
}
fragment Article on BrandAppRoute {
id
language {
name
}
url {
path
}
tags {
id
jsonValue
}
}
`;
let after: string | undefined = undefined;
let hasNextPage = true;
let contextsearchdata: ApolloQueryResult<SearchPagesQuery>;
let allResults: SearchResult = {
results : []
};
while (hasNextPage) {
try{
contextsearchdata = await graphQLClient.query<SearchPagesQuery>({
query: searchquery,
variables: {
language,
numberOfPages,
pageId,
rootFolderId,
after
},
});
const currentPageResults = contextsearchdata?.data.pageResults?.results || [];
allResults.results = [...allResults.results || [], ...currentPageResults];
const pageInfo = contextsearchdata?.data?.pageResults?.pageInfo;
if (pageInfo) {
hasNextPage = pageInfo.hasNext;
after = pageInfo.endCursor;
} else {
hasNextPage = false;
}
}catch(error){
console.log('Error fetching data:', error);
hasNextPage = false;
}
}
return allResults;
};
export default searchContentPages;
In the above logic, we are using ApolloGraphQL client to make GraphQL query. We have allResults which is an empty array at start. We check the condition whether hasNext is true and if yes, we concatenate the results till the value is false. The after value is assigned based on endcursor value. Once you get all results, we can implement any front end logic for pagination. The code uses TypeScript and everyresult is typeSafe.
Step 2: We created a Article.js which will call the search component and get the results.
const request = await searchContentPages(
rootFolderId,
Tags,
layoutData?.sitecore?.route?.itemId,
layoutData?.sitecore?.context?.language,
numberOfPages
);
//This is the actual list
const playList = request?.results;
In above code, we are calling the searchContentPages component passing the required parameter. rootFolderID is the start location from where the query executes,Tags is an array in our case which help to filter Tag based search.ItemId and Language for more drill filtering and numberofPages represent the first n number of output from the results.
I hope, you would have good insights around implementing the search with pagination using GraphQL.Keep Learning and Happy Sitecoring!!!
You can check my other blogs too if interested. Blog Website
Comments
Post a Comment