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

Meilisearch を Fumadocs に統合する

[Meilisearch](https://www.meilisearch.com) は、タイプミス許容とフィルタリング機能を備えたオープンソースの検索エンジンです。セルフホストすることも、Meilisearch Cloud を利用することもできます。

## セットアップ [#setup]

依存関係をインストールします。

```package-install
meilisearch
```

### API キー [#api-keys]

[セルフホスト](https://www.meilisearch.com/docs/learn/self_hosted/getting_started_with_self_hosted_meilisearch)するか [Meilisearch Cloud](https://www.meilisearch.com/cloud) を利用して Meilisearch インスタンスを起動し、その認証情報を `.env.local` に保存します(`NEXT_PUBLIC_` の部分は、Vite ベースのフレームワークでは `VITE_`、Astro では `PUBLIC_` に置き換えてください)。

```dotenv
NEXT_PUBLIC_MEILISEARCH_HOST="URL of your instance"
NEXT_PUBLIC_MEILISEARCH_SEARCH_KEY="search API key"
MEILISEARCH_ADMIN_KEY="Default Admin API Key"
```

検索キーはブラウザに公開されるため、`docs` インデックスの検索のみが可能なキーを[作成してください](https://www.meilisearch.com/docs/learn/security/basic_security)。管理キーは外部に公開しないでください。

### ドキュメントの同期 [#sync-documents]

`static.json` ルートを事前レンダリングしてビルド時に検索ドキュメントをエクスポートし、ビルドのたびにスクリプトで Meilisearch へ同期します。

```ts tab="Next.js" title="app/static.json/route.ts"
import { source } from '@/lib/source';
import { toDocuments } from 'fumadocs-core/search/meilisearch';

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/meilisearch';
import { Meilisearch } from 'meilisearch';

// 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[];

const client = new Meilisearch({
  host: process.env.NEXT_PUBLIC_MEILISEARCH_HOST!,
  apiKey: process.env.MEILISEARCH_ADMIN_KEY!,
});

await sync(client, {
  indexName: 'docs',
  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/meilisearch';

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/meilisearch';
import { Meilisearch } from 'meilisearch';

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

const client = new Meilisearch({
  host: process.env.VITE_MEILISEARCH_HOST!,
  apiKey: process.env.MEILISEARCH_ADMIN_KEY!,
});

await sync(client, {
  indexName: 'docs',
  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/meilisearch';

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/meilisearch';
import { Meilisearch } from 'meilisearch';

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

const client = new Meilisearch({
  host: process.env.VITE_MEILISEARCH_HOST!,
  apiKey: process.env.MEILISEARCH_ADMIN_KEY!,
});

await sync(client, {
  indexName: 'docs',
  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/meilisearch';

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/meilisearch';
import { Meilisearch } from 'meilisearch';

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

const client = new Meilisearch({
  host: process.env.VITE_MEILISEARCH_HOST!,
  apiKey: process.env.MEILISEARCH_ADMIN_KEY!,
});

await sync(client, {
  indexName: 'docs',
  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/meilisearch';

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/meilisearch';
import { Meilisearch } from 'meilisearch';

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

const client = new Meilisearch({
  host: process.env.PUBLIC_MEILISEARCH_HOST!,
  apiKey: process.env.MEILISEARCH_ADMIN_KEY!,
});

await sync(client, {
  indexName: 'docs',
  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` スクリプトはビルドのたびに同期を行うため、デプロイ先のプラットフォームにも同じ環境変数を設定してください。

### 検索 UI [#search-ui]

UI の実装方法には、いくつかの選択肢があります。

* [Fumadocs UI の検索ダイアログ](/docs/next/search/meilisearch)を使用する。

* 組み込みの検索クライアントフックを使用して独自に構築する。

  ```ts twoslash
  import { Meilisearch } from 'meilisearch';
  import { useMeilisearch } from 'fumadocs-core/search/client';

  const client = new Meilisearch({
    host: 'host url',
    apiKey: 'search api key',
  });

  const { search, onSearchChange, isLoading, data } = useMeilisearch({
    client,
    indexName: 'docs',
  });
  ```

  ### $Fumadocs

  | Prop        | Type                              | Description                              |
  | ----------- | --------------------------------- | ---------------------------------------- |
  | `client`    | `Meilisearch`                     | Meilisearch client with a search API key |
  | `indexName` | `string`                          |                                          |
  | `tag?`      | `string \| string[] \| undefined` | Filter results with specific tag(s).     |
  | `locale?`   | `string \| undefined`             | Filter results by locale.                |

* Meilisearch の公式クライアントを直接使用する。

## オプション [#options]

### タグフィルター [#tag-filter]

タグフィルターを設定するには、`static.json` ルートで `toDocuments()` に `tag` を渡します。

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

あわせて、検索クライアントも更新してください。

* **Fumadocs UI**: 検索 UI で[タグフィルター](/docs/next/search/meilisearch#tag-filter)を有効にします。
* **検索クライアント**: `useMeilisearch()` にタグを渡します。

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

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

### 国際化 [#internationalization]

ドキュメントには各ページのロケールが含まれているため、`locale` を渡すと特定の言語で検索できます。

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

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

### インデックス設定 [#index-settings]

`sync()` が設定するのは `searchableAttributes` と `filterableAttributes` のみで、シノニムやタイポ許容などのその他の設定はそのまま保持されます。これらの設定は Meilisearch のダッシュボードまたは `meilisearch` クライアントから調整できます。

## 内部の仕組み [#under-the-hood]

各見出しと段落はそれぞれ1つのドキュメントとして扱われ、ページタイトル、URL、パンくずリストを持ちます。検索対象は `content` のみで、検索クライアントは結果をページ単位でグループ化します。

| 属性        | 説明                          |
| --------- | --------------------------- |
| `type`    | `page`、`heading` または `text` |
| `content` | ページタイトル、見出し、または段落           |
| `heading` | 所属する見出しの ID (任意)            |
| `tag`     | 結果を絞り込むためのタグ (任意)           |
| `locale`  | ページのロケール (任意)               |