# 機能設計書：38-検索プラグイン

## 1. 機能概要

### 1.1 機能の目的
検索プラグイン（@payloadcms/plugin-search）は、Payload CMSに全文検索インデックス機能を追加するプラグインである。コレクションのドキュメント変更を自動的に検索インデックスコレクションに同期し、サイト全体の検索機能を実現する。

### 1.2 機能の範囲
- 検索インデックス用コレクション（search）の自動生成
- ドキュメント変更時の自動インデックス同期
- ドキュメント削除時のインデックス自動削除
- 優先度（priority）によるソート対応
- 一括再インデックス機能
- 下書き・公開状態に応じた同期制御
- 多言語対応（ローカライズ）
- カスタム同期関数（beforeSync）のサポート

### 1.3 関連する業務フロー
- サイト内検索機能の実装
- 複数コレクションを横断した統合検索
- 検索結果の優先度調整
- インデックスの手動再構築

## 2. 機能詳細

### 2.1 プラグイン初期化

#### 処理フロー
```
searchPlugin(config)
    ↓
ローカライズ設定の解決
    ↓
対象コレクションにフック登録
    ↓
searchコレクション生成
    ↓
拡張されたConfig返却
```

#### 設定オプション
```typescript
type SearchPluginConfig = {
  collections?: string[]              // インデックス対象コレクション
  beforeSync?: BeforeSync             // 同期前処理関数
  defaultPriorities?: {               // デフォルト優先度
    [collection: string]: number | ((doc) => number | Promise<number>)
  }
  deleteDrafts?: boolean              // ドラフト削除同期（デフォルト: true）
  syncDrafts?: boolean                // ドラフト同期（デフォルト: false）
  localize?: boolean                  // ローカライズ対応
  reindexBatchSize?: number           // 再インデックスバッチサイズ（デフォルト: 50）
  searchOverrides?: Partial<CollectionConfig>  // searchコレクションオーバーライド
  skipSync?: SkipSyncFunction         // 同期スキップ判定関数
}
```

### 2.2 生成されるsearchコレクション

#### フィールド構造
| フィールド名 | 型 | 説明 |
|-------------|-----|------|
| title | text | 検索タイトル（readOnly） |
| priority | number | 検索優先度（サイドバー配置） |
| doc | relationship | 参照ドキュメント（インデックス付き、readOnly） |
| docUrl | ui | ドキュメントへのリンク（カスタムコンポーネント） |

#### アクセス制御
```typescript
access: {
  create: () => false,  // 自動生成のみ
  read: () => true,     // 全員読み取り可能
}
```

#### エンドポイント
```typescript
{
  method: 'post',
  path: '/reindex',
  handler: generateReindexHandler(pluginConfig),
}
```

### 2.3 同期処理

#### afterChange: syncWithSearch
```
ドキュメント変更
    ↓
skipSync判定
    ↓ (スキップ時は終了)
DocToSync形式にデータ整形
    ↓
重複チェック（syncedDocsSet）
    ↓
beforeSync呼び出し（設定時）
    ↓
create: searchコレクションに新規作成
update: 既存検索ドキュメント更新
    ↓
ドラフト処理（deleteDrafts時）
```

#### beforeDelete: deleteFromSearch
```
ドキュメント削除
    ↓
searchコレクションから対応レコード削除
（doc.relationTo + doc.value で特定）
```

### 2.4 再インデックス処理

#### POST /api/search/reindex
```
リクエスト: { collections: ['pages', 'posts'] }
    ↓
権限検証（delete, update権限必須）
    ↓
コレクション検証
    ↓
トランザクション開始
    ↓
各コレクションごと:
  - 既存インデックス削除
  - バッチ処理でドキュメント取得
  - 各ロケールごとに同期
    ↓
トランザクションコミット
    ↓
結果レスポンス
```

### 2.5 下書き・公開状態の制御

```typescript
// syncDrafts: false（デフォルト）
const doSync = syncDrafts || (!syncDrafts && status !== 'draft')

// deleteDrafts: true（デフォルト）
if (deleteDrafts && status === 'draft') {
  // 公開バージョンがあればインデックス維持
  // なければインデックス削除
}
```

## 3. データモデル

### 3.1 DocToSync型
```typescript
type DocToSync = {
  doc: {
    relationTo: string    // コレクションスラッグ
    value: string         // ドキュメントID
  }
  title: string
  [key: string]: any      // カスタムフィールド
}
```

### 3.2 BeforeSync型
```typescript
type BeforeSync = (args: {
  originalDoc: Record<string, any>
  payload: Payload
  req: PayloadRequest
  searchDoc: DocToSync
}) => DocToSync | Promise<DocToSync>
```

