DEV Community

Sebastián Aliaga
Sebastián Aliaga

Posted on

Handling Hierarchical Taxonomies in Sitecore Search

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:

  1. Preserve hierarchy in the search index
  2. Enable frontend parsing to reconstruct parent-child relationships
  3. Maintain backward compatibility with flat taxonomies
  4. Support flexible filtering at both parent and child levels

How It Works

The Architecture

Our solution consists of three key components:

  1. TaxonomyService - GraphQL-powered service for fetching tag relationships
  2. resolveHierarchicalTags - Utility function that resolves parent-child relationships
  3. 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,
  },
];
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  1. Extracts the child tag name from the item's fields
  2. Uses the tag's ID to query for its parent via GraphQL
  3. If a parent exists, returns "Parent__Child" format
  4. If no parent exists, falls back to just the child name
  5. 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;
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Queries the parent item's tagTitle field
  • Returns null if 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;
}
Enter fullscreen mode Exit fullscreen mode

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',
  },
];
Enter fullscreen mode Exit fullscreen mode

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' },
      },
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Processing Flow

  1. Extract child names: ['Treatment', 'Transportation']
  2. Query parents via GraphQL:
    • {guid-1} → Parent: 'Water'
    • {guid-2} → Parent: 'Infrastructure'
  3. Build hierarchical strings: ['Water__Treatment', 'Infrastructure__Transportation']
  4. Join into meta prop: content: 'Water__Treatment, Infrastructure__Transportation'

Search Index Result

The search index now contains:

{
  "practices": "Water__Treatment, Infrastructure__Transportation"
}
Enter fullscreen mode Exit fullscreen mode

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' }
// ]
Enter fullscreen mode Exit fullscreen mode

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
  },
];
Enter fullscreen mode Exit fullscreen mode

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 ?? '',
}
Enter fullscreen mode Exit fullscreen mode

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_FIELDS constants)
  • 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)