Skip to content
How-Tos

Federated search

time to complete
45 minutes

A common requirement for storefronts is to provide a unified search experience that returns both content (blog posts, articles, pages) and commerce (products) results from a single search input. This tutorial shows you how to implement a federated search approach that performs parallel searches against a content index and the Adobe Commerce GraphQL API.

In this tutorial, you’ll learn how to create a combined search experience that displays results from content and commerce in a single view, providing shoppers with comprehensive search results across your entire site.

What You’ll Build

By the end of this tutorial, you’ll have:

  • A content index configuration that indexes the pages, blogs, and content for your site
  • A custom search block that performs parallel searches against content and commerce
  • A tabbed interface displaying separate results for content and products
  • A fully functional federated search experience

Prerequisites

Before starting this tutorial, make sure you have:

  • A working Commerce storefront on Edge Delivery Services
  • Basic understanding of JavaScript and asynchronous operations
  • Familiarity with customizing blocks in your code repository
  • Access to configure content indexing for your site (similar to enrichment configuration)
  • Adobe Commerce backend configured with product catalog

Overview

Developing this federated search solution involves:

  1. Creating an index for the pages and content of your site
  2. Building a custom search block that fetches both content and commerce data
  3. Implementing a tabbed UI to display results separately
  4. Processing search queries in parallel for optimal performance

This method keeps content and commerce searches distinct while delivering a unified experience to the user.

Step 1: Create the Content Index Configuration

Create a helix-query.yaml file in the root of your repository with this configuration:

version: 1
indices:
content:
target: /content-index.json
exclude:
- '/blog/index'
include:
- /blog/**
- /pages/**
properties:
title:
select: head > meta[property="og:title"]
value: |
attribute(el, 'content')
description:
select: head > meta[name="description"]
value: |
attribute(el, 'content')
lastModified:
select: none
value: |
parseTimestamp(headers['last-modified'], 'ddd, DD MMM YYYY hh:mm:ss GMT')
content:
select: main
value: |
textContent(el)

What this does:

This configuration tells Edge Delivery Services to automatically index your site’s content pages. When you publish pages, the system:

  • Crawls pages matching /blog/** and /pages/** (excludes /blog/index)
  • Extracts metadata (title, description, content) from each page
  • Generates a searchable JSON index at /content-index.json

Key fields:

  • select: CSS selector or metadata to extract
  • value: Extraction method (e.g., attribute(), textContent())
  • See the AEM indexing documentation for advanced property configurations

Commit and push helix-query.yaml. The index generates automatically when you publish pages.

Verify: After publishing pages, access https://[your-domain]/content-index.json to see the indexed pages as JSON.

Content index JSON output showing indexed pages with title, description, and content fields

Step 2: Create Sample Content Pages

To validate your federated search setup, publish content pages that can be indexed. For example, create and publish blog posts in a /blog directory:

Create a few pages like:

  • /blog/winter-collection
  • /blog/summer-sale
  • /blog/holiday-shopping-guide

Here’s an example of what your content page might look like:

Example blog page for Winter Collection

Step 3: Create the Combined Search Block

Next, create a custom block to perform the federated search. This block processes both the content and commerce searches using the Product Discovery drop-in.

Create a new directory: blocks/combined-search/

Create blocks/combined-search/combined-search.js:

/**
* Combined Search Block
* Performs parallel searches against content index and commerce search API
*/
// Import the search function from the product discovery dropin
import { search } from '@dropins/storefront-product-discovery/api.js';
let contentIndex = null;
/**
* 1. Fetch and cache the content index
*/
async function getContentIndex() {
if (contentIndex) return contentIndex;
const response = await fetch('/content-index.json');
const data = await response.json();
contentIndex = data.data;
return contentIndex;
}
/**
* 2. Search the content index (client-side filtering)
*/
function searchContent(query, index) {
const searchTerm = query.toLowerCase();
return index.filter((item) => {
return item.title?.toLowerCase().includes(searchTerm)
|| item.description?.toLowerCase().includes(searchTerm)
|| item.content?.toLowerCase().includes(searchTerm);
});
}
/**
* 3. Search commerce products using the search dropin
*/
async function searchProducts(query) {
try {
const results = await search({
phrase: query || '',
currentPage: 1,
pageSize: 12,
filter: [
{ attribute: 'visibility', in: ['Search', 'Catalog, Search'] }
]
});
return results?.items || [];
} catch (error) {
console.error('Error searching for products:', error);
return [];
}
}
/**
* 4. Perform parallel searches and render results
*/
async function performSearch(query, block) {
// Perform both searches in parallel for optimal performance
const [contentIndex, products] = await Promise.all([
getContentIndex(),
searchProducts(query)
]);
// Filter content results client-side
const contentResults = searchContent(query, contentIndex);
// Render results (implementation details omitted - see reference link)
renderContentResults(contentResults, block.querySelector('.content-results'));
renderProductResults(products, block.querySelector('.product-results'));
}
/**
* 5. Initialize the block
*/
export default async function decorate(block) {
// Get search query from URL
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('q') || '';
// Build the UI with tabs for Products and Content
block.innerHTML = `
<div class="combined-search-container">
<div class="search-input-wrapper">
<input type="text" class="search-input" />
<button class="search-button">🔍</button>
</div>
<div class="tabs">
<button class="tab-button active" data-tab="products">Products</button>
<button class="tab-button" data-tab="content">Content</button>
</div>
<div class="tab-content">
<div class="tab-pane active" data-tab="products">
<div class="product-results"></div>
</div>
<div class="tab-pane" data-tab="content">
<div class="content-results"></div>
</div>
</div>
</div>
`;
// Safely set the search input value to prevent XSS
const searchInput = block.querySelector('.search-input');
searchInput.value = query;
// Setup event listeners for search and tabs (implementation details omitted)
setupSearchHandlers(block, performSearch);
setupTabs(block);
// Pre-fetch content index for faster subsequent searches
getContentIndex();
// If there's a query in URL, perform the search
if (query) {
performSearch(query, block);
}
}
// Helper functions: renderContentResults(), renderProductResults(), setupTabs(), setupSearchHandlers()
// See reference implementation for complete rendering and UI logic

