---
title: "Search Client Integrations"
description: "Search Client Integrations provide client-side query execution, state coordination, and UI bindings for documentation search across multiple search engines. They bridge the gap between user input i..."
last_updated: "2026-10-12T07:53:59.827853+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/search-client-integrations"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/core/src/search/client/orama-cloud.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-cloud.ts)
- [packages/base-ui/src/components/dialog/search-orama.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search-orama.tsx)
- [packages/radix-ui/src/components/dialog/search-orama.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search-orama.tsx)
- [packages/base-ui/src/components/dialog/search-algolia.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search-algolia.tsx)
- [packages/radix-ui/src/components/dialog/search-algolia.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search-algolia.tsx)
- [packages/base-ui/src/components/dialog/search-default.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search-default.tsx)
- [packages/radix-ui/src/components/dialog/search-default.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search-default.tsx)
- [packages/core/src/search/client/orama-cloud-legacy.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-cloud-legacy.ts)
- [packages/core/src/search/client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client.ts)
- [packages/core/src/search/orama/create-server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/orama/create-server.ts)
- [packages/core/src/search/client/mixedbread.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/mixedbread.ts)
- [packages/core/src/search/mixedbread.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/mixedbread.ts)
- [packages/core/src/search/client/orama-static.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-static.ts)
- [packages/create-app/template/+orama-cloud/@app/components/search.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/create-app/template/%2Borama-cloud/%40app/components/search.tsx)
- [packages/base-ui/src/components/dialog/search.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search.tsx)
- [packages/radix-ui/src/components/dialog/search.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search.tsx)
- [packages/core/src/search/client/fetch.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/fetch.ts)
- [packages/preview/src/components/ai/search.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx)
- [packages/core/src/search/client/flexsearch-static.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/flexsearch-static.ts)
- [packages/core/src/search/client/algolia.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/algolia.ts)
- [packages/create-app/src/plugins/orama-cloud.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/create-app/src/plugins/orama-cloud.ts)
- [packages/core/src/search/flexsearch.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/flexsearch.ts)
- [packages/base-ui/src/contexts/search.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/search.tsx)
- [packages/radix-ui/src/contexts/search.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/search.tsx)
- [packages/core/src/search/server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/server.ts)
- [packages/core/src/search/orama-cloud-legacy.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/orama-cloud-legacy.ts)
- [packages/core/src/search/orama-cloud.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/orama-cloud.ts)
- [packages/core/src/search/orama/create-db.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/orama/create-db.ts)
- [packages/core/src/search/orama/search/advanced.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/orama/search/advanced.ts)
- [packages/core/src/search/orama/search/simple.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/orama/search/simple.ts)
</details>

## Overview

Search Client Integrations provide client-side query execution, state coordination, and UI bindings for documentation search across multiple search engines. They bridge the gap between user input inside search dialogs and backend or in-browser search indexes, handling debouncing, result caching, dependency tracking, and asynchronous client loading.

