Handling Hierarchical Taxonomies in Sitecore Search
Sitecore's search functionality doesn't natively support hierarchical facets.
Originally published on DEV Community.

The Challenge
Sitecore's search functionality doesn't natively support hierarchical facets. When you have taxonomies with parent-child relationships (like "Water → Treatment" or "Infrastructure → Transportation"), the search index treats them as flat values, losing the important hierarchical context that helps users understand relationships and filter content more effectively.
Our Solution: The Parent__Child Pattern
To work around this limitation, we've implemented a solution that encodes hierarchical relationships directly into the search metadata using a double underscore (__) separator pattern. This allows us to:
- Preserve hierarchy in the search index
- Enable frontend parsing to reconstruct parent-child relationships
- Maintain backward compatibility with flat taxonomies
- Support flexible filtering at both parent and child levels
How It Works
The Architecture
Our solution consists of three key components:
TaxonomyService- GraphQL-powered service for fetching tag relationshipsresolveHierarchicalTags- Utility function that resolves parent-child relationshipsbuildTagMetaProps- Helper function that processes multiple taxonomy groups
Step-by-Step Process
1. Tag Group Definition
In our metadata services (like ProjectsDetailMetadataService), we define tag groups that need hierarchical resolution:
const tagGroups = [
{
items: projectDetailPageProps?.practices,
field: SEARCH_FIELDS.PRACTICES,
},
{
items: projectDetailPageProps?.priorities,
field: SEARCH_FIELDS.PRIORITIES,
},
{
items: projectDetailPageProps?.technologies,
field: SEARCH_FIELDS.TECHNOLOGIES,
},
{
items: projectDetailPageProps?.markets,
field: SEARCH_FIELDS.MARKETS,
},
{
items: projectDetailPageProps?.region,
field: SEARCH_FIELDS.REGION,
},
];Each group contains:
items: Array of tag item references from Sitecorefield: The search field name where the metadata will be storedmode(optional): Either'hierarchical'(default) or'flat'for non-hierarchical tags
2. Hierarchical Resolution
The buildTagMetaProps helper processes these groups and calls resolveHierarchicalTags for each hierarchical group:
export async function resolveHierarchicalTags(
service: TaxonomyService,
items: TagItemRef[]
): Promise<string[]> {
if (!Array.isArray(items) || items.length === 0) return [];
const results = await Promise.all(
items.map(async (item) => {
const childName: string = item?.fields?.tagTitle?.value ?? item?.fields?.title?.value ?? '';
if (!childName) return null;
const id = item?.id;
if (!id) return childName;
try {
const parentName = await service.getTagParent(id);
return parentName ? `${parentName}__${childName}` : childName;
} catch {
return childName;
}
})
);
return results.filter((v): v is string => !!v && v.trim().length > 0);
}What this does:
- Extracts the child tag name from the item's fields
- Uses the tag's ID to query for its parent via GraphQL
- If a parent exists, returns
"Parent__Child"format - If no parent exists, falls back to just the child name
- Handles errors gracefully by returning the child name
3. GraphQL Parent Resolution
The TaxonomyService.getTagParent() method uses Sitecore's GraphQL API to fetch parent relationships:
async getTagParent(tagId: string): Promise<string | null> {
const graphQLClient = this.getClient();
const query = gql`
query GetTagParent($tagId: String!, $language: String!) {
tag: item(path: $tagId, language: $language) {
parent {
fields {
name
jsonValue
}
}
}
}
`;
const result = await graphQLClient.request<TagParentResponse>(query, {
tagId,
language: this.language,
});
const tagName =
result.tag?.parent?.fields?.find(
(f: { name: string; jsonValue: { value?: string } }) => f.name === 'tagTitle'
)?.jsonValue?.value ?? null;
if (tagName && tagName.trim().length > 0) return tagName;
return null;
}Key points:
- Queries the parent item's
tagTitlefield - Returns
nullif no parent exists or if the parent isn't a tag item - Handles language-specific queries automatically
4. Meta Props Generation
The buildTagMetaProps function orchestrates the entire process:
export async function buildTagMetaProps(
service: TaxonomyService,
groups: Array<{
field: string;
items: unknown;
mode?: 'hierarchical' | 'flat';
flatSelector?: (item: { fields?: { [key: string]: { value?: string } } }) => string | undefined;
}>
): Promise<SimpleMetaProp[]> {
const meta: SimpleMetaProp[] = [];
for (const group of groups) {
const mode = group.mode ?? 'hierarchical';
if (mode === 'hierarchical') {
const itemsArray = group.items as TagItemRef[] | undefined;
if (Array.isArray(itemsArray) && itemsArray.length > 0) {
const values = await resolveHierarchicalTags(service, itemsArray);
if (values.length > 0) {
meta.push({ name: group.field, content: values.join(', ') });
}
}
} else {
// Flat mode - direct value extraction
const itemsArray = group.items as
| Array<{ fields?: { [key: string]: { value?: string } } }>
| undefined;
if (Array.isArray(itemsArray) && itemsArray.length > 0) {
const extractor =
group.flatSelector ||
((i: { fields?: { [key: string]: { value?: string } } }) =>
i?.fields?.tagTitle?.value ?? i?.fields?.title?.value ?? '');
const values = itemsArray
.map((i) => extractor(i))
.filter((v): v is string => !!v && v.trim().length > 0);
if (values.length > 0) {
meta.push({ name: group.field, content: values.join(', ') });
}
}
}
}
return meta;
}Output format:
The function returns an array of meta props like:
[
{
name: 'practices',
content: 'Water__Treatment, Infrastructure__Transportation, Energy__Renewable',
},
{
name: 'technologies',
content: 'AI__Machine Learning, IoT__Sensors',
},
];Example: Real-World Usage
Input Data Structure
A project detail page might have:
projectDetailPageProps = {
practices: [
{
id: '{guid-1}',
fields: {
tagTitle: { value: 'Treatment' },
},
},
{
id: '{guid-2}',
fields: {
tagTitle: { value: 'Transportation' },
},
},
],
};Processing Flow
- Extract child names:
['Treatment', 'Transportation'] - Query parents via GraphQL:
\{guid-1\}→ Parent:'Water'\{guid-2\}→ Parent:'Infrastructure'
- Build hierarchical strings:
['Water__Treatment', 'Infrastructure__Transportation'] - Join into meta prop:
content: 'Water__Treatment, Infrastructure__Transportation'
Search Index Result
The search index now contains:
{
"practices": "Water__Treatment, Infrastructure__Transportation"
}Frontend Handling
On the frontend, you can parse these hierarchical values:
// Parse hierarchical taxonomy values
function parseHierarchicalTags(tagString: string): Array<{ parent: string; child: string }> {
return tagString.split(', ').map((tag) => {
const parts = tag.split('__');
if (parts.length === 2) {
return { parent: parts[0], child: parts[1] };
}
return { parent: null, child: tag }; // Fallback for flat tags
});
}
// Usage
const practices = parseHierarchicalTags(
metaProps.find((p) => p.name === 'practices')?.content || ''
);
// Result: [
// { parent: 'Water', child: 'Treatment' },
// { parent: 'Infrastructure', child: 'Transportation' }
// ]This enables you to:
- Group filters by parent in the UI
- Show hierarchical navigation (e.g., "Water > Treatment")
- Filter by parent or child independently
- Display relationships visually
Benefits of This Approach
1. Search Engine Compatibility
- Works with any search engine that supports string metadata
- No special hierarchical facet support required
- Simple string matching for filtering
2. Flexible Frontend Implementation
- Frontend can parse and reconstruct hierarchy
- Supports both parent-level and child-level filtering
- Easy to implement UI components like hierarchical dropdowns
3. Backward Compatibility
- Falls back gracefully when no parent exists
- Works with flat taxonomies (no parent) automatically
- Error handling ensures robustness
4. Performance
- Parallel processing with
Promise.all()for multiple tags - Efficient GraphQL queries (single query per tag)
- Caching opportunities at the GraphQL layer
Advanced Features
Flat Mode Support
For non-hierarchical tags, you can use flat mode:
const tagGroups = [
{
items: projectDetailPageProps?.location,
field: SEARCH_FIELDS.LOCATION,
mode: 'flat', // Skip hierarchical resolution
},
];Custom Selectors
You can provide custom value extractors for flat mode:
{
items: customItems,
field: SEARCH_FIELDS.CUSTOM,
mode: 'flat',
flatSelector: (item) => item?.fields?.customField?.value ?? '',
}Best Practices
1. Error Handling
Always handle cases where:
- Parent lookup fails (GraphQL errors)
- Tag has no parent (flat taxonomy)
- Tag item is missing required fields
2. Performance Considerations
- Use
Promise.all()for parallel parent lookups - Consider caching parent relationships if they don't change frequently
- Batch GraphQL queries when possible
3. Naming Conventions
- Use consistent field names (
SEARCH_FIELDSconstants) - Ensure parent and child names don't contain
__(reserved separator) - Validate tag names before indexing
4. Testing
Test scenarios:
- Tags with parents
- Tags without parents (flat)
- Tags with missing IDs
- GraphQL query failures
- Empty tag arrays
Conclusion
This hierarchical taxonomy solution provides a robust, flexible approach to handling parent-child relationships in Sitecore search. By encoding hierarchy directly into search metadata using the Parent__Child pattern, we maintain compatibility with standard search engines while enabling rich hierarchical filtering and navigation in the frontend.
The solution is:
- ✅ Simple - Easy to understand and maintain
- ✅ Flexible - Supports both hierarchical and flat taxonomies
- ✅ Robust - Handles errors gracefully
- ✅ Performant - Parallel processing and efficient queries
- ✅ Extensible - Easy to add new taxonomy groups