# 機能設計書 50-検索インデックス

## 概要

本ドキュメントは、Ghost CMSにおける検索インデックス機能の設計仕様を記載したものである。

### 本機能の処理概要

検索インデックス機能は、サイト内検索を実現するためのデータを提供するAPIエンドポイント群である。Admin API（認証必要）とContent API（公開用）の2つのAPIセットを提供し、それぞれ異なるユースケースに対応する。Admin APIは管理画面での検索機能に使用され、下書きや非公開コンテンツも含む。Content APIはSodo Searchウィジェットなどの公開サイト向け検索に使用され、公開コンテンツのみを返却する。

**業務上の目的・背景**：検索機能を実現するには、コンテンツデータを効率的に取得してインデックスを構築する必要がある。検索インデックスAPIは、記事・ページ・タグ・ユーザーのデータを一括取得し、クライアントサイドでの検索インデックス構築を可能にする。

**機能の利用シーン**：
- Admin画面でのコンテンツ検索（下書き含む）
- Sodo Searchウィジェットでの公開コンテンツ検索
- サードパーティ検索サービスとの連携

**主要な処理内容**：
1. 記事データの取得（Admin: 全ステータス / Content: 公開のみ）
2. ページデータの取得（Admin APIのみ）
3. タグデータの取得
4. ユーザー/著者データの取得
5. 検索に必要な最小限のカラムのみ返却

**関連システム・外部連携**：
- Sodo Search（apps/sodo-search）との連携
- Admin画面の検索機能との連携
- サードパーティ検索エンジン

**権限による制御**：
- Admin API: posts/tags/users のbrowse権限が必要
- Content API: Content APIキーによる認証（公開データのみ）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 100 | 検索モーダル | 主機能 | 公開コンテンツ検索用データの提供 |
| 13 | エディタ画面 | 補助機能 | 記事検索・リンク挿入時のデータ提供 |

## 機能種別

API / データ取得 / 検索インデックス

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| なし | - | - | パラメータなしで全データ取得 | - |

### 入力データソース

- 認証情報（Admin API: Staff token / Content API: Content API key）

## 出力仕様

### Admin API出力データ

#### fetchPosts / fetchPages

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | 記事/ページID |
| uuid | string | UUID |
| url | string | 公開URL |
| title | string | タイトル |
| slug | string | スラッグ |
| status | string | ステータス（draft/published/scheduled/sent） |
| published_at | datetime | 公開日時 |
| visibility | string | 公開範囲 |

#### fetchTags

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | タグID |
| slug | string | スラッグ |
| name | string | タグ名 |
| url | string | タグページURL |

#### fetchUsers

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | ユーザーID |
| slug | string | スラッグ |
| url | string | プロフィールURL |
| name | string | 表示名 |
| profile_image | string | プロフィール画像URL |

### Content API出力データ

#### fetchPosts

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | 記事ID |
| slug | string | スラッグ |
| title | string | タイトル |
| excerpt | string | 抜粋 |
| url | string | 公開URL |
| updated_at | datetime | 更新日時 |
| visibility | string | 公開範囲 |

#### fetchAuthors

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | 著者ID |
| slug | string | スラッグ |
| name | string | 表示名 |
| url | string | 著者ページURL |
| profile_image | string | プロフィール画像URL |

#### fetchTags

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | タグID |
| slug | string | スラッグ |
| name | string | タグ名 |
| url | string | タグページURL |

### 出力先

- JSON APIレスポンス

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信
   └─ Admin API または Content API
2. 認証チェック
   ├─ Admin API: Staff token検証 + 権限チェック
   └─ Content API: Content API key検証
3. データ取得
   ├─ postsService.browsePosts() - 記事/ページ
   ├─ models.Tag.findPage() - タグ
   └─ models.User/Author.findPage() - ユーザー
4. レスポンス返却
   └─ JSONフォーマットでデータ返却
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト] --> B{API種別}
    B -->|Admin API| C[Staff認証チェック]
    B -->|Content API| D[Content APIキーチェック]
    C --> E{権限あり?}
    E -->|No| F[403 Forbidden]
    E -->|Yes| G[データ取得]
    D --> H{APIキー有効?}
    H -->|No| I[401 Unauthorized]
    H -->|Yes| G
    G --> J{エンドポイント}
    J -->|fetchPosts| K[postsService.browsePosts]
    J -->|fetchPages| K
    J -->|fetchTags| L[models.Tag.findPage]
    J -->|fetchUsers| M[models.User.findPage]
    J -->|fetchAuthors| N[models.Author.findPage]
    K --> O[JSONレスポンス]
    L --> O
    M --> O
    N --> O
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-50-001 | Admin APIステータスフィルター | draft, published, scheduled, sentの全ステータスを取得 | Admin API fetchPosts |
| BR-50-002 | Content APIステータスフィルター | 公開済み記事のみ取得（type:post） | Content API fetchPosts |
| BR-50-003 | Content APIタグフィルター | visibility:publicのタグのみ取得 | Content API fetchTags |
| BR-50-004 | 最大件数制限 | limit: 10000で最大10000件を取得 | 全エンドポイント |
| BR-50-005 | ソート順 | updated_at DESCで更新日時の新しい順 | 全エンドポイント |
| BR-50-006 | キャッシュ無効化なし | cacheInvalidate: falseでキャッシュ無効化をスキップ | 全エンドポイント |

