Integrating Sitecore Search APIs: Custom Schema Mapping and GraphQL Integration

Integrating Sitecore Search APIs: Custom Schema Mapping and GraphQL Integration

Sitecore

Search is among those functionalities which appear simple in demonstrations but start becoming complex as soon as the actual data and environment comes into the picture. In case you have developed your solution based on Sitecore XM Cloud or headless JSS/Next.js front-end, you will find yourself dealing with two rather disparate yet very often misunderstood parts:

  1. Sitecore Search, the SaaS discovery/search product (crawler, index, widgets, SDK)
  2. Sitecore's GraphQL APIs, used to query content from the CMS itself (Experience Edge / Content SDK)

This post walks through both: how to define and map a custom schema for Sitecore Search, and how GraphQL fits into the picture either as the CMS content layer your search results link back to, or as the delivery mechanism feeding a custom crawler source.

Two Different "Search" Concepts - Don't Mix Them Up

Before touching any code, it's worth being explicit about scope, because a lot of confusion in this space comes from treating these as one thing:

  • Sitecore's Content GraphQL API (available through Experience Edge or the Content SDK's SearchService) queries your content tree, items, templates, and fields. This is the API you use to render pages and pull structured content into your Next.js head.
  • Sitecore Search is a separate, independent SaaS product. It doesn't query your content tree directly; it crawls your rendered website (or an API endpoint) and builds its own index, entirely decoupled from Experience Edge. You configure it through the Customer Engagement Console (CEC), not through Sitecore's content APIs.

That distinction matters because "schema mapping" means something different in each context: in GraphQL, you're working with strongly typed template fields; in Sitecore Search, you're mapping crawled or API-sourced data into domain attributes.

Part 1: Custom Schema Mapping in Sitecore Search

Domain Attributes Are Your Schema

In the CEC, navigate to Administration β†’ Domain Settings β†’ Attributes. Attributes are the fields your content gets indexed with; think of them as your schema definition. Sitecore ships a handful of default entities (Content, Product, Category, Store); most content-site implementations work primarily with the Content entity.

A typical attribute set for a content site looks like this:

Attribute Type Features
title String Textual Relevance, Return in API Response
description String Textual Relevance, Return in API Response
url String Return in API Response
image_url String Return in API Response
type String Facets, Return in API Response
category String Facets, Suggestions, Return in API Response
date Timestamp Sorting, Return in API Response
site_name String Facets (critical for multi-site)

The one and only crucial point is that once the flag "Will be used in" appears on an attribute, then it cannot be altered in any way later. In case you forget to turn Facets ON for some attributes and remember about that fact later, there is no way to do it β€” you have to delete the attribute and create a new one.

Mapping Data In: Sources and Document Extractors

A Source is a crawler configuration pointed at a site (or set of pages). For each site in a multi-site setup, you'll typically create a separate source. When adding a source, Web Crawler (Advanced) is generally the right choice; it supports JavaScript rendering, multi-language crawling, and gives you full control over extraction, as opposed to a basic crawler that just indexes raw HTML.

The actual schema mapping happens in the Document Extractor. A Cheerio-based JS function that runs against the crawled HTML and maps whatever it finds to your attributes:

function extract(request, response, document) {

const $ = document.content;

const title =

$('meta[property="og:title"]').attr('content') || $('title').text() || '';

const description =

$('meta[property="og:description"]').attr('content') ||

$('meta[name="description"]').attr('content') ||

'';

const imageUrl = $('meta[property="og:image"]').attr('content') || '';

// Structured metadata rendered by your JSS layout

const type = $('body').data('page-type') || 'content';

const siteName = $('body').data('site-name') || 'default';

return {

title,

description,

image_url: imageUrl,

type,

site_name: siteName,

};

}

Rather than scraping visible page text for structured fields, it's far more reliable to render dedicated data-* attributes on <body> from your JSS layout (driven by your SXA site settings) and extract those directly. It keeps the mapping predictable and resistant to markup changes.

You can also use this stage to exclude pages from search entirely; check for a custom meta flag your CMS renders and return null:

const exclude = $('meta[property="excludeFromSearch"]').attr('content');

if (exclude === 'true') return null;

Mapping Data from an API Instead of HTML

If you're indexing content that isn't publicly rendered as HTML, a headless product catalogue, for example, Sitecore Search also supports an API-based source. Rather than crawling pages, it sends requests (including GraphQL queries) to your endpoint, parses the JSON response, and maps fields into your attribute schema. This is the pattern to reach for when your source data comes from a GraphQL product API rather than a crawlable page; the extractor logic shifts from parsing HTML with Cheerio to mapping JSON fields directly to attributes, with support for pagination, token auth, and custom headers.

Part 2: Where GraphQL Fits

Content GraphQL (Experience Edge / Content SDK)

This is the API your JSS/Next.js head uses to pull structured content separate from Sitecore Search entirely. Experience Edge exposes a read-only schema with item, layout, and search entry points for querying by path, rendering layout data, or running boolean field searches. The newer Content SDK offers a SearchService class that wraps this for a more type-safe developer experience:

import { SearchService } from '@sitecore-content-sdk/search';

const searchService = new SearchService({

contextId: 'SITECORE_EDGE_CONTEXT_ID',

});

const response = await searchService.search({

searchIndexId: '1234567890',

keyphrase: 'product',

});

This is useful for querying content-tree-backed indexes with type safety and pagination, but it's still querying Sitecore content, not the Sitecore Search product's crawler-built index.

Sitecore Search's Own SDK

