# バッチ設計書 14-reindexHandler

## 概要

本ドキュメントは、Payload CMSの検索プラグインにおける再インデックスハンドラ（reindexHandler）のバッチ処理設計を定義する。

### 本バッチの処理概要

reindexHandlerは、指定されたコレクションの全ドキュメントを検索インデックスに再同期するバッチ処理エンドポイントである。

**業務上の目的・背景**：検索プラグインは、コレクションのドキュメントを専用の検索コレクションにインデックス化することで、高速な全文検索を実現する。しかし、インデックスとソースデータの不整合（データ破損、設定変更、マイグレーション後等）が発生する場合がある。本ハンドラは、指定コレクションの全ドキュメントを再スキャンし、検索インデックスを完全に再構築することで、データの整合性を回復する。

**バッチの実行タイミング**：管理者がAPIエンドポイント（POST /api/reindex）を呼び出した時、または管理画面の再インデックスボタンを押した時に実行される。手動トリガーが基本。

**主要な処理内容**：
1. 権限検証（検索コレクションへのCRUD権限）
2. 対象コレクションの検証（検索プラグイン設定に含まれるか）
3. 既存インデックスの削除（指定コレクションのインデックスのみ）
4. ドキュメントのバッチ取得（ページネーション）
5. 各ドキュメントの各ロケールに対する検索インデックス同期
6. トランザクション管理（コミット/ロールバック）
7. 処理結果の返却（成功件数、スキップ件数等）

**前後の処理との関連**：検索プラグイン（plugin-search）のコア機能。通常のドキュメント操作時はフックで自動同期されるが、本ハンドラは手動での完全再構築を提供する。

**影響範囲**：検索コレクション（デフォルト: 'search'）のドキュメント。既存インデックスの削除と再作成が行われる。

## バッチ種別

データ連携 / 検索インデックス再構築 / バッチ処理

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 随時（手動実行） |
| 実行時刻 | 任意 |
| 実行曜日 | 任意 |
| 実行日 | 任意 |
| トリガー | 手動（API呼び出し） |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| 検索プラグイン有効 | 検索プラグインがPayload設定で有効化されていること |
| 権限 | ユーザーが検索コレクションへのupdate/delete権限を持つこと |
| 有効なコレクション | 指定コレクションが検索プラグインの設定に含まれること |

### 実行可否判定

- req.jsonが未定義の場合は400エラー
- 権限不足の場合は401エラー
- 無効なコレクション指定の場合は400エラー

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| collections | string[] | Yes | - | 再インデックス対象のコレクションスラッグ配列 |

### プラグイン設定（pluginConfig）

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| searchOverrides.slug | string | No | 'search' | 検索コレクションのスラッグ |
| collections | string[] | Yes | - | 検索対象コレクション配列 |
| reindexBatchSize | number | Yes | - | バッチあたりの処理件数 |
| syncDrafts | boolean | No | false | ドラフトを検索インデックスに含めるか |
| skipSync | (args) => boolean | No | - | 同期スキップ判定関数 |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| HTTPリクエスト | JSON | collectionsパラメータ |
| 対象コレクション | DB | 再インデックス対象のドキュメント |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| HTTPレスポンス | JSON | 処理結果メッセージ |
| 検索コレクション | DB | 再構築されたインデックス |

### レスポンス構造

| 項目名 | 型 | 説明 |
|--------|-----|------|
| message | string | 処理結果メッセージ（コレクション名、成功件数、スキップ件数、総件数） |

## 処理フロー

### 処理シーケンス

```
1. リクエスト解析
   └─ req.json()からcollections配列を取得
2. 権限検証
   └─ getAccessResults()で検索コレクションへの権限を確認
3. コレクション検証
   └─ 指定コレクションが検索プラグイン設定に含まれるか確認
4. トランザクション開始
   └─ initTransaction(req)
5. コレクションごとの処理
   5.1. 既存インデックス削除
        └─ payload.delete({ where: { 'doc.relationTo': collection } })
   5.2. ドキュメント総数カウント
        └─ payload.count({ collection })
   5.3. バッチ処理ループ
        └─ totalBatches = ceil(totalDocs / batchSize)
   5.4. ドキュメント取得
        └─ payload.find({ collection, page, limit: batchSize })
   5.5. ロケールループ
        └─ 各ドキュメント × 各ロケールで処理
   5.6. skipSync判定
        └─ プラグインのskipSync関数で同期スキップを判定
   5.7. 検索インデックス同期
        └─ syncDocAsSearchIndex({ operation: 'create' or 'update' })
6. 結果集計
   └─ 成功件数、スキップ件数を計算
7. トランザクションコミット
   └─ commitTransaction(req)
8. レスポンス返却
   └─ 処理結果メッセージをJSON形式で返却
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト受信] --> B{req.json存在?}
    B -->|No| C[400: Bad Request]
    B -->|Yes| D[collections取得]
    D --> E[権限検証]
    E --> F{権限あり?}
    F -->|No| G[401: Unauthorized]
    F -->|Yes| H[コレクション検証]
    H --> I{有効なコレクション?}
    I -->|No| J[400: Invalid Args]
    I -->|Yes| K[トランザクション開始]
    K --> L{次のコレクション?}
    L -->|No| M[結果集計]
    L -->|Yes| N[既存インデックス削除]
    N --> O[ドキュメント総数カウント]
    O --> P{次のバッチ?}
    P -->|No| L
    P -->|Yes| Q[ドキュメント取得]
    Q --> R{次のドキュメント?}
    R -->|No| P
    R -->|Yes| S{次のロケール?}
    S -->|No| R
    S -->|Yes| T[skipSync判定]
    T --> U{スキップ?}
    U -->|Yes| S
    U -->|No| V[syncDocAsSearchIndex]
    V --> S
    M --> W{エラー発生?}
    W -->|Yes| X[ロールバック]
    X --> Y[500: Error]
    W -->|No| Z[コミット]
    Z --> AA[200: Success]
```