### 取得カラム設計

Admin APIとContent APIでは取得カラムが異なる：

| エンドポイント | Admin API | Content API |
|--------------|-----------|-------------|
| Posts | id, uuid, url, title, slug, status, published_at, visibility | id, slug, title, excerpt, url, updated_at, visibility |
| Tags | id, slug, name, url | id, slug, name, url |
| Users | id, slug, url, name, profile_image | - |
| Authors | - | id, slug, name, url, profile_image |

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| fetchPosts | posts | SELECT | 記事データの取得 |
| fetchPages | posts | SELECT | ページデータの取得（type=page） |
| fetchTags | tags | SELECT | タグデータの取得 |
| fetchUsers | users | SELECT | ユーザーデータの取得 |
| fetchAuthors | users | SELECT | 著者データの取得 |

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

すべてSELECT操作（読み取り専用）。

**postsテーブル**:
- filter条件でtype（post/page）とstatus（draft/published/scheduled/sent）を指定
- 最大10000件を取得
- 指定カラムのみ取得（検索に必要な最小限）

**tagsテーブル**:
- Content APIではvisibility:publicフィルター適用
- 最大10000件を取得

**usersテーブル**:
- 最大10000件を取得
- profile_imageを含む著者情報を取得

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 401 | Unauthorized | APIキーが無効 | 有効なAPIキーを使用 |
| 403 | Forbidden | 権限不足 | 適切な権限を持つユーザーでアクセス |

### リトライ仕様

- 読み取り専用APIのため、クライアント側でのリトライを想定

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

読み取り専用APIのため、トランザクション管理は不要。

## パフォーマンス要件

- limit: 10000で大量データを一括取得
- 検索に必要な最小限のカラムのみ取得
- cacheInvalidate: falseでキャッシュ無効化をスキップ

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

- Admin API: Staff認証 + browse権限チェック
- Content API: Content APIキー認証
- 非公開コンテンツはContent API経由でアクセス不可
- 下書き・予約投稿はAdmin API経由のみアクセス可能

## 備考

- Admin APIのpermissionsはdocName + methodの組み合わせで権限チェック
- Content APIのpermissionsはtrue（認証のみ、権限チェックなし）
- fetchAuthors（Content API）はmodels.Authorを使用（Userのエイリアス）

---

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

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: Admin API検索インデックスを理解する

管理画面向けの検索インデックスAPIを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | search-index.js | `ghost/core/core/server/api/endpoints/search-index.js` | Admin API検索インデックス |

**読解のコツ**: 4つのエンドポイント（fetchPosts, fetchPages, fetchTags, fetchUsers）の構造を比較しながら読む。

**主要処理フロー**:
- **7行目**: docName: 'search_index' でAPIドキュメント名を定義
- **8-25行目**: fetchPostsアクション - 記事取得
- **12-15行目**: permissionsでposts.browse権限をチェック
- **17-22行目**: オプション設定（filter, limit, order, columns）
- **27-45行目**: fetchPagesアクション - ページ取得（type:page）
- **46-63行目**: fetchTagsアクション - タグ取得
- **64-81行目**: fetchUsersアクション - ユーザー取得

#### Step 2: Content API検索インデックスを理解する

公開サイト向けの検索インデックスAPIを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | search-index-public.js | `ghost/core/core/server/api/endpoints/search-index-public.js` | Content API検索インデックス |

**主要処理フロー**:
- **7行目**: docName: 'search_index' でAPIドキュメント名を定義
- **8-22行目**: fetchPostsアクション - 公開記事取得
- **12行目**: permissions: true（認証のみ）
- **15行目**: filter: 'type:post'（公開記事のみ）
- **18行目**: excerptカラムを含む（検索結果表示用）
- **24-37行目**: fetchAuthorsアクション - 著者取得
- **36行目**: models.Author.findPage()を使用
- **39-54行目**: fetchTagsアクション - 公開タグ取得
- **49行目**: filter: 'visibility:public'で公開タグのみ

