Fumadocs

Mixedbread

Integrate Mixedbread Search with Fumadocs

Mixedbread provides semantic search, users can search with natural language and find content by meaning rather than keywords.

Setup

Install dependencies:

npm install @mixedbread/sdk

API Key

Sign up at Mixedbread, create an API key and a store for your docs, and store them in .env.local:

MXBAI_API_KEY="your API key"
MIXEDBREAD_STORE_ID="your store ID"

Searching runs on your server, the API key is never exposed to the browser.

Sync Documents

Pre-render a static.json route to export the search documents on build, and sync them to your store with a script after each build:

app/static.json/route.ts
import { source } from '@/lib/source';
import { toDocuments } from 'fumadocs-core/search/mixedbread';

export const revalidate = false;

export async function GET() {
  return Response.json(await toDocuments(source));
}
scripts/sync-content.ts
import * as fs from 'node:fs/promises';
import { type DocumentRecord, sync } from 'fumadocs-core/search/mixedbread';
import Mixedbread from '@mixedbread/sdk';

// Next.js writes it under `route-cache` when built with an adapter, like on Vercel
const [file] = await Array.fromAsync(fs.glob('.next/server/{app,route-cache/*/*/$}/static.json.body'));
const records = JSON.parse(await fs.readFile(file, 'utf-8')) as DocumentRecord[];

await sync(new Mixedbread(), {
  storeIdentifier: process.env.MIXEDBREAD_STORE_ID!,
  documents: records,
});

console.log(`search updated: ${records.length} records`);
package.json
{
  "scripts": {
    "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}

sync() uploads a file for each page, and skips the pages unchanged since the last sync. Files of removed pages are deleted, use a store dedicated to your docs. The build script syncs on every build, set the same variables on your deployment platform.

Search API

Create a route to search your store:

app/api/search/route.ts
import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';
import Mixedbread from '@mixedbread/sdk';

export const { GET } = createMixedbreadSearchAPI({
  client: new Mixedbread(), // reads `MXBAI_API_KEY`
  storeIdentifier: process.env.MIXEDBREAD_STORE_ID!,
});

Search UI

You can consider different options for implementing the UI:

  • Using Fumadocs UI search dialog.

  • Build your own using the built-in search client hook:

    import {  } from 'fumadocs-core/search/client';
    
    const { , , ,  } = ({
      : '/api/search',
      // every search is a request to Mixedbread
      : 300,
    });

Options

Tag Filter

To configure tag filtering, pass a tag to toDocuments() in the static.json route:

toDocuments(source, { tag: (page) => page.slugs[0] });

And update your search client:

  • Fumadocs UI: Enable Tag Filter on Search UI.

  • Search Client: pass a tag to useFetchSearch().

    import { useFetchSearch } from 'fumadocs-core/search/client';
    
    const { search, onSearchChange, isLoading, data } = useFetchSearch({
      tag: '<your tag value>', 
      // ...
    });

Internationalization

Documents include the locale of their pages, pass locale to search in a language:

import { useI18n } from 'fumadocs-ui/contexts/i18n';
import { useFetchSearch } from 'fumadocs-core/search/client';

const { locale } = useI18n();
const { search, onSearchChange, isLoading, data } = useFetchSearch({
  locale, 
  // ...
});

Under the Hood

Each page is a file keyed by its URL, with the page title, URL, breadcrumbs, tags and locale as metadata. Its chunks are the title, headings and paragraphs of the page, every result is a chunk:

Chunk MetadataDescription
kindpage, heading or text
headingID of the heading it belongs to (optional)
tableTable of a table row (optional)

How is this guide?

Last updated on