## データベース操作仕様

### 操作別データベース影響一覧

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ドキュメント総数カウント | {collection} | SELECT (COUNT) | totalDocsの取得 |
| ドキュメント取得 | {collection} | SELECT | バッチ単位での取得 |
| 既存インデックス削除 | {searchSlug} | DELETE | 対象コレクションのインデックス削除 |
| インデックス作成 | {searchSlug} | INSERT | 新規インデックス作成 |
| インデックス更新 | {searchSlug} | UPDATE | 既存インデックスの更新 |

### テーブル別操作詳細

#### 対象コレクション（{collection}）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT (COUNT) | * | _status = 'published' (syncDrafts=false時) | totalDocs取得 |
| SELECT | * | page, limit: batchSize | バッチ取得 |

#### 検索コレクション（{searchSlug}）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| DELETE | * | doc.relationTo = collection | 既存インデックス削除 |
| INSERT | doc, title, priority | 変換後データ | 新規インデックス |
| UPDATE | doc, title, priority | 変換後データ | 既存更新（ロケール追加時） |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| BAD_REQUEST | 入力エラー | req.jsonが未定義 | リクエスト形式を確認 |
| UNAUTHORIZED | 認証エラー | 検索コレクションへの権限不足 | ユーザー権限を確認 |
| INVALID_ARGS | 入力エラー | 無効なコレクション指定 | 検索プラグイン設定を確認 |
| INTERNAL_ERROR | システムエラー | 処理中のエラー | ログを確認してリトライ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし（手動再実行） |
| リトライ間隔 | N/A |
| リトライ対象エラー | N/A |

### 障害時対応

- エラー発生時はトランザクションをロールバック
- 500エラーとエラーメッセージを返却
- 個別コレクションのエラーはログに記録して次のコレクションを処理

## トランザクション仕様

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | 全コレクションの処理全体 |
| コミットタイミング | 全処理完了時 |
| ロールバック条件 | いずれかの処理でエラー発生時 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | 対象コレクションの総ドキュメント数 × ロケール数 |
| 目標処理時間 | reindexBatchSizeとドキュメント数に依存 |
| メモリ使用量上限 | バッチサイズ分のドキュメントがメモリに保持 |

## 排他制御

- トランザクションによる排他制御
- 同一コレクションへの並行再インデックスは避けるべき

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| エラーログ | skipSync関数エラー時 | エラー詳細 |
| エラーログ | コレクション処理エラー時 | コレクション名、エラー詳細 |
| エラーログ | syncDocAsSearchIndex失敗時 | エラー詳細 |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| 処理時間 | 設定に依存 | HTTPタイムアウト |
| エラー発生 | 1件以上 | HTTPレスポンス（500） |

## 備考

### ロケール処理

- 多言語対応のPayload設定では、各ドキュメントを全ロケールで処理
- 最初のロケール: operation = 'create'（インデックス作成）
- 以降のロケール: operation = 'update'（インデックス更新）

### ドラフト処理

- syncDrafts = false（デフォルト）の場合、_status = 'published'のドキュメントのみ処理
- syncDrafts = trueの場合、ドラフトを含む全ドキュメントを処理

### skipSync関数

```typescript
skipSync: async ({ collectionSlug, doc, locale, req }) => boolean
```

- trueを返すとそのドキュメント/ロケールの同期をスキップ
- エラーが発生した場合はログ出力して同期を続行

### CORSヘッダー

- headersWithCors関数でCORSヘッダーを付与
- クロスオリジンリクエストに対応

### 結果メッセージ形式

```
Successfully reindexed {collections}. {count}/{total} documents indexed ({skips} skipped).
```