#### Step 3: 投稿サービスを理解する

postsService.browsePosts()の実装を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | posts-service.js | `ghost/core/core/server/services/posts/posts-service.js` | 投稿サービス実装 |

**主要処理フロー**:
- browsePosts()メソッドでデータ取得ロジックを実装
- フィルター、ソート、カラム指定の処理

#### Step 4: モデル定義を理解する

Tag、Userモデルの定義を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | tag.js | `ghost/core/core/server/models/tag.js` | タグモデル定義 |
| 4-2 | user.js | `ghost/core/core/server/models/user.js` | ユーザーモデル定義 |

### プログラム呼び出し階層図

```
API Request
    │
    ├─ Admin API (/ghost/api/admin/search-index/)
    │      │
    │      ├─ fetchPosts
    │      │      ├─ permissions: posts.browse
    │      │      └─ postsService.browsePosts({
    │      │             filter: 'type:post+status:[draft,published,scheduled,sent]',
    │      │             limit: '10000',
    │      │             columns: [id, uuid, url, title, slug, status, published_at, visibility]
    │      │         })
    │      │
    │      ├─ fetchPages
    │      │      ├─ permissions: posts.browse
    │      │      └─ postsService.browsePosts({
    │      │             filter: 'type:page+status:[draft,published,scheduled]',
    │      │             limit: '10000',
    │      │             columns: [...]
    │      │         })
    │      │
    │      ├─ fetchTags
    │      │      ├─ permissions: tags.browse
    │      │      └─ models.Tag.findPage({limit: '10000', columns: [...]})
    │      │
    │      └─ fetchUsers
    │             ├─ permissions: users.browse
    │             └─ models.User.findPage({limit: '10000', columns: [...]})
    │
    └─ Content API (/ghost/api/content/search-index/)
           │
           ├─ fetchPosts
           │      ├─ permissions: true (authenticated)
           │      └─ postsService.browsePosts({
           │             filter: 'type:post',
           │             limit: '10000',
           │             columns: [id, slug, title, excerpt, url, updated_at, visibility]
           │         })
           │
           ├─ fetchAuthors
           │      ├─ permissions: true (authenticated)
           │      └─ models.Author.findPage({limit: '10000', columns: [...]})
           │
           └─ fetchTags
                  ├─ permissions: true (authenticated)
                  └─ models.Tag.findPage({
                         filter: 'visibility:public',
                         limit: '10000',
                         columns: [...]
                     })
```

### データフロー図

```
[Admin API]                      [Content API]

GET /search-index/posts/  ──────▶ ┌──────────────────────────┐
GET /search-index/pages/         │   search-index.js        │
GET /search-index/tags/          │   search-index-public.js │
GET /search-index/users/         └──────────────────────────┘
                                           │
                                           ▼
                                 ┌──────────────────────────┐
                                 │   Permission Check       │
                                 │   (Admin: docName+method)│
                                 │   (Content: true)        │
                                 └──────────────────────────┘
                                           │
                                           ▼
                                 ┌──────────────────────────┐
                                 │   Data Service           │
                                 │   - postsService         │
                                 │   - models.Tag           │
                                 │   - models.User/Author   │
                                 └──────────────────────────┘
                                           │
                                           ▼
                                 ┌──────────────────────────┐
                                 │   Database               │
                                 │   - posts (type filter)  │
                                 │   - tags (visibility)    │
                                 │   - users                │
                                 └──────────────────────────┘
                                           │
                                           ▼
                                 ┌──────────────────────────┐
                                 │  JSON Response           │
                                 │  - data[]                │
                                 │  - meta (pagination)     │
                                 └──────────────────────────┘
                                           │
                                           ▼
                                 ┌──────────────────────────┐
                                 │  Client (Sodo Search)    │
                                 │  - Flexsearch Index      │
                                 │  - Admin Search          │
                                 └──────────────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| search-index.js | `ghost/core/core/server/api/endpoints/search-index.js` | ソース | Admin API検索インデックス |
| search-index-public.js | `ghost/core/core/server/api/endpoints/search-index-public.js` | ソース | Content API検索インデックス |
| posts-service.js | `ghost/core/core/server/services/posts/posts-service.js` | ソース | 投稿サービス |
| tag.js | `ghost/core/core/server/models/tag.js` | ソース | タグモデル |
| user.js | `ghost/core/core/server/models/user.js` | ソース | ユーザーモデル |
| search-index.js | `apps/sodo-search/src/search-index.js` | ソース | クライアント検索インデックス |
