# 機能設計書 49-サイト内検索

## 概要

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

### 本機能の処理概要

サイト内検索機能は、Ghostサイトの公開コンテンツ（記事、著者、タグ）を全文検索するための機能である。Sodo Searchウィジェットとして提供され、サイト訪問者がキーボードショートカット（Cmd/Ctrl+K）または検索ボタンから検索モーダルを呼び出し、リアルタイムにコンテンツを検索できる。Flexsearchライブラリを使用したクライアントサイド検索を実装している。

**業務上の目的・背景**：コンテンツが増加するにつれ、目的の記事を見つけることが困難になる。サイト内検索機能により、訪問者は素早く目的のコンテンツにアクセスでき、サイトのユーザビリティと回遊率が向上する。

**機能の利用シーン**：
- 訪問者が特定のキーワードで記事を検索
- 著者名での検索
- タグ名での検索
- キーボードショートカット（Cmd/Ctrl+K）での即座検索
- ハッシュURL（#/search）での検索ページ直接アクセス

**主要な処理内容**：
1. 検索インデックスの取得：Content APIから記事・著者・タグデータを取得
2. インデックス構築：Flexsearchでクライアントサイドインデックスを構築
3. 検索実行：ユーザー入力に対してリアルタイム検索
4. 結果表示：記事・著者・タグの検索結果をカテゴリ別に表示
5. ナビゲーション：検索結果クリックで該当ページへ遷移

**関連システム・外部連携**：
- Content API（search-index-public）との連携
- Flexsearchライブラリ
- ghost/i18n（多言語対応）

**権限による制御**：
- 公開コンテンツのみ検索対象
- Content APIキーによる認証（テーマに埋め込み）
- 非公開記事、下書きは検索対象外

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 100 | 検索モーダル | 主機能 | サイト内コンテンツの検索 |
| 100 | 検索モーダル | 補助機能 | 検索UIウィジェットの表示 |

## 機能種別

全文検索 / クライアントサイド処理 / UIウィジェット

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| searchValue | string | Yes | 検索クエリ | なし |
| adminUrl | string | Yes（初期化時） | Ghost管理URL | 有効なURL |
| apiKey | string | Yes（初期化時） | Content APIキー | 有効なAPIキー |
| locale | string | No | ロケール設定 | 言語コード |

### 入力データソース

- ユーザーのキーボード入力
- テーマのdata属性による設定

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| posts | array | 検索結果の記事配列 |
| authors | array | 検索結果の著者配列 |
| tags | array | 検索結果のタグ配列 |

### 出力先

- 検索モーダル内の結果表示領域

## 処理フロー

### 処理シーケンス

```
1. 検索モーダル表示
   └─ Cmd/Ctrl+K または 検索ボタンクリック
2. インデックス初期化（初回のみ）
   ├─ Content API から記事データ取得
   ├─ Content API から著者データ取得
   ├─ Content API から タグデータ取得
   └─ Flexsearch インデックス構築
3. 検索入力
   └─ ユーザーがキーワード入力
4. 検索実行
   ├─ postsIndex.search()
   ├─ authorsIndex.search()
   └─ tagsIndex.search()
5. 結果表示
   └─ カテゴリ別に検索結果をレンダリング
6. ナビゲーション
   └─ 結果クリックで該当ページへ遷移
```

### フローチャート

```mermaid
flowchart TD
    A[検索トリガー] --> B{モーダル表示}
    B --> C{インデックス初期化済み?}
    C -->|No| D[Content APIからデータ取得]
    D --> E[Flexsearchインデックス構築]
    E --> F[検索入力待機]
    C -->|Yes| F
    F --> G[ユーザー入力]
    G --> H[Flexsearch検索実行]
    H --> I[結果正規化]
    I --> J[結果表示]
    J --> K{結果クリック?}
    K -->|Yes| L[ページ遷移]
    K -->|No| G
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-49-001 | 公開コンテンツのみ | 公開済み記事・ページのみ検索対象 | インデックス構築時 |
| BR-49-002 | 遅延インデックス構築 | モーダル表示時にインデックス構築を開始 | 初回モーダル表示時 |
| BR-49-003 | CJK対応 | 中国語・日本語・韓国語の文字単位トークナイズ | CJK文字検出時 |
| BR-49-004 | RTL対応 | 右から左言語のリバーストークナイズ | dir=rtl時 |
| BR-49-005 | キーボードショートカット | Cmd/Ctrl+Kで検索モーダル表示 | トリガーボタン存在時 |

### 計算ロジック

Flexsearchインデックス構築:
```javascript
this.postsIndex = new Flexsearch.Document({
    tokenize: tokenize,  // 'forward' or 'reverse'
    rtl: rtl,            // true for RTL languages
    document: {
        id: 'id',
        index: ['title', 'excerpt'],
        store: true
    },
    encoder: encoderSet  // CJK対応エンコーダー
});
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 記事取得 | posts | SELECT | 公開記事の取得（Content API経由） |
| 著者取得 | users | SELECT | 著者データの取得（Content API経由） |
| タグ取得 | tags | SELECT | 公開タグの取得（Content API経由） |

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

直接DBアクセスなし。Content API経由でデータ取得。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | Network Error | Content APIへのアクセス失敗 | コンソールにエラー出力、空配列を返却 |
| 401 | Unauthorized | APIキーが無効 | 正しいAPIキーを設定 |

### リトライ仕様

