# Fumadocs Core (the core library of Fumadocs): Mixedbread
URL: /zh-TW/docs/headless/search/mixedbread
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/mixedbread.zh-TW.mdx

將 Mixedbread Search 整合至 Fumadocs

[Mixedbread](https://www.mixedbread.com) 提供語意搜尋功能，讓使用者能以自然語言搜尋，依照語意而非關鍵字找到內容。

## 設定 [#setup]

安裝相依套件：

```package-install
@mixedbread/sdk
```

### API 金鑰 [#api-key]

請先在 [Mixedbread](https://platform.mixedbread.com) 註冊，並為您的文件建立一組 [API 金鑰](https://platform.mixedbread.com/platform?next=api-keys)和一個[儲存區](https://platform.mixedbread.com/platform?next=stores)，再將兩者存放到 `.env.local` 中：

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

搜尋會在您的伺服器上執行，因此 API 金鑰絕不會暴露給瀏覽器。

### 同步文件 [#sync-documents]

預先算繪 `static.json` 路由，以便在建置時匯出搜尋文件，並在每次建置後透過指令碼將這些文件同步至您的儲存區：

```ts tab="Next.js" title="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));
}

```

```ts tab="Next.js" title="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`);

```

```json tab="Next.js" title="package.json"
{
  "scripts": {
    "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

```ts tab="React Router" title="app/routes/static.json.ts"
import { source } from '@/lib/source';
import { toDocuments } from 'fumadocs-core/search/mixedbread';

export async function loader() {
  return Response.json(await toDocuments(source));
}

```

```ts tab="React Router" title="app/routes.ts"
import { index, route, type RouteConfig } from '@react-router/dev/routes';

export default [
  index('routes/home.tsx'),
  route('static.json', 'routes/static.json.ts'), // [!code ++:1]
] satisfies RouteConfig;

```

```ts tab="React Router" title="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';

const records = JSON.parse(await fs.readFile('build/client/static.json', 'utf-8')) as DocumentRecord[];

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

console.log(`search updated: ${records.length} records`);

```

```json tab="React Router" title="package.json"
{
  "scripts": {
    "build": "react-router build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

```ts tab="TanStack Start" title="src/routes/static[.]json.ts"
import { createFileRoute } from '@tanstack/react-router';
import { source } from '@/lib/source';
import { toDocuments } from 'fumadocs-core/search/mixedbread';

export const Route = createFileRoute('/static.json')({
  server: {
    handlers: {
      GET: async () => Response.json(await toDocuments(source)),
    },
  },
});

```

```ts tab="TanStack Start" title="vite.config.ts"
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import { nitro } from 'nitro/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    tanstackStart({
      prerender: {
        enabled: true,
      },
      pages: [ // [!code ++:3]
        { path: '/static.json' }
      ],
    }),
    nitro({
      preset: 'vercel',
    }),
  ],
});

```

```ts tab="TanStack Start" title="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';

const records = JSON.parse(await fs.readFile('.vercel/output/static/static.json', 'utf-8')) as DocumentRecord[];

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

console.log(`search updated: ${records.length} records`);

