Orientation
Content Processing
Search Infrastructure
Rendering and UI
Developer Tooling
How It Works
The following files were used as context for generating this wiki page:
Search Clients in the Fumadocs ecosystem provide a unified interface for document search across diverse backends. They bridge the gap between UI components and search indexes, ensuring that users experience consistent behavior regardless of whether the search is performed against a local index (static/in-memory), a remote API, or a managed search provider.
By abstracting search logic behind a standard SearchClient interface, this subsystem allows developers to toggle between providers without refactoring UI components. This is achieved by utilizing the useDocsSearch hook, which manages loading states, result aggregation, and debouncing, enabling a seamless integration into documentation dialogs.
The design relies on a separation of concerns where the search engine handles indexing and querying, while the client implementation handles communication and result formatting. This architecture ensures that as indexing strategies evolve, the front-end remains decoupled from the low-level search implementation details.
useDocsSearch Orchestration HookThe useDocsSearch hook is the primary entry point for managing search state in the browser. It implements a reactive workflow that responds to query updates and provider changes.
Mechanistically, it maintains four internal states: search (the current string), results (the SortedResult data), error, and isLoading. It utilizes useDebounce to throttle input updates, preventing excessive calls to remote APIs.
Crucially, it manages task cancellation using a ref object (activeTaskRef). When dependencies (like the client configuration or debounced query) change, the hook interrupts any pending execution to prevent race conditions. The core execution logic:
allowEmpty flag).client.search() method.activeTaskRef.current.interrupt is true, it skips result updates (178|, 184|, 186|).Sources: packages/core/src/search/client.ts:83-197
Each search client implements the SearchClient interface, which defines a mandatory search method returning Awaitable<SortedResult[]>.
Sources: packages/core/src/search/client.ts:73-76, packages/core/src/search/client/fetch.ts:27-52, packages/core/src/search/client/orama-static.ts:95-118, packages/core/src/search/client/algolia.ts:54-89, packages/core/src/search/client/flexsearch-static.ts:23-41
Static clients download search indexes once and cache them in memory. This improves search speed by eliminating round-trips for subsequent queries.
For orama-static, the mechanism is:
getDBCached checks a module-level cache Map keyed by the index URL (86|).loadDB fetches the index, initializes an Orama instance using create, and populates it using load (74|).101|, 112|).Warning
The search index database is cached in a module-level Map variable cache. If the application dynamically changes the from URL without full page reloads, the client will fail to update its local index until a full refresh occurs.
Sources: packages/core/src/search/client/orama-static.ts:34-93
The UI components (SearchDialog) interact with the search clients via common Context providers. For example, OramaSearchDialog wraps the core dialog and injects a client generated by oramaCloudClient.
The interaction flow is:
SearchDialogInput.onSearchChange updates the state in the hook provided by the parent SearchProvider.useDocsSearch reacts to the state change, triggers the SearchClient.search method, and updates the data result.SearchDialogList renders the result array provided by the useSearchList hook.Sources: packages/base-ui/src/components/dialog/search-orama.tsx:68-76, packages/radix-ui/src/components/dialog/search.tsx:299-403
When multiple hits correspond to a single documentation page, clients must perform grouping to avoid redundant list items. In the algoliaClient, this is handled by groupResults:
Set<string> named scannedUrls tracks unique URLs (28|).Sources: packages/core/src/search/client/algolia.ts:26-52
Developers can integrate the search subsystem by creating a dialog component and wrapping it in a provider. Here is a minimal implementation using the default fetchClient:
// Example: Using the Search Dialog with the Fetch client
import { fetchClient } from 'fumadocs-core/search/client/fetch';
import { useDocsSearch } from 'fumadocs-core/search/client';
function MySearch() {
const { search, setSearch, query } = useDocsSearch({
client: fetchClient({ api: '/api/search' }),
});
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
{query.isLoading && <span>Searching...</span>}
<ul>
{Array.isArray(query.data) && query.data.map(item => (
<li key={item.id}>{item.content}</li>
))}
</ul>
</div>
);
}Sources: packages/core/src/search/client/fetch.ts:27-52, packages/core/src/search/client.ts:83-197