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 relationships -
resolveHierarchicalTags- Utility function that resolves parent-child relationships -
buildTagMetaProps- 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 Sitecore -
field: The search field name where the metadata will be stored -
mode(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
Top comments (0)