```

```json tab="TanStack Start" title="package.json"
{
  "scripts": {
    "build": "vite build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

```ts tab="Waku" title="src/pages/_api/static.json.ts"
import { source } from '@/lib/source';
import { toDocuments } from 'fumadocs-core/search/mixedbread';

export async function GET() {
  return Response.json(await toDocuments(source));
}

export async function getConfig() {
  return {
    render: 'static' as const,
  } as const;
}

```

```ts tab="Waku" title="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';

const records = JSON.parse(await fs.readFile('dist/public/static.json', 'utf-8')) as DocumentRecord[];

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

console.log(`search updated: ${records.length} records`);

```

```json tab="Waku" title="package.json"
{
  "scripts": {
    "build": "waku build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

```ts tab="Astro" title="src/pages/static.json.ts"
import type { APIRoute } from 'astro';
import { source } from '@/lib/source';
import { toDocuments } from 'fumadocs-core/search/mixedbread';

export const prerender = true;

export const GET: APIRoute = async () => Response.json(await toDocuments(source));

```

```ts tab="Astro" title="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';

const records = JSON.parse(await fs.readFile('dist/static.json', 'utf-8')) as DocumentRecord[];

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

console.log(`search updated: ${records.length} records`);

```

```json tab="Astro" title="package.json"
{
  "scripts": {
    "build": "astro build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

`sync()` 會為每個頁面上傳一個檔案，並略過自上次同步後未變更的頁面。已移除頁面的檔案也會一併刪除，因此請使用專供您的文件使用的儲存區。`build` 指令碼會在每次建置時進行同步，請在您的部署平台上設定相同的變數。

### 搜尋 API [#search-api]

建立用來搜尋您儲存區的路由：

```ts tab="Next.js" title="app/api/search/route.ts"
import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';
import Mixedbread from '@mixedbread/sdk';

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

```ts tab="React Router" title="app/routes/search.ts"
import type { Route } from './+types/search';
import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';
import Mixedbread from '@mixedbread/sdk';

const server = createMixedbreadSearchAPI({
  client: new Mixedbread(), // 會讀取 `MXBAI_API_KEY`
  storeIdentifier: process.env.MIXEDBREAD_STORE_ID!,
});

export async function loader({ request }: Route.LoaderArgs) {
  return server.GET(request);
}
```

```ts tab="TanStack Start" title="src/routes/api/search.ts"
import { createFileRoute } from '@tanstack/react-router';
import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';
import Mixedbread from '@mixedbread/sdk';

const server = createMixedbreadSearchAPI({
  client: new Mixedbread(), // 會讀取 `MXBAI_API_KEY`
  storeIdentifier: process.env.MIXEDBREAD_STORE_ID!,
});

export const Route = createFileRoute('/api/search')({
  server: {
    handlers: {
      GET: async ({ request }) => server.GET(request),
    },
  },
});
```

```ts tab="Waku" title="src/pages/_api/api/search.ts"
import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';
import Mixedbread from '@mixedbread/sdk';

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

```ts tab="Astro" title="src/pages/api/search.ts"
import type { APIRoute } from 'astro';
import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';
import Mixedbread from '@mixedbread/sdk';
import { getSecret } from 'astro:env/server';

// 於每次請求時算繪，需要轉接器
export const prerender = false;

const server = createMixedbreadSearchAPI({
  client: new Mixedbread({ apiKey: getSecret('MXBAI_API_KEY') }),
  storeIdentifier: getSecret('MIXEDBREAD_STORE_ID')!,
});

export const GET: APIRoute = ({ request }) => server.GET(request);
```

### 搜尋 UI [#search-ui]

您可以考慮以下幾種實作 UI 的方式：

* 使用 [Fumadocs UI 搜尋對話方塊](/docs/next/search/mixedbread)。
* 透過內建的搜尋用戶端 hook 自行打造：

  ```ts twoslash
  import { useFetchSearch } from 'fumadocs-core/search/client';

  const { search, onSearchChange, isLoading, data } = useFetchSearch({
    api: '/api/search',
    // 每次搜尋都會向 Mixedbread 發送一次請求
    delayMs: 300,
  });
  ```

## 選項 [#options]

### 標籤篩選 [#tag-filter]

若要設定標籤篩選，請在 `static.json` 路由中將 `tag` 傳入 `toDocuments()`：

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

接著更新您的搜尋用戶端：

* **Fumadocs UI**：在搜尋 UI 中啟用[標籤篩選](/docs/next/search/mixedbread#tag-filter)。
* **搜尋用戶端**：將標籤傳入 `useFetchSearch()`。

  ```ts
  import { useFetchSearch } from 'fumadocs-core/search/client';

  const { search, onSearchChange, isLoading, data } = useFetchSearch({
    tag: '<your tag value>', // [!code ++]
    // ...
  });
  ```

### 國際化 [#internationalization]

文件會包含所屬頁面的語系，傳入 `locale` 即可搜尋特定語言的內容：

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

const { locale } = useI18n();
const { search, onSearchChange, isLoading, data } = useFetchSearch({
  locale, // [!code ++]
  // ...
});
```

## 運作原理 [#under-the-hood]

每個頁面都是一個以 URL 為鍵的檔案，並以頁面標題、URL、Breadcrumb、標籤和語系作為中繼資料。頁面的區塊 (chunk) 包括頁面標題、各個標題與段落，而每筆搜尋結果都是一個區塊：

| 區塊中繼資料    | 說明                        |
| --------- | ------------------------- |
| `kind`    | `page`、`heading` 或 `text` |
| `heading` | 所屬標題的 ID (選用)             |
| `table`   | 表格列所屬的表格 (選用)             |