For the Sitecore Search product itself, front-end integration doesn't go through raw GraphQL; it goes through a dedicated React SDK. As of 2025/2026 there are two SDK paths, and it's easy to conflate them:

  • @sitecore-search/react + @sitecore-search/ui β€” the original SDK, with a prebuilt widget kit (PreviewSearch, SearchResults, etc.) built on styled-components.
  • @sitecore-cloudsdk/search β€” the newer Cloud SDK approach (shared with CDP/Personalize integrations), authenticated via an Edge Context ID rather than a standalone API key. This is the direction new composable builds are generally pointed toward, though the original SDK remains fully supported and, at the time of writing, better documented with more community examples.

A minimal setup wires a WidgetsProvider at the layout level:

import { WidgetsProvider } from '@sitecore-search/react';

const searchConfig = {

env: process.env.NEXT_PUBLIC_SEARCH_ENV as string, // 'prod' | 'staging' | 'prodEu' | 'apse2'

customerKey: process.env.NEXT_PUBLIC_SEARCH_CUSTOMER_KEY as string,

apiKey: process.env.NEXT_PUBLIC_SEARCH_API_KEY as string,

};

export default function Layout({ children }) {

return <WidgetsProvider {...searchConfig}>{children}</WidgetsProvider>;

}

And a results component consumes the mapped attributes you defined in Part 1:

import { useSearchResults, widget, FilterEqual } from '@sitecore-search/react';

interface SearchResultItem {

id: string;

title: string;

description: string;

url: string;

image_url?: string;

site_name?: string;

}

const SearchResultsComponent = () => {

const {

widgetRef,

queryResult: {

data: { total_item: totalItems = 0, facet: facets = [], content: results = [] } = {},

},

} = useSearchResults<SearchResultItem>({

query: (query) =>

query

.getRequest()

.addSearchQueryFilter(

new FilterEqual('site_name', process.env.NEXT_PUBLIC_SITE_NAME as string)

),

});

return (

<div ref={widgetRef}>

{results.map((r) => (

<a key={r.id} href={r.url}>{r.title}</a>

))}

</div>

);

};

export const SearchResults = widget(SearchResultsComponent, undefined, 'search-results');

Notice how directly this maps back to the attribute schema: title, description, url, image_url, and site_name are exactly the fields defined as domain attributes and populated by the document extractor. That's the real thread connecting Part 1 and Part 2: the schema you design in the CEC is the shape of every object your front end consumes.

Practical Gotchas Worth Planning For

A few things consistently trip up teams integrating Sitecore Search:

  • Attribute and widget feature flags are immutable after creation. Get facets, sorting, and relevance settings right up front.
  • Non-Prod and Prod are separate tenants with no config export/import. Sources, extractors, widget settings, and boosting rules must be manually replicated between environments. Keep document extractor scripts in source control and maintain a changelog for everything else.
  • Multi-site scoping depends entirely on a consistent site_name-style attribute, populated via a data attribute your layout renders and filtered on at query time with something like FilterEqual.
  • The full @sitecore-search/ui package can meaningfully affect Lighthouse scores. For performance-sensitive pages, using the SDK hooks (usePreviewSearch, useSearchResults) directly with your own styled components is often the better trade-off.

Wrapping Up

Custom schema mapping in Sitecore Search comes down to two disciplines: define your domain attributes deliberately (because feature flags can't be changed later) and write extractors that map either crawled HTML or an API/GraphQL response cleanly into that schema. GraphQL's role depends on which "search" you mean: it's the delivery mechanism for your CMS content via Experience Edge or the Content SDK, and it can also be the source data format an API-based Sitecore Search crawler consumes. Keeping those two layers conceptually separate but consistently mapped through the same attribute names on the front end is what makes the whole integration hold together cleanly across environments.

Written by
Janki

Janki Suthar

Technical Architect

Hi, I'm Janki Suthar. I work as a Technical Architect and Sitecore Certified Software Developer at Arroact Technologies, where my days are split between Sitecore XP, XM, and XM Cloud on one side, and React.js, Next.js, and .NET on the other.

What draws me to this stack is the challenge of making two very different worlds, a structured CMS backend and a dynamic frontend, work together seamlessly. Sitecore AI has become a big part of that lately, and I've been digging into how it changes what personalization can actually look like in practice.

I've learned the best fix is usually the simple one. Given a choice, I'll always pick the version that's easier to explain, even if it took longer to get there.

Related Blogs blue-line-vector-3

Developing Code Assistants and AI Prompts in Sitecore Stream
21 August 26 β€’ 10 min read
Sitecore
Developing Code Assistants and AI Prompts in Sitecore Stream
Most personalization bugs don’t come from the architecture. They come from the tiny bits: …
Read More
Sitecore XM Cloud GraphQL: A Practical Guide to Building Efficient Content Queries
19 August 26 β€’ 18 min read
Sitecore
Sitecore XM Cloud GraphQL: A Practical Guide to Building Efficient Content Queries
Introduction Sitecore XM Cloud is a headless CMS. This implies that while the content is …
Read More
Taming Environment Sprawl: A Practical Governance Framework for Sitecore Cloud Portal
14 August 26 β€’ 12 min read
Sitecore
Taming Environment Sprawl: A Practical Governance Framework for Sitecore Cloud Portal
If you have worked with  Sitecore Cloud Portal for more than a few months, you alrea…
Read More
Make Smarter Decisions with an Accurate Sitecore Project Estimate. Get Your Free Sitecore Project Estimate
Get Project Estimate