### 3.3 SkipSyncFunction型
```typescript
type SkipSyncFunction = (args: {
  collectionSlug: string
  doc: any
  locale: string | undefined
  req: PayloadRequest
}) => boolean | Promise<boolean>
```

### 3.4 ER図
```
+--------------------+       +------------------+
|    Source Doc      |       |    search        |
+--------------------+       +------------------+
| id (PK)            |<------| doc.value (FK)   |
| title              |       | doc.relationTo   |
| _status            |       | title            |
| ...                |       | priority         |
+--------------------+       | createdAt        |
                             | updatedAt        |
                             +------------------+
```

## 4. インターフェース仕様

### 4.1 プラグイン使用例
```typescript
import { searchPlugin } from '@payloadcms/plugin-search'

export default buildConfig({
  plugins: [
    searchPlugin({
      collections: ['pages', 'posts', 'products'],
      defaultPriorities: {
        pages: 10,
        posts: (doc) => doc.featured ? 20 : 5,
        products: 15,
      },
      beforeSync: async ({ searchDoc, originalDoc }) => ({
        ...searchDoc,
        description: originalDoc.excerpt,
        categories: originalDoc.categories?.map(c => c.name),
      }),
      skipSync: async ({ doc, locale }) => {
        return !doc.searchable || locale === 'draft'
      },
    }),
  ],
})
```

### 4.2 エクスポート
```typescript
// プラグイン本体
export { searchPlugin } from './index.js'

// クライアントコンポーネント
export { LinkToDoc, ReindexButton } from './exports/client.js'

// 型定義
export type {
  SearchPluginConfig,
  DocToSync,
  BeforeSync,
  SkipSyncFunction,
} from './types.js'
```

### 4.3 再インデックスAPIリクエスト
```typescript
// リクエスト
POST /api/search/reindex
Content-Type: application/json

{
  "collections": ["pages", "posts"]
}

// レスポンス（成功）
{
  "message": "Successfully reindexed pages, posts: 150 of 160 documents (10 drafts skipped)"
}

// レスポンス（権限エラー）
{
  "message": "You are not allowed to perform this action"
}
```

## 5. 画面設計

### 5.1 管理画面

#### 検索結果一覧画面
- パス: `/admin/collections/search`
- デフォルト列: title
- 説明: 自動生成された検索結果の一覧
- アクション: ReindexButton（再インデックスボタン）

#### 検索結果詳細画面
- パス: `/admin/collections/search/{id}`
- サイドバー:
  - priority（編集可能）
  - doc（参照ドキュメント、readOnly）
  - docUrl（ドキュメントへのリンク）
- 本文: title（readOnly）

### 5.2 UIコンポーネント

#### ReindexButton
- 一覧画面のアクションとして表示
- クリックでコレクション選択モーダル
- 選択コレクションの再インデックス実行

#### LinkToDoc
- 参照ドキュメントへの直接リンク
- サイドバーに表示

## 6. ビジネスロジック

### 6.1 重複同期防止
```typescript
const docKey = config.localization
  ? `${collection}:${id}:${locale}`
  : `${collection}:${id}`

const syncedDocsSet = req.context?.syncedDocsSet || new Set()
if (syncedDocsSet.has(docKey)) {
  return doc  // 重複スキップ
}
syncedDocsSet.add(docKey)
```

### 6.2 優先度計算
```typescript
let defaultPriority = 0
if (defaultPriorities) {
  const priority = defaultPriorities[collection]
  if (typeof priority === 'function') {
    defaultPriority = await priority(doc)
  } else {
    defaultPriority = priority
  }
}
```

### 6.3 ローカライズ対応
```typescript
// プラグイン設定
const shouldLocalize =
  typeof pluginConfig.localize === 'boolean'
    ? pluginConfig.localize
    : Boolean(config.localization)

// titleフィールド
{
  name: 'title',
  type: 'text',
  localized: pluginConfig.localize,
}
```

## 7. エラー処理

### 7.1 同期エラー
```typescript
try {
  await payload.update({ ... })
} catch (err) {
  payload.logger.error({ err, msg: `Error updating ${searchSlug} document.` })
}
```

### 7.2 再インデックスエラー
```typescript
try {
  await deleteIndexes(collection)
  await reindexCollection(collection)
} catch (err) {
  payload.logger.error({
    err,
    msg: t('error:unableToReindexCollection', { collection })
  })
}
```

### 7.3 トランザクションロールバック
```typescript
const shouldCommit = await initTransaction(req)
try {
  // 処理
  await commitTransaction(req)
} catch (err) {
  await killTransaction(req)
  return Response.json({ message: err.message }, { status: 500 })
}
```

## 8. セキュリティ考慮事項

### 8.1 アクセス制御
- searchコレクションはcreate: () => falseで直接作成を禁止
- 再インデックスAPIはdelete + update権限を検証

