The useSearch hook provides a powerful client-side search engine using Orama for documentation sites. It handles index creation, search queries, and result formatting with built-in support for stemming, grouping, faceting, and customizable result types.
useSearch creates an in-memory search index from your sitemap data and provides instant search results with fuzzy matching, boosting, and tolerance controls. It's designed to work seamlessly with documentation structures that include pages, sections, subsections, component parts, and exports.
import { useSearch } from '@mui/internal-docs-infra/useSearch';
function SearchComponent() {
const { results, search, isReady, defaultResults, buildResultUrl } = useSearch({
sitemap: () => import('./sitemap'),
maxDefaultResults: 10,
enableStemming: true,
});
const handleSearch = (query: string) => {
search(query);
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
{results.results.map((group) =>
group.items.map((result) => (
<a key={result.id} href={buildResultUrl(result)}>
{result.title}
</a>
)),
)}
</div>
);
}
The hook expects a sitemap that follows this structure:
interface Sitemap {
schema: Record<string, any>;
data: Record<string, SitemapSectionData>;
}
interface SitemapSectionData {
title: string;
prefix: string;
pages: SitemapPage[];
}
interface SitemapPage {
title: string;
slug: string;
path: string;
description: string;
keywords?: string[];
sections?: Record<string, SitemapSection>;
parts?: Record<string, SitemapPart>;
exports?: Record<string, SitemapExport>;
}
Enable stemming to improve search quality by reducing words to their root form:
const { search } = useSearch({
sitemap: () => import('./sitemap'),
enableStemming: true, // Default: true
});
When enabled, searches for "running" will also match "run", "runs", and "runner".
Control how tolerant the search is to typos:
const { search } = useSearch({
sitemap: () => import('./sitemap'),
tolerance: 2, // Default: 1
});
Higher tolerance values allow more character differences between the query and results.
Configure how many results to return:
const { search, defaultResults } = useSearch({
sitemap: () => import('./sitemap'),
maxDefaultResults: 10, // Default: undefined (all pages)
limit: 20, // Default: 20
});
When maxDefaultResults is undefined, all pages are included in the default results (but not headings, exports, or parts).
Include page categories in result groups for better organization:
const { search } = useSearch({
sitemap: () => import('./sitemap'),
includeCategoryInGroup: true, // Groups become "Overview Pages" vs just "Pages"
});
Boost specific fields to prioritize certain result types. You can import defaultSearchBoost and extend it:
import { useSearch, defaultSearchBoost } from '@mui/internal-docs-infra/useSearch';
const { search } = useSearch({
sitemap: () => import('./sitemap'),
boost: {
...defaultSearchBoost,
title: 3, // Override specific values
},
});
The default boost values are:
export const defaultSearchBoost = {
type: 100,
group: 100,
slug: 2,
path: 2,
title: 2,
page: 6,
pageKeywords: 15,
description: 1.5,
part: 1.5,
export: 1.3,
sectionTitle: 50,
section: 3,
subsection: 2.5,
props: 1.5,
dataAttributes: 1.5,
cssVariables: 1.5,
sections: 0.7,
subsections: 0.3,
keywords: 1.5,
};
The hook returns results grouped by category. Each group contains items with a discriminated union type:
type SearchResults = { group: string; items: SearchResult[] }[];
type SearchResult =
| PageSearchResult
| PartSearchResult
| ExportSearchResult
| SectionSearchResult
| SubsectionSearchResult;
Top-level documentation pages:
interface PageSearchResult extends BaseSearchResult {
type: 'page';
page?: string;
pageKeywords?: string;
sections?: string;
subsections?: string;
}
Top-level headings within a page:
interface SectionSearchResult extends BaseSearchResult {
type: 'section';
section: string;
}
Nested headings within a page:
interface SubsectionSearchResult extends BaseSearchResult {
type: 'subsection';
subsection: string;
}
Component parts with API documentation:
interface PartSearchResult extends BaseSearchResult {
type: 'part';
part: string;
export: string;
props?: string;
dataAttributes?: string;
cssVariables?: string;
}
Exported functions/components with API documentation:
interface ExportSearchResult extends BaseSearchResult {
type: 'export';
export: string;
props?: string;
dataAttributes?: string;
cssVariables?: string;
}
All result types extend this base interface:
interface BaseSearchResult {
id?: string;
title?: string;
description?: string;
slug: string;
path: string;
sectionTitle: string;
prefix: string;
keywords?: string;
score?: number;
group?: string;
}
Override how pages are converted to search entries:
const { search } = useSearch({
sitemap: () => import('./sitemap'),
flattenPage: (page, sectionData) => {
return [
{
type: 'page',
title: page.title,
slug: page.slug,
path: page.path,
description: page.description,
sectionTitle: sectionData.title,
prefix: sectionData.prefix,
},
];
},
});
Override how search hits are formatted:
const { search } = useSearch({
sitemap: () => import('./sitemap'),
formatResult: (hit) => {
return {
type: 'page',
id: hit.id,
title: hit.document.title.toUpperCase(),
description: hit.document.description,
slug: hit.document.slug,
path: hit.document.path,
sectionTitle: hit.document.sectionTitle,
prefix: hit.document.prefix,
score: hit.score,
};
},
});
resultsObject containing current search results, count, and elapsed time:
results: {
results: SearchResults; // Array of grouped results
count: number; // Total number of matches
elapsed: ElapsedTime; // Time taken for search
}
isReadyBoolean indicating whether the search index has been created and is ready:
isReady: boolean;
searchFunction to perform a search query with optional parameters:
search: (value: string, options?: SearchBy) => Promise<void>;
The options parameter supports:
facets: Faceted search configurationgroupBy: Group results by a fieldlimit: Override the default result limitoffset: Pagination offsetwhere: Filter conditionsdefaultResultsDefault results shown when search is empty, in the same format as results:
defaultResults: {
results: SearchResults; // Array of grouped results
count: number; // Total number of default results
elapsed: ElapsedTime; // Always { raw: 0, formatted: '0ms' }
}
buildResultUrlFunction to build URLs from search results with proper hash fragments:
buildResultUrl: (result: SearchResult) => string;
The buildResultUrl function handles different result types:
#section-slug to the page path#subsection-slug to the page path#part-name to the page path#export-name or #api-reference to the page pathExample output:
// Page result
buildResultUrl(pageResult); // "/components/button"
// Section result
buildResultUrl(sectionResult); // "/components/button#usage"
// Export result
buildResultUrl(exportResult); // "/components/button#api-reference"
The search index is built asynchronously when the component mounts. Use the isReady flag to show loading states:
const { isReady, search, results } = useSearch({
sitemap: () => import('./sitemap'),
});
if (!isReady) {
return <div>Loading search index...</div>;
}
Index creation is fast but happens only once per mount. Consider memoizing the sitemap import for optimal performance.
The useSearch hook requires a sitemap with page metadata. Here's how to set one up:
Use the transformMarkdownMetadata remark plugin to automatically extract titles, descriptions, and sections from your MDX pages:
// Each MDX page will have metadata extracted:
export const metadata = {
title: 'Button',
description: 'A clickable button component.',
keywords: ['button', 'click'],
};
Create a sitemap index file using createSitemap:
// app/sitemap/index.ts
import { createSitemap } from '@mui/internal-docs-infra/createSitemap';
import Components from '../components/page.mdx';
import Functions from '../functions/page.mdx';
export const sitemap = createSitemap(import.meta.url, {
Components,
Functions,
});
Import the sitemap dynamically in your search component:
const { search, results } = useSearch({
sitemap: () => import('../sitemap'),
});
See createSitemap for the full API and transformMarkdownMetadata for metadata extraction options.
type, title, and slug to prioritize exact matchesbuildResultUrl to ensure consistent URL formatting across result typesisReady before allowing searches to prevent errorsinterface UseSearchOptions {
/** Function that returns a promise resolving to sitemap data */
sitemap: () => Promise<{ sitemap?: Sitemap }>;
/** Maximum number of default results to show */
maxDefaultResults?: number;
/** Search tolerance for fuzzy matching */
tolerance?: number;
/** Maximum number of search results */
limit?: number;
/** Enable stemming and stopwords (uses English by default) */
enableStemming?: boolean;
/** Boost values for different result types and fields */
boost?: Partial<Record<string, number>>;
/** Include page categories in groups: "Overview Pages" vs "Pages" */
includeCategoryInGroup?: boolean;
/** Custom function to flatten sitemap pages into search results */
flattenPage?: (page: SitemapPage, sectionData: SitemapSectionData) => SearchResult[];
/** Custom function to format Orama search hits into typed results */
formatResult?: <TDocument = unknown>(hit: Result<TDocument>) => SearchResult;
}
type SearchBy<T> = Pick<
SearchParams<Orama<T>>,
'facets' | 'groupBy' | 'limit' | 'offset' | 'where'
>;
type SearchResults = { group: string; items: SearchResult[] }[];
interface UseSearchResult<T> {
/** Current search results with count and timing */
results: { results: SearchResults; count: number; elapsed: ElapsedTime };
/** Whether the search index is ready */
isReady: boolean;
/** Function to update search value and get new results */
search: (value: string, by?: SearchBy<T>) => Promise<void>;
/** Default results shown when search is empty */
defaultResults: { results: SearchResults; count: number; elapsed: ElapsedTime };
/** Build a URL from a search result */
buildResultUrl: (result: SearchResult) => string;
}
createSitemap - Define sitemap data for search indexingtransformMarkdownMetadata - Extract page metadata from MDXloadServerSitemap - Runtime sitemap loadingwithDocsInfra - Next.js plugin that configures the build