Sources: [packages/core/src/search/client.ts:78-197](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client.ts#L78-L197)

## Search Client React Hook Architecture

### Hook API and State Structure

The `useDocsSearch` hook manages user search input, debounced query execution, error handling, loading states, and result caching across different search provider backends. It accepts `clientOptions` containing preset configurations or a direct `SearchClient` instance, alongside an optional `customDeps` dependency list.

```typescript
export function useDocsSearch(
  clientOptions: ClientPreset & {
    /**
     * The debounced delay for performing a search (in ms).
     * .
     * @defaultValue 100
     */
    delayMs?: number;

    /**
     * still perform search even if query is empty.
     *
     * @defaultValue false
     */
    allowEmpty?: boolean;
  },
  customDeps?: DependencyList,
): UseDocsSearch {
```
Sources: [packages/core/src/search/client.ts:83-100](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client.ts#L83-L100)

The return value conforms to the `UseDocsSearch` interface, exposing the raw input string, its setter, and a query status object holding loading flags, result arrays, or errors.

```typescript
interface UseDocsSearch {
  search: string;
  setSearch: (v: string) => void;
  query: {
    isLoading: boolean;
    data?: SortedResult[] | 'empty';
    error?: Error;
  };
}
```
Sources: [packages/core/src/search/client.ts:14-22](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client.ts#L14-L22)

### Client Preset Resolution and Deprecation

`ClientPreset` supports both legacy string-keyed configuration types and modern direct `SearchClient` references. When a legacy `type` string is provided, `useDocsSearch` dynamically imports the respective client module and constructs the search client asynchronously.

```typescript
export type ClientPreset =
  | ({
      /**
       * @deprecated Pass `client: fetchClient(...)` instead.
       */
      type: 'fetch';
    } & FetchOptions)
  | ({
      /**
       * @deprecated Pass `client: oramaStaticClient(...)` instead.
       */
      type: 'static';
    } & StaticOptions)
  | ({
      /**
       * @deprecated Pass `client: algoliaClient(...)` instead.
       */
      type: 'algolia';
    } & AlgoliaOptions)
  | ({
      /**
       * @deprecated Pass `client: oramaCloudClient(...)` instead.
       */
      type: 'orama-cloud';
    } & OramaCloudOptions)
  | ({
      /**
       * @deprecated Pass `client: oramaCloudLegacyClient(...)` instead.
       */
      type: 'orama-cloud-legacy';
    } & OramaCloudLegacyOptions)
  | ({
      /**
       * @deprecated Pass `client: flexsearchStaticClient(...)` instead.
       */
      type: 'flexsearch-static';
    } & FlexsearchStaticOptions)
  | ({
      /**
       * @deprecated Use `createMixedbreadSearchAPI` from `fumadocs-core/search/mixedbread` instead.
       * This client-side approach exposes your API key in the browser.
       * The server-side approach keeps the key secure and uses `client: fetchClient(...)` on the client.
       */
      type: 'mixedbread';
    } & MixedbreadOptions)
  | {
      client: SearchClient;
    };
```
Sources: [packages/core/src/search/client.ts:24-71](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client.ts#L24-L71)

| Preset Type | Module Import Path | Options Interface |
| :--- | :--- | :--- |
| `fetch` | `@/search/client/fetch` | `FetchOptions` |
| `static` | `@/search/client/orama-static` | `StaticOptions` |
| `algolia` | `@/search/client/algolia` | `AlgoliaOptions` |
| `orama-cloud` | `@/search/client/orama-cloud` | `OramaCloudOptions` |
| `orama-cloud-legacy` | `@/search/client/orama-cloud-legacy` | `OramaCloudLegacyOptions` |
| `flexsearch-static` | `./client/flexsearch-static` | `FlexsearchStaticOptions` |
| `mixedbread` | `@/search/client/mixedbread` | `MixedbreadOptions` |

Sources: [packages/core/src/search/client.ts:110-143](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client.ts#L110-L143)

> [!WARNING]
> Using string-keyed presets like `type: 'fetch'` is deprecated. Pass a pre-instantiated client such as `client: fetchClient(...)` directly to avoid runtime dynamic imports and ensure type-safe dependency tracking.

Sources: [packages/core/src/search/client.ts:24-71](https://github.com/blade47/fumadopos/blob/HEAD/packages/core/src/search/client.ts#L24-L71)

### Debouncing and Task Interruption

Search input is processed through `useDebounce` with a configurable `delayMs` defaulting to `100` milliseconds. Setting `allowEmpty` to `true` forces search execution even when the query string is empty.

```typescript
const { delayMs = 100, allowEmpty = false, ...clientRest } = clientOptions;
const [search, setSearch] = useState('');
const [results, setResults] = useState<SortedResult[] | 'empty'>('empty');
const [error, setError] = useState<Error>();
const [isLoading, setIsLoading] = useState(false);
const debouncedValue = useDebounce(search, delayMs);
```
Sources: [packages/core/src/search/client.ts:101-106](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client.ts#L101-L106)

Active search tasks are tracked via `activeTaskRef`. When dependencies or debounced values change before an in-flight search resolves, the previous task is immediately marked as interrupted, preventing stale responses from overwriting newer query results.

```typescript
const [activeDeps, setActiveDeps] = useState<DependencyList | null>(null);
const activeTaskRef = useRef<{
  deps: DependencyList;
  start: () => void;
  interrupt: boolean;
} | null>(null);

if (
  !isEqualShallow(activeDeps, deps) &&
  (!activeTaskRef.current || !isEqualShallow(activeTaskRef.current.deps, deps))
) {
  if (activeTaskRef.current) activeTaskRef.current.interrupt = true;

  activeTaskRef.current = {
    deps,
    interrupt: false,
    async start() {
      try {
        setIsLoading(true);

        let res: SortedResult[] | 'empty';
        if (debouncedValue.length === 0 && !allowEmpty) res = 'empty';
        else res = await (await client).search(debouncedValue);

        if (!this.interrupt) {
          setActiveDeps(deps);
          setError(undefined);
          setResults(res);
        }
      } catch (err) {
        if (!this.interrupt) setError(err as Error);
      } finally {
        if (!this.interrupt) setIsLoading(false);
      }
    },
  };
  void activeTaskRef.current.start();
}
```
Sources: [packages/core/src/search/client.ts:154-191](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client.ts#L154-L191)

> [!NOTE]
> If `allowEmpty` is `false` and the debounced query length is zero, the hook bypasses the search client call entirely and sets the result data directly to `'empty'`.

Sources: [packages/core/src/search/client.ts:174-176](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client.ts#L174-L176)

### Query Caching via Fetch Client

The built-in `fetchClient` utilizes an internal `Map` cache (`globalCache` by default, or a custom map passed via `FetchOptions`) keyed by the fully constructed request URL string. Before executing a network request, it checks for a cached `SortedResult[]` entry.

```typescript
const globalCache = new Map();

export function fetchClient({
  api = '/api/search',
  locale,
  tag,
  cache = globalCache,
}: FetchOptions = {}): SearchClient {
  return {
    deps: [api, locale, tag],
    async search(query) {
      const url = new URL(api, window.location.origin);
      url.searchParams.set('query', query);
      if (locale) url.searchParams.set('locale', locale);
      if (tag) url.searchParams.set('tag', Array.isArray(tag) ? tag.join(',') : tag);

      const key = url.toString();
      const cached = cache.get(key);
      if (cached) return cached;

      const res = await fetch(url);
      if (!res.ok) throw new Error(await res.text());
      const result = (await res.json()) as SortedResult[];
      cache.set(key, result);
      return result;
    },
  };
```
Sources: [packages/core/src/search/client/fetch.ts:25-51](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/fetch.ts#L25-L51)

## Default Route Querying via Fetch

### Fetch Client Configuration and Parameters

The `fetchClient` factory function constructs a client adhering to the `SearchClient` interface by querying an API endpoint over HTTP. It accepts `FetchOptions` to customize the network request behavior and parameter serialization.

| Option Key | Type | Default Value | Purpose |
| :--- | :--- | :--- | :--- |
| `api` | `string` | `'/api/search'` | API route for the search endpoint, supporting absolute URLs. |
| `tag` | `string \| string[]` | `undefined` | Filter results with one or more tags (comma-joined if an array). |
| `locale` | `string` | `undefined` | Filter search results by language locale. |
| `cache` | `Map<string, SortedResult[]>` | `globalCache` | Storage map used for query caching across searches. |

Sources: [packages/core/src/search/client/fetch.ts:4-23](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/fetch.ts#L4-L23), [packages/core/src/search/client/fetch.ts:25-32](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/fetch.ts#L25-L32)

When `search(query)` is invoked, `fetchClient` builds a `URL` instance using `window.location.origin` as the base, appending query parameters for `query`, `locale`, and `tag`. If `tag` is provided as an array, its elements are joined with commas.

```typescript
async search(query) {
  const url = new URL(api, window.location.origin);
  url.searchParams.set('query', query);
  if (locale) url.searchParams.set('locale', locale);
  if (tag) url.searchParams.set('tag', Array.isArray(tag) ? tag.join(',') : tag);

```
Sources: [packages/core/src/search/client/fetch.ts:35-40](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/fetch.ts#L35-L40)

> [!WARNING]
> If the fetch response status is not OK (`!res.ok`), `fetchClient` reads the response body as text and throws it immediately as an `Error`.

Sources: [packages/core/src/search/client/fetch.ts:45-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/fetch.ts#L45-L46)

### Default Search Dialog Integration

The `DefaultSearchDialog` component integrates `fetchClient` with [Base UI Components](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/base-ui-components) and Radix primitives, wrapping `useDocsSearch` to power accessible search dialogs.

```tsx
export default function DefaultSearchDialog({
  defaultTag,
  tags = [],
  api,
  delayMs,
  allowClear = false,
  links = [],
  footer,
  ...props
}: DefaultSearchDialogProps) {
  const { locale } = useI18n();
  const [tag, setTag] = useState(defaultTag);
  const { search, setSearch, query } = useDocsSearch({
    client: fetchClient({
      api,
      locale,
      tag,
    }),
    delayMs,
  });
```
Sources: [packages/radix-ui/src/components/dialog/search-default.tsx:50-69](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search-default.tsx#L50-L69)

| Prop Name | Type | Default Value | Description |
| :--- | :--- | :--- | :--- |
| `links` | `SearchLink[]` | `[]` | Default fallback links displayed when no search query is active. |
| `defaultTag` | `string` | `undefined` | Initial active filter tag. |
| `tags` | `TagItem[]` | `[]` | Available filter tags rendered in the dialog footer. |
| `api` | `string` | `undefined` | Custom search API URL passed to `fetchClient`. |
| `delayMs` | `number` | `undefined` | Debounce delay for executing search requests. |
| `allowClear` | `boolean` | `false` | Allows users to clear active tag filters. |
| `footer` | `ReactNode` | `undefined` | Custom React node rendered alongside the tag list in the footer. |

Sources: [packages/radix-ui/src/components/dialog/search-default.tsx:25-48](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search-default.tsx#L25-L48)

`DefaultSearchDialog` synchronizes the dialog's tag state with locale changes and passes the resulting query items to `SearchDialogList`. When the query data evaluates to `'empty'`, it falls back to rendering the memoized default `links` converted into `SortedResult` items.

```tsx
const defaultItems = useMemo<SortedResult[] | null>(() => {
  if (links.length === 0) return null;
  return links.map(([name, link]) => ({
    type: 'page',
    id: name,
    content: name,
    url: link,
  }));
}, [links]);
```
Sources: [packages/radix-ui/src/components/dialog/search-default.tsx:70-78](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search-default.tsx#L70-L78)

```tsx
<SearchDialogList items={query.data !== 'empty' ? query.data : defaultItems} />
```
Sources: [packages/radix-ui/src/components/dialog/search-default.tsx:93-93](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search-default.tsx#L93-L93)

> [!TIP]
> If `tags` array length is greater than zero, `DefaultSearchDialog` automatically renders a `TagsList` component inside `SearchDialogFooter` equipped with tag selection handlers and clear options.

Sources: [packages/radix-ui/src/components/dialog/search-default.tsx:96-104](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dialog/search-default.tsx#L96-L104)

## Orama Cloud Client Adapters

### Query Pipeline and Index Modes

Fumadocs provides client adapters for both modern Orama Cloud (`@orama/core`) and legacy Orama clients (`@oramacloud/client`) via `oramaCloudClient` and `oramaCloudLegacyClient`. Each adapter constructs a `SearchClient` configured with dependencies and an asynchronous `search(query)` execution pipeline.

```typescript
export function oramaCloudClient(options: OramaCloudOptions): SearchClient {
  const { index = 'default', client, params: extraParams, tag } = options;

  return {
    deps: [index, client, tag],
```
Sources: [packages/core/src/search/client/orama-cloud.ts:40-44](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-cloud.ts#L40-L44)

```typescript
export function oramaCloudLegacyClient(options: OramaCloudLegacyOptions): SearchClient {
  const { index = 'default', client, params: extraParams = {}, tag } = options;

  return {
    deps: [index, client, tag],
```
Sources: [packages/core/src/search/client/orama-cloud-legacy.ts:40-44](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-cloud-legacy.ts#L40-L44)

The client supports two indexing strategies: `default` (structured Fumadocs JSON indexes) and `crawler` (web crawler indexes). When `index` is set to `'crawler'`, search queries target the web crawler datasource, grouping results into page and text items.

```typescript
if (index === 'crawler') {
  const result = await client.search({
    datasources: [],
    ...extraParams,
    term: query,
    where: {
      category: tag
        ? {
            eq: tag.slice(0, 1).toUpperCase() + tag.slice(1),
          }
        : undefined,
      ...extraParams?.where,
    },
    limit: 10,
  });
```
Sources: [packages/core/src/search/client/orama-cloud.ts:49-63](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-cloud.ts#L49-L63)

In `default` mode, queries invoke `client.search` with a maximum limit of 20 results, grouping matches by `page_id` with up to 7 results per group. The resulting grouped hits are mapped into `SortedResult` objects using `createContentHighlighter` to process markdown highlights.

```typescript
const result = await client.search({
  datasources: [],
  ...extraParams,
  term: query,
  limit: 20,
  where: removeUndefined({
    tag,
    ...extraParams?.where,
  }),
  groupBy: {
    properties: ['page_id'],
    max_results: 7,
    ...extraParams?.groupBy,
  },
});
```
Sources: [packages/core/src/search/client/orama-cloud.ts:88-102](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-cloud.ts#L88-L102)

> [!WARNING]
> In `oramaCloudClient`, if the total mapped result list exceeds 80 items, the client slices the array to a maximum length of 80 before returning.

Sources: [packages/core/src/search/client/orama-cloud.ts:131-131](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-cloud.ts#L131-L131)

### Orama Search Dialog Integration

The `OramaSearchDialog` component integrates Orama Cloud search into application dialogs using [Base UI Components](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/base-ui-components), accepting client instances, parameters, and tag configurations.

```tsx
export default function OramaSearchDialog({
  client,
  searchOptions,
  tags = [],
  defaultTag,
  showOrama = true,
  allowClear = false,
  index,
  footer,
  links = [],
  ...props
}: OramaSearchDialogProps) {
  const { locale } = useI18n();
  const [tag, setTag] = useState(defaultTag);
  const { search, setSearch, query } = useDocsSearch({
    client: oramaCloudClient({
      client,
      index,
      params: searchOptions,
      locale,
      tag,
    }),
  });
```
Sources: [packages/base-ui/src/components/dialog/search-orama.tsx:54-76](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search-orama.tsx#L54-L76)

| Prop Name | Type | Default Value | Description |
| :--- | :--- | :--- | :--- |
| `client` | `OramaCloudOptions['client']` | — | Orama Cloud client instance. |
| `searchOptions` | `OramaCloudOptions['params']` | `undefined` | Additional search parameters. |
| `index` | `OramaCloudOptions['index']` | `undefined` | Index type (`'default'` or `'crawler'`). |
| `tags` | `TagItem[]` | `[]` | List of filter tags for the dialog footer. |
| `defaultTag` | `string` | `undefined` | Initial active filter tag. |
| `showOrama` | `boolean` | `true` | Controls the visibility of the "Search powered by Orama" branding label. |
| `allowClear` | `boolean` | `false` | Allows users to clear active tag filters. |
| `links` | `SearchLink[]` | `[]` | Fallback links when no search query is active. |

Sources: [packages/base-ui/src/components/dialog/search-orama.tsx:25-49](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search-orama.tsx#L25-L49)

## Static Orama and Flexsearch Engines

### Static Orama Client Implementation

Fumadocs provides client execution for in-browser static search indices through `oramaStaticClient`. The client accepts `StaticOptions` configuration parameters governing index fetching, schema initialization, tag filtering, and localized database management.

```typescript
export interface StaticOptions {
  /**
   * Where to download exported search indexes (URL)
   *
   * @defaultValue '/api/search'
   */
  from?: string;

  initOrama?: (locale?: string) => AnyOrama | Promise<AnyOrama>;

  /**
   * Filter results with specific tag(s).
   */
  tag?: string | string[];

  /**
   * Filter by locale (unsupported at the moment)
   */
  locale?: string;

  /**
   * extra options for search
   */
  search?: Partial<SearchParams<Orama<unknown>>>;
}
```
Sources: [packages/core/src/search/client/orama-static.ts:8-32](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-static.ts#L8-L32)

| Option Name | Type | Default Value | Description |
| :--- | :--- | :--- | :--- |
| `from` | `string` | `'/api/search'` | URL endpoint to download exported search indexes. |
| `initOrama` | `(locale?: string) => AnyOrama \| Promise<AnyOrama>` | `create({ schema: { _: 'string' }, language: locale })` | Custom Orama instance initializer per locale. |
| `tag` | `string \| string[]` | `undefined` | Filter results by specific tag(s). |
| `locale` | `string` | `undefined` | Filter by locale. |
| `search` | `Partial<SearchParams<Orama<unknown>>>` | `undefined` | Extra parameters for search execution. |

Sources: [packages/core/src/search/client/orama-static.ts:8-32](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-static.ts#L8-L32)

Database loading is handled asynchronously via `loadDB`, which fetches exported index payloads and populates an in-memory database map cache keyed by locale. When internationalization data types are encountered, instances are built per locale entry, whereas single-index payloads initialize a default empty locale key.

```typescript
async function loadDB(
  from: string,
  initOrama: StaticOptions['initOrama'] = (locale) =>
    create({ schema: { _: 'string' }, language: locale }),
): Promise<Database> {
  const res = await fetch(from);

  if (!res.ok)
    throw new Error(
      `failed to fetch exported search indexes from ${from}, make sure the search database is exported and available for client.`,
    );

  const data = (await res.json()) as ExportedData;
  const dbs: Database = new Map();

  if (data.type === 'i18n') {
    await Promise.all(
      Object.entries(data.data).map(async ([k, v]) => {
        const db = await initOrama(k);

        load(db, v);
        dbs.set(k, {
          type: v.type,
          db,
        });
      }),
    );
  } else {
    const db = await initOrama();
    load(db, data);
    dbs.set('', {
      type: data.type,
      db,
    });
  }

  return dbs;
}
```
Sources: [packages/core/src/search/client/orama-static.ts:45-82](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-static.ts#L45-L82)

> [!WARNING]
> If a requested locale is missing from the loaded database collection, `oramaStaticClient` logs a warning message and falls back to the first available database value in the map.

Sources: [packages/core/src/search/client/orama-static.ts:104-109](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/orama-static.ts#L104-L109)

Execution branches between simple and advanced search helpers depending on the database type property (`'simple'` or `'advanced'`). Simple searches delegate to `searchSimple` with tolerance and title boost configurations, while advanced searches use `searchAdvanced` with `groupBy` page ID properties and tag filters.

```typescript
export async function searchSimple(
  db: Orama<typeof simpleSchema>,
  query: string,
  params: Partial<SearchParams<Orama<typeof simpleSchema>, SimpleDocument>> = {},
): Promise<SortedResult[]> {
  const highlighter = createContentHighlighter(query);
  const result = await search(db, {
    term: query,
    tolerance: 1,
    ...params,
    boost: {
      title: 2,
      ...('boost' in params ? params.boost : undefined),
    },
  });
```
Sources: [packages/core/src/search/orama/search/simple.ts:5-19](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/orama/search/simple.ts#L5-L19)

```typescript
export async function searchAdvanced(
  db: Orama<typeof advancedSchema>,
  query: string,
  tag: string | string[] = [],
  {
    mode = 'fulltext',
    ...override
  }: Partial<SearchParams<Orama<typeof advancedSchema>, AdvancedDocument>> = {},
): Promise<SortedResult[]> {
  if (typeof tag === 'string') tag = [tag];

  const params = {
    limit: 60,
    mode,
    ...override,
    where: removeUndefined({
      tags:
        tag.length > 0
          ? {
              containsAll: tag,
            }
          : undefined,
      ...override.where,
    }),
    groupBy: {
      properties: ['page_id'],
      maxResult: 8,
      ...override.groupBy,
    },
    properties: mode === 'fulltext' ? ['content'] : ['content', 'embeddings'],
  } as SearchParams<typeof db, AdvancedDocument>;
```
Sources: [packages/core/src/search/orama/search/advanced.ts:6-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/orama/search/advanced.ts#L6-L36)

### Static Flexsearch Client Implementation

Local in-browser client execution using Flexsearch is provided by `flexsearchStaticClient`. The client accepts `FlexsearchStaticOptions` to configure endpoint fetching, default locales, and tags.

```typescript
export interface FlexsearchStaticOptions {
  /**
   * @defaultValue `/api/search`
   */
  from?: string;
  locale?: string;
  tag?: string | string[];
}
```
Sources: [packages/core/src/search/client/flexsearch-static.ts:6-13](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/flexsearch-static.ts#L6-L13)

| Option Name | Type | Default Value | Description |
| :--- | :--- | :--- | :--- |
| `from` | `string` | `'/api/search'` | URL endpoint to download exported Flexsearch indexes. |
| `locale` | `string` | `''` | Target locale key for querying. |
| `tag` | `string \| string[]` | `undefined` | Optional tag filter for search queries. |

Sources: [packages/core/src/search/client/flexsearch-static.ts:6-13](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/flexsearch-static.ts#L6-L13)

The initialization routine checks whether `window` is defined before executing the asynchronous fetch function `init`. It parses exported JSON payloads, iterating over internationalized raw records or standard document maps to instantiate Flexsearch document wrappers via `initDocument`.

```typescript
function initDocument(data: Record<string, string>) {
  const document = createDocument();
  for (const [k, v] of Object.entries(data)) document.import(k, v);
  return document;
}
```
Sources: [packages/core/src/search/client/flexsearch-static.ts:15-19](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/flexsearch-static.ts#L15-L19)

```typescript
async function init(from: string) {
  const res = await fetch(from);

  if (!res.ok)
    throw new Error(
      `failed to fetch exported search indexes from ${from}, make sure the search database is exported and available for client.`,
    );

  const data = (await res.json()) as ExportedData;
  const dbs = new Map<string, Document<Doc>>();

  if (data.type === 'i18n') {
    for (const [locale, map] of Object.entries(data.raw)) {
      dbs.set(locale, initDocument(map));
    }

    return dbs;
  } else {
    dbs.set('', initDocument(data.raw));
  }

  return dbs;
```
Sources: [packages/core/src/search/client/flexsearch-static.ts:43-64](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/flexsearch-static.ts#L43-L64)

> [!NOTE]
> `flexsearchStaticClient` registers dependency tracking for `from`, `locale`, and `tag`, returning an empty array if the requested locale database has not been loaded into the map cache.

Sources: [packages/core/src/search/client/flexsearch-static.ts:33-39](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/flexsearch-static.ts#L33-L39)

## Algolia Search Client and Grouping

### Algolia Lite Client Adapter

The Algolia client implementation provides search integration via `algoliaClient`, accepting `AlgoliaOptions` to configure the target index, search client instance, tag filters, locale, and an optional custom `onSearch` handler function.

```typescript
export interface AlgoliaOptions {
  indexName: string;
  client: LiteClient;

  /**
   * Filter results with specific tag.
   */
  tag?: string;

  locale?: string;

  onSearch?: (
    query: string,
    tag?: string,
    locale?: string,
  ) => Promise<{
    results: SearchResponse<BaseIndex>[];
  }>;
}
```
Sources: [packages/core/src/search/client/algolia.ts:6-24](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/algolia.ts:6-24)

| Option Name | Type | Default Value | Description |
| :--- | :--- | :--- | :--- |
| `indexName` | `string` | *required* | Target Algolia index name. |
| `client` | `LiteClient` | *required* | Algolia lite client instance. |
| `tag` | `string` | `undefined` | Optional tag filter string (`tag:${tag}`). |
| `locale` | `string` | `undefined` | Optional locale identifier passed to custom search handlers. |
| `onSearch` | `Function` | `undefined` | Custom search function overriding default requests. |

Sources: [packages/core/src/search/client/algolia.ts:6-24](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/algolia.ts:6-24)

> [!WARNING]
> If the search query string contains only whitespace or is empty after trimming (`query.trim().length === 0`), `algoliaClient` immediately returns an empty array without executing a network request.

Sources: [packages/core/src/search/client/algolia.ts:58-59](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/algolia.ts:58-59)

### Hierarchical Hit Grouping and Execution Flow

When executing a search request, `algoliaClient` checks whether a custom `onSearch` function is provided; otherwise, it invokes `client.searchForHits` with a request configuration containing a distinct value of `5`, `hitsPerPage` set to `10`, and an optional tag filter. The resulting hits are processed by `groupResults` to construct a hierarchical list of pages, headings, and text blocks.

```typescript
const result = onSearch
  ? await onSearch(query, tag, locale)
  : await client.searchForHits<BaseIndex>({
      requests: [
        {
          type: 'default',
          indexName,
          query,
          distinct: 5,
          hitsPerPage: 10,
          filters: tag ? `tag:${tag}` : undefined,
        },
      ],
    });

```
Sources: [packages/core/src/search/client/algolia.ts:61-75](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/algolia.ts:61-75)

```mermaid
sequenceDiagram
    participant AlgoliaSearchDialog as AlgoliaSearchDialog
    participant algoliaClient as algoliaClient
    participant groupResults as groupResults
    AlgoliaSearchDialog->>algoliaClient: search(query)
    algoliaClient->>groupResults: groupResults(hits)
    groupResults-->>algoliaClient: SortedResult[]
    algoliaClient-->>AlgoliaSearchDialog: highlighted results
```
Sources: [packages/base-ui/src/components/dialog/search-algolia.tsx:61-67](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dialog/search-algolia.tsx#L61-L67), [packages/core/src/search/client/algolia.ts:26-52](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/algolia.ts#L26-L52), [packages/core/src/search/client/algolia.ts:58-87](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/algolia.ts#L58-L87)

The grouping function `groupResults` tracks scanned URLs in a `Set<string>`. For each unique URL encountered, it pushes a parent page result into the accumulator array before appending individual section or text hits with constructed anchor URLs.

```typescript
function groupResults(hits: Hit<BaseIndex>[]): SortedResult[] {
  const grouped: SortedResult[] = [];
  const scannedUrls = new Set<string>();

  for (const hit of hits) {
    if (!scannedUrls.has(hit.url)) {
      scannedUrls.add(hit.url);

      grouped.push({
        id: hit.url,
        type: 'page',
        breadcrumbs: hit.breadcrumbs,
        url: hit.url,
        content: hit.title,
      });
    }

    grouped.push({
      id: hit.objectID,
      type: hit.content === hit.section ? 'heading' : 'text',
      url: hit.section_id ? `${hit.url}#${hit.section_id}` : hit.url,
      content: hit.content,
    });
  }

  return grouped;
}
```
Sources: [packages/core/src/search/client/algolia.ts:26-52](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/algolia.ts:26-52)

## Mixedbread Client Vector Retrieval

### Mixedbread Options and Heading Transformations

The Mixedbread client integration supports vector search queries against a remote store via `mixedbreadClient` or server-side endpoints through `createMixedbreadSearchAPI`. `MixedbreadOptions` defines the configuration required to connect to a store instance using the Mixedbread SDK client.

```typescript
export interface MixedbreadOptions {
  /**
   * The identifier of the store to search in
   */
  storeIdentifier: string;

  /**
   * The Mixedbread SDK client instance
   */
  client: Mixedbread;

  /**
   * Filter results with specific tag.
   */
  tag?: string;

  /**
   * Filter by locale (unsupported at the moment)
   */
  locale?: string;
}
```
Sources: [packages/core/src/search/client/mixedbread.ts:8-28](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/mixedbread.ts:8-28)

| Option Name | Type | Default Value | Description |
| :--- | :--- | :--- | :--- |
| `storeIdentifier` | `string` | *required* | The identifier of the store to search in. |
| `client` | `Mixedbread` | *required* | The Mixedbread SDK client instance. |
| `tag` | `string` | `undefined` | Filter results with a specific tag. |
| `locale` | `string` | `undefined` | Filter by locale (unsupported at the moment). |

Sources: [packages/core/src/search/client/mixedbread.ts:8-28](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/mixedbread.ts:8-28)

> [!WARNING]
> `mixedbreadClient` is deprecated because running client-side SDK queries exposes your API key in the browser. The recommended approach is to use `createMixedbreadSearchAPI` on the server and connect with a fetch client on the client.

Sources: [packages/core/src/search/client/mixedbread.ts:63-67](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/mixedbread.ts:63-67)

### Search Execution and Chunk Processing

When a search query is submitted, `mixedbreadClient` queries `client.stores.search` requesting a top-k limit of 10 items with metadata enabled and tag filtering applied via `generated_metadata.tag`. Each returned search result chunk is expanded into structured page and heading results.

```typescript
async search(query) {
  if (!query.trim()) {
    return [];
  }

  const res = await client.stores.search({
    query,
    store_identifiers: [storeIdentifier],
    top_k: 10,
    filters: {
      key: 'generated_metadata.tag',
      operator: 'eq',
      value: tag,
    },
    search_options: {
      return_metadata: true,
    },
  });
```
Sources: [packages/core/src/search/client/mixedbread.ts:73-90](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/mixedbread.ts:73-90)

```typescript
return (res.data as StoreSearchResult[]).flatMap((item) => {
  const metadata = item.generated_metadata;

  const url = metadata.url || '#';
  const title = metadata.title || 'Untitled';

  const chunkResults: SortedResult[] = [
    {
      id: `${item.file_id}-${item.chunk_index}-page`,
      type: 'page',
      content: title,
      url,
    },
  ];

  const headingTitle =
    item.type === 'text' && item.text ? extractHeadingTitle(item.text) : '';

  if (headingTitle) {
    slugger.reset();

    chunkResults.push({
      id: `${item.file_id}-${item.chunk_index}-heading`,
      type: 'heading',
      content: headingTitle,
      url: `${url}#${slugger.slug(headingTitle)}`,
    });
  }

  return chunkResults;
});
```
Sources: [packages/core/src/search/client/mixedbread.ts:92-122](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/mixedbread.ts:92-122)

The helper function `extractHeadingTitle` inspects text content to verify whether it starts with a markdown heading indicator (`#`). If it does, the first line is split, cleaned with `removeMarkdown` with image alt text disabled, and slugified using `github-slugger` for anchor URL generation.

```typescript
function extractHeadingTitle(text: string): string {
  const trimmedText = text.trim();

  if (!trimmedText.startsWith('#')) {
    return '';
  }

  const lines = trimmedText.split('\n');
  const firstLine = lines[0]?.trim();

  if (firstLine) {
    // Use remove-markdown to convert to plain text and remove colons
    return removeMd(firstLine, {
      useImgAltText: false,
    });
  }

  return '';
}
```
Sources: [packages/core/src/search/client/mixedbread.ts:43-61](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/mixedbread.ts:43-61)

> [!NOTE]
> `slugger.reset()` is invoked before processing each heading title to ensure deterministic slug generation across chunk result iterations.

Sources: [packages/core/src/search/client/mixedbread.ts:111-111](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/client/mixedbread.ts:111-111)

## Search Context and Keyboard Shortcuts

### Global Search Context and Provider Architecture

The `SearchProvider` component establishes global search state using `SearchContext`, exposing options for dialog preloading, custom empty-state links, and hotkey bindings. When `preload` is enabled (defaulting to `true`), the search dialog state is initialized to `undefined` until the dialog is opened, triggering lazy loading of `DefaultSearchDialog`.

```tsx
export function SearchProvider({
  SearchDialog = DefaultSearchDialog,
  children,
  preload = true,
  options,
  hotKey = DEFAULT_HOT_KEYS,
  links,
}: SearchProviderProps) {
  const [isOpen, setIsOpen] = useState(preload ? false : undefined);
  const onKeyDown = useEffectEvent((e: KeyboardEvent) => {
    if (hotKey.every((v) => (typeof v.key === 'string' ? e.key === v.key : v.key(e)))) {
      setIsOpen((open) => !open);
      e.preventDefault();
    }
  });

  useEffect(() => {
    window.addEventListener('keydown', onKeyDown);
    return () => {
      window.removeEventListener('keydown', onKeyDown);
    };
  }, []);

  return (
    <SearchContext
      value={useMemo(
        () => ({
          enabled: true,
          open: isOpen ?? false,
          hotKey,
          setOpenSearch: setIsOpen,
        }),
        [isOpen, hotKey],
      )}
    >
      <Suspense fallback={null}>
        {isOpen !== undefined && (
          <SearchDialog
            open={isOpen}
            onOpenChange={setIsOpen}
            // @ts-expect-error -- insert prop for official UIs
            links={links}
            {...options}
          />
        )}
      </Suspense>

      {children}
    </SearchContext>
  );
}
```
Sources: [packages/base-ui/src/contexts/search.tsx:112-162](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/search.tsx#L112-L162)

| Property | Type | Default Value | Description |
| :--- | :--- | :--- | :--- |
| `preload` | `boolean` | `true` | Preload search dialog before opening it. |
| `links` | `SearchLink[]` | `undefined` | Custom links to be displayed if search is empty. |
| `hotKey` | `HotKey[]` | `DEFAULT_HOT_KEYS` | Hotkeys for triggering the search dialog. |
| `SearchDialog` | `ComponentType<SharedProps>` | `DefaultSearchDialog` | Replace default search dialog (e.g. Algolia Search). |
| `options` | `Partial<SharedProps & Record<string, unknown>>` | `undefined` | Additional properties passed to the dialog. |

Sources: [packages/base-ui/src/contexts/search.tsx:36-69](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/search.tsx#L36-L69)

> [!NOTE]
> The `SearchOnly` wrapper component inspects `search.enabled` from the context and renders its children only when search has been successfully initialized via a provider.

Sources: [packages/base-ui/src/contexts/search.tsx:164-171](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/search.tsx#L164-L171)

### Hotkey Bindings and Platform Detection

Keyboard shortcuts are managed via global window event listeners registered inside `SearchProvider`. The default hotkey configuration `DEFAULT_HOT_KEYS` combines a modifier key condition with the `k` key.

```tsx
const DEFAULT_HOT_KEYS: HotKey[] = [
  {
    key: (e) => e.metaKey || e.ctrlKey,
    display: <MetaOrControl />,
  },
  {
    key: 'k',
    display: 'K',
  },
];
```
Sources: [packages/base-ui/src/contexts/search.tsx:99-108](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/search.tsx#L99-L108)

The `MetaOrControl` helper component checks `window.navigator.userAgent` against a regular expression testing for Windows or Linux platforms to dynamically switch the displayed shortcut symbol between `⌘` and `Ctrl`.

```tsx
function MetaOrControl() {
  const [key, setKey] = useState('⌘');

  useEffect(() => {
    if (/Windows|Linux/i.test(window.navigator.userAgent)) setKey('Ctrl');
  }, []);

  return key;
}
```
Sources: [packages/base-ui/src/contexts/search.tsx:89-97](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/search.tsx#L89-L97)

> [!WARNING]
> `onKeyDown` evaluates every configured hotkey item in `hotKey` using `.every()`. If `v.key` is a function, it receives the native `KeyboardEvent`; otherwise, it matches `e.key === v.key`. When all match, `setIsOpen` toggles the dialog state and `e.preventDefault()` is invoked.

Sources: [packages/base-ui/src/contexts/search.tsx:121-126](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/search.tsx#L121-L126)

### AI Search Context and Preview Trigger Integration

The `AISearch` component provides a separate React context specifically for AI chat search integration, wrapping state for `open`, `setOpen`, and `chat` helpers initialized via `@ai-sdk/react`.

```tsx
export function AISearch({ children }: { children: ReactNode }) {
  const [open, setOpen] = useState(false);
  const chat = useChat({
    id: 'search',
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),
  });

  return (
    <Context value={useMemo(() => ({ chat, open, setOpen }), [chat, open])}>{children}</Context>
  );
}
```
Sources: [packages/preview/src/components/ai/search.tsx:292-304](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx#L292-L304)

Trigger components such as `AISearchTrigger` consume this context to toggle visibility and update `data-state` attributes (`open` or `closed`). When `position` is set to `float`, the trigger button fixes its layout at the bottom corner with transition animations.

```tsx
export function AISearchTrigger({
  position = 'default',
  className,
  ...props
}: ComponentProps<'button'> & { position?: 'default' | 'float' }) {
  const { open, setOpen } = useAISearchContext();

  return (
    <button
      data-state={open ? 'open' : 'closed'}
      className={cn(
        position === 'float' && [
          'fixed bottom-4 gap-3 w-24 inset-e-[calc(--spacing(4)+var(--removed-body-scroll-bar-size,0px))] shadow-lg z-20 transition-[translate,opacity]',
          open && 'translate-y-10 opacity-0',
        ],
        className,
      )}
      onClick={() => setOpen(!open)}
      {...props}
    >
      {props.children}
    </button>
  );
}
```
Sources: [packages/preview/src/components/ai/search.tsx:306-329](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx#L306-L329)

## Related

- [Search Server Indexing](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/search-server-indexing)
- [Base UI Components](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/base-ui-components)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