### 8.2 再インデックス権限検証
```typescript
const permissions = [
  searchAccessResults.delete,
  searchAccessResults.update
]
if (!permissions.every(Boolean)) {
  return Response.json(
    { message: t('error:notAllowedToPerformAction') },
    { status: 401 }
  )
}
```

## 9. パフォーマンス考慮事項

### 9.1 バッチ処理
```typescript
const batchSize = pluginConfig.reindexBatchSize || 50

for (let i = 0; i < totalBatches; i++) {
  const { docs } = await payload.find({
    limit: batchSize,
    page: i + 1,
    ...
  })
  // 各ドキュメント処理
}
```

### 9.2 インデックス最適化
```typescript
// docフィールドにインデックス設定
{
  name: 'doc',
  type: 'relationship',
  index: true,  // 検索高速化
  maxDepth: 0,  // 関連データ取得抑制
}
```

### 9.3 重複削除
```typescript
// 同一ドキュメントの重複インデックスを削除
if (duplicativeDocs.length > 0) {
  await payload.delete({
    collection: searchSlug,
    where: { id: { in: duplicativeDocIDs } },
  })
}
```

## 10. 非機能要件対応

### 10.1 トランザクション対応
- 再インデックス処理はトランザクション内で実行
- エラー時はロールバック

### 10.2 ローカライズ対応
- 各ロケールごとに個別のインデックスエントリ作成
- skipSync関数でロケール別の制御が可能

### 10.3 ゴミ箱対応
```typescript
// ゴミ箱に入ったドキュメントは検索から削除
const isTrashDocument = doc && 'deletedAt' in doc && doc.deletedAt
if (isTrashDocument) {
  await payload.delete({ id: searchDocID, collection: searchSlug })
}
```

## 11. コードリーディングガイド

### 11.1 推奨読解順序

1. **型定義の理解**
   - `packages/plugin-search/src/types.ts` (1-127行目)
   - SearchPluginConfig、DocToSync、BeforeSync型

2. **プラグイン本体**
   - `packages/plugin-search/src/index.ts` (11-84行目)
   - フック登録とコレクション生成

3. **検索コレクション生成**
   - `packages/plugin-search/src/Search/index.ts` (9-116行目)
   - フィールド定義とエンドポイント

4. **同期処理**
   - `packages/plugin-search/src/utilities/syncDocAsSearchIndex.ts` (3-283行目)
   - create/update処理の詳細

5. **再インデックス処理**
   - `packages/plugin-search/src/utilities/generateReindexHandler.ts` (21-229行目)
   - バッチ処理とトランザクション

### 11.2 プログラム呼び出し階層図
```
searchPlugin(config)
├── 対象コレクション設定
│   ├── afterChange: syncWithSearch
│   │   └── syncDocAsSearchIndex
│   │       ├── skipSync判定
│   │       ├── beforeSync呼び出し
│   │       └── payload.create/update
│   └── beforeDelete: deleteFromSearch
│       └── payload.delete
└── generateSearchCollection
    └── endpoints: /reindex
        └── generateReindexHandler
            ├── validatePermissions
            ├── deleteIndexes
            └── reindexCollection
                └── syncDocAsSearchIndex
```

### 11.3 データフロー図
```
[ドキュメント変更]
      ↓
afterChange (syncWithSearch)
      ↓
+------------------------+
| skipSync判定            |
+------------------------+
      ↓ (同期可)
+------------------------+
| DocToSync形式変換       |
| - doc.relationTo       |
| - doc.value            |
| - title                |
+------------------------+
      ↓
+------------------------+
| beforeSync             |
| (カスタム処理)          |
+------------------------+
      ↓
+------------------------+
| searchコレクション更新   |
| create / update        |
+------------------------+
```

### 11.4 関連ファイル一覧
| ファイルパス | 役割 |
|-------------|------|
| packages/plugin-search/src/index.ts | プラグインメイン実装 |
| packages/plugin-search/src/types.ts | 型定義 |
| packages/plugin-search/src/Search/index.ts | searchコレクション生成 |
| packages/plugin-search/src/Search/hooks/syncWithSearch.ts | 同期フック |
| packages/plugin-search/src/Search/hooks/deleteFromSearch.ts | 削除フック |
| packages/plugin-search/src/utilities/syncDocAsSearchIndex.ts | 同期処理実装 |
| packages/plugin-search/src/utilities/generateReindexHandler.ts | 再インデックスAPI |
| packages/plugin-search/src/Search/ui/ReindexButton/ | 再インデックスボタンUI |
| packages/plugin-search/package.json | パッケージ設定 |

## 12. 変更履歴

| 日付 | バージョン | 変更内容 |
|------|-----------|----------|
| 2026-03-01 | 1.0 | 初版作成 |