- API取得失敗時はエラーをコンソール出力
- リトライは実装されていない（空配列を返却）

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

クライアントサイド処理のため、トランザクション管理は不要。

## パフォーマンス要件

- インデックス構築: 初回アクセス時に1回
- 検索実行: リアルタイム（数十ミリ秒以内）
- 最大10000件のコンテンツをインデックス化

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

- Content APIキーはテーマ内に公開されるが読み取り専用
- 非公開コンテンツはAPI経由で取得不可
- XSS対策はReactによる自動エスケープ

## 備考

- Sodo Searchはスタンドアロンのnpmパッケージ（apps/sodo-search）
- UMDバンドルとしてビルドされCDN配信
- テーマの{{ghost_head}}で自動読み込み

---

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

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

### 推奨読解順序

#### Step 1: アプリケーション構造を理解する

まず、Sodo Searchアプリの全体構造を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | app.js | `apps/sodo-search/src/app.js` | Reactアプリのメインコンポーネント |

**読解のコツ**: Reactコンポーネントとして実装されており、状態管理はコンポーネント内で行う。

**主要処理フロー**:
- **9-30行目**: コンストラクタでSearchIndex初期化、状態設定
- **35-39行目**: componentDidMountでスクロールバー幅計算、初期化
- **79-87行目**: setupSearchIndexでインデックス構築
- **95-104行目**: initSetupでハッシュURLとキーボードショートカット設定
- **167-183行目**: addKeyboardShortcutsでCmd/Ctrl+K設定

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

Flexsearchを使用したインデックス構築を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | search-index.js | `apps/sodo-search/src/search-index.js` | 検索インデックス実装 |

**主要処理フロー**:
1. **3-12行目**: CJKエンコーダープリセット定義
2. **14-27行目**: isCJK関数でCJK文字判定
3. **29-52行目**: tokenizeCjkByCodePointでCJKトークナイズ
4. **59-102行目**: SearchIndexクラスのコンストラクタ
5. **67-76行目**: postsIndexのDocument設定
6. **104-110行目**: #populatePostIndexでAPIからデータ取得
7. **112-124行目**: #fetchPostsでContent API呼び出し
8. **188-192行目**: init関数で全インデックス構築
9. **210-227行目**: search関数で検索実行

#### Step 3: バックエンドAPIを理解する

Content APIの検索インデックスエンドポイントを確認する。

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

**主要処理フロー**:
- **8-22行目**: fetchPostsアクションで公開記事取得
- **14-18行目**: limit: 10000で最大件数設定
- **23-37行目**: fetchAuthorsアクションで著者取得
- **38-55行目**: fetchTagsアクションでpublic タグ取得

#### Step 4: UIコンポーネントを理解する

検索モーダルのUIを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | popup-modal.js | `apps/sodo-search/src/components/popup-modal.js` | 検索モーダルUI |

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

```
User Interaction (Cmd/Ctrl+K or Button Click)
    │
    ├─ App (React Component)
    │      │
    │      ├─ setupSearchIndex()
    │      │      └─ searchIndex.init()
    │      │             │
    │      │             ├─ #populatePostIndex()
    │      │             │      └─ fetch(Content API /search-index/posts/)
    │      │             │
    │      │             ├─ #populateAuthorsIndex()
    │      │             │      └─ fetch(Content API /search-index/authors/)
    │      │             │
    │      │             └─ #populateTagsIndex()
    │      │                    └─ fetch(Content API /search-index/tags/)
    │      │
    │      └─ PopupModal
    │             │
    │             └─ searchIndex.search(value)
    │                    ├─ postsIndex.search()
    │                    ├─ authorsIndex.search()
    │                    └─ tagsIndex.search()
    │
    └─ Search Results Display
```

### データフロー図

```
[初期化]                    [検索]                        [表示]

App初期化 ────────────────▶ ┌──────────────────────────┐
                           │   SearchIndex            │
Content API ──────────────▶ │          │               │
/search-index/posts/        │          ▼               │
/search-index/authors/      │   Flexsearch.Document    │
/search-index/tags/         │          │               │
                           │          ▼               │
                           │   インデックス構築         │
                           └──────────────────────────┘
                                       │
                                       ▼
ユーザー入力 ─────────────▶ ┌──────────────────────────┐
                           │   search(value)          │
                           │          │               │
                           │          ├─ posts        │
                           │          ├─ authors      │
                           │          └─ tags         │
                           └──────────────────────────┘
                                       │
                                       ▼
                           ┌──────────────────────────┐
                           │  PopupModal              │ ───▶ 検索結果表示
                           │  - PostResults           │
                           │  - AuthorResults         │
                           │  - TagResults            │
                           └──────────────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| app.js | `apps/sodo-search/src/app.js` | ソース | Reactアプリメイン |
| search-index.js | `apps/sodo-search/src/search-index.js` | ソース | 検索インデックス |
| popup-modal.js | `apps/sodo-search/src/components/popup-modal.js` | ソース | 検索モーダルUI |
| frame.js | `apps/sodo-search/src/components/frame.js` | ソース | iFrameラッパー |
| app-context.js | `apps/sodo-search/src/app-context.js` | ソース | Reactコンテキスト |
| index.js | `apps/sodo-search/src/index.js` | ソース | エントリーポイント |
| search-index-public.js | `ghost/core/core/server/api/endpoints/search-index-public.js` | ソース | Content API |