Key concepts explained:

  1. Content index caching (getContentIndex): The content index is fetched once and cached in memory for subsequent searches, improving performance.

  2. Client-side content search (searchContent): Because the content index is a JSON file, we can filter it client-side using the JavaScript filter() method. This is fast and doesn’t require additional API calls.

  3. Product search (searchProducts): Products are searched using the Commerce search() function from the Product Discovery drop-in, which handles the GraphQL query to Adobe Commerce. The visibility filter ensures only searchable products are returned.

  4. Parallel execution (performSearch): Both searches execute in parallel using Promise.all(), ensuring that the slower query doesn’t delay the faster one.

  5. Tabbed UI: Results are organized in separate tabs (Products and Content) for a clean user experience.

Step 4: Add Styling for the Search Block

Create blocks/combined-search/combined-search.css and add the styles from the reference implementation .

Step 5: Create the Search Results Page

Create a dedicated page in your project to host the combined search block. Using your content authoring tool (for example, DA.live, Google Docs, SharePoint), add a new page at /search.

Example search page

This addition creates a page that uses your combined-search block.

Step 6: Test Your Implementation

Quick verification:

  1. Content index: Visit https://[your-domain]/content-index.json - verify pages are indexed with metadata
  2. Product search: Navigate to /search?q=winter - check Products tab shows results with images, titles, prices
  3. Content search: Try /search?q=blog - verify Content tab displays titles and descriptions
  4. Search UI: Type a query, press Enter - confirm URL updates with ?q= parameter and tabs refresh
  5. Edge cases: Test empty results, special characters, short queries (1-2 chars)

Expected result:

Federated search results showing search input, Products and Content tabs, with product cards displayed in the Products tab */}

Complete Example

Here’s a complete example of how the federated search works in practice:

User Journey:

  1. Shopper types “winter” in the search box
  2. System performs parallel searches:
    • Content search finds blog posts about “Winter Collection 2024”
    • Product search finds products tagged with “winter”
  3. Results display in separate tabs:
    • Products Tab: Displays winter jackets, boots, accessories
    • Content Tab: Displays blog posts, buying guides, seasonal content

Products tab showing winter product results with images and prices

Content tab showing blog posts and articles related to winter

Performance Considerations

Troubleshooting

Common Issues

  • Content index returns 404 error

    Verify that you’ve correctly configured the query.yaml file and that you’ve published pages that match your include patterns. The index is only generated after the configuration is in place and pages are published.

  • No products appear in search results

    Check that your GraphQL endpoint is correctly configured. In the browser console, verify that the GraphQL query is executing without errors. Ensure your Adobe Commerce instance has products with the searched terms.

  • Content search returns too many irrelevant results

    Improve the search algorithm in the searchContent() function. Consider implementing:

    • Ranking by relevance (title matches weighted higher than content matches)
    • Fuzzy matching for better results
    • Filtering out pages with robots: noindex
  • Search is slow or times out

    • Check the size of your content index (should be under 5MB for good performance)
    • Ensure you’re caching the content index properly (see getContentIndex() function)
    • Consider using a CDN to cache the content-index.json file

Getting Help

If you encounter issues not covered here:

Summary

In this tutorial, you learned how to:

  • ✅ Configure a content index using query.yaml to index the pages for your site
  • ✅ Create a custom combined search block that queries both content and commerce
  • ✅ Implement parallel async searches for optimal performance
  • ✅ Build a tabbed interface to organize and display federated search results
  • ✅ Handle edge cases and provide a smooth user experience

You now have a fully functional federated search solution that combines content and commerce search in your storefront. This approach provides a simple yet effective way to help shoppers discover both products and content across your entire site.

The implementation is maintainable, performant, and can be extended with additional features like filtering, sorting, and advanced ranking algorithms as your requirements grow.