# 機能設計書 74-Search（Sodo Search）

## 概要

本ドキュメントは、Ghost CMSのサイト内検索機能を提供するフロントエンドウィジェット「Sodo Search」の機能設計書である。Sodo SearchはReactで実装されており、Flexsearch.jsによるクライアントサイド全文検索を使用して、記事・タグ・著者の即座の検索結果表示を実現する。

### 本機能の処理概要

Sodo Searchは、Ghostサイトのテーマに埋め込まれる検索ウィジェットで、モーダルポップアップ形式で検索UIを表示する。初回表示時に検索インデックスをクライアントサイドに構築し、ユーザーの入力に対してリアルタイムで検索結果を返す。サーバーサイドへのリクエストなしに高速な検索体験を提供する点が特徴である。

**業務上の目的・背景**：大量のコンテンツを持つサイトでは、訪問者が目的の記事を見つけることが困難になる。Sodo Searchは、高速なクライアントサイド検索により、ユーザー体験を向上させる。Flexsearch.jsを使用することで、サーバー負荷を抑えつつ、タイピング中のリアルタイム検索を実現する。

**機能の利用シーン**：
- サイト訪問者が特定のトピックに関する記事を探す際
- 著者ページへ移動したい場合
- タグで記事を絞り込みたい場合
- キーボードショートカット（Cmd/Ctrl + K）で素早く検索を開始する際

**主要な処理内容**：
1. 検索モーダルの表示（カスタムトリガーボタン、キーボードショートカット、URLハッシュ）
2. Ghost Content APIから記事・タグ・著者データを取得
3. Flexsearchによるクライアントサイドインデックス構築
4. リアルタイム検索とハイライト付き結果表示
5. キーボードナビゲーション（矢印キー、Enter）
6. CJK（中国語・日本語・韓国語）文字のトークン化対応

**関連システム・外部連携**：
- Ghost Content API（`/ghost/api/content/search-index/`）：検索インデックスデータ取得
- Flexsearch.js：クライアントサイド全文検索エンジン

**権限による制御**：
- 本機能は認証不要で利用可能（パブリック検索）
- Content API Keyによるアクセス制御

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 100 | 検索モーダル | 主機能 | サイト内コンテンツの検索 |

## 機能種別

検索処理 / UI表示 / クライアントサイドインデックス

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| searchValue | string | No | 検索クエリ | なし |
| data-sodo-search | string | Yes | GhostサイトのAdmin URL | 有効なURL |
| data-key | string | Yes | Content API Key | 有効なAPIキー |
| data-styles | string | No | カスタムスタイルシートURL | 有効なURL |
| data-locale | string | No | 表示言語 | 有効なロケールコード |

### 入力データソース

- HTMLスクリプトタグ属性：`data-sodo-search`, `data-key`, `data-styles`, `data-locale`
- キーボード入力：検索クエリ
- Ghost Content API：記事・タグ・著者データ

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| showPopup | boolean | ポップアップ表示状態 |
| searchValue | string | 現在の検索クエリ |
| indexComplete | boolean | インデックス構築完了フラグ |
| posts | Post[] | 検索結果の記事一覧 |
| authors | Author[] | 検索結果の著者一覧 |
| tags | Tag[] | 検索結果のタグ一覧 |

### 出力先

- DOM（iframeベースの検索モーダル）
- ブラウザナビゲーション（結果クリック時のページ遷移）

## 処理フロー

### 処理シーケンス

```
1. 初期化（initSetup）
   └─ URLハッシュ（#/search）をチェックしてモーダル表示
   └─ キーボードショートカット登録（Cmd/Ctrl + K）
   └─ カスタムトリガーボタン（data-ghost-search）にイベント登録

2. インデックス構築（setupSearchIndex）
   └─ モーダル初回表示時に実行
   └─ Ghost Content APIから記事データ取得
   └─ Ghost Content APIから著者データ取得
   └─ Ghost Content APIからタグデータ取得
   └─ Flexsearchインデックスに追加

3. 検索実行
   └─ searchValue変更時にsearchIndex.search()を呼び出し
   └─ 記事・著者・タグそれぞれのインデックスで検索
   └─ 結果の正規化と404URLのフィルタリング

4. 結果表示
   └─ 著者→タグ→記事の順で結果表示
   └─ 検索クエリに一致する部分をハイライト
   └─ キーボードナビゲーション対応

5. 結果選択
   └─ クリックまたはEnterキーで選択
   └─ 選択アイテムのURLにページ遷移
```

### フローチャート

```mermaid
flowchart TD
    A[検索トリガー] --> B{トリガー種別}
    B -->|ボタンクリック| C[showPopup: true]
    B -->|Cmd+K| C
    B -->|#/search| C
    C --> D{インデックス構築済み?}
    D -->|No| E[Content APIデータ取得]
    E --> F[Flexsearchインデックス構築]
    F --> G[indexComplete: true]
    D -->|Yes| H[検索入力待ち]
    G --> H

    H --> I[検索クエリ入力]
    I --> J[searchIndex.search実行]
    J --> K{結果あり?}
    K -->|Yes| L[結果表示]
    K -->|No| M[No matches found表示]
    L --> N{キーボード操作}
    N -->|↑/↓| O[選択移動]
    N -->|Enter| P[ページ遷移]
    N -->|Escape| Q[モーダルクローズ]
    O --> N
    M --> H
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-74-01 | 遅延インデックス構築 | インデックス構築はモーダル初回表示時まで遅延 | showPopup && !indexStarted |
| BR-74-02 | RTLサポート | RTL言語では検索トークン化方向を逆転 | dir === 'rtl' |
| BR-74-03 | CJKトークン化 | 中国語・日本語・韓国語は1文字ずつトークン化 | isCJK(codePoint) === true |
| BR-74-04 | 結果表示順序 | 著者→タグ→記事の順で表示 | 常に |
| BR-74-05 | 404URLフィルタリング | URLが/404/で終わる結果を除外 | invalidUrlRegex.test(url) |
| BR-74-06 | ページネーション | 記事結果は初期10件、追加10件ずつ表示 | DEFAULT_MAX_POSTS = 10, STEP_MAX_POSTS = 10 |

### 計算ロジック

- 検索インデックス対象フィールド：
  - 記事：title, excerpt
  - 著者：name
  - タグ：name

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| インデックス取得 | posts | SELECT | 公開済み記事のtitle, excerpt, url取得 |
| インデックス取得 | users | SELECT | 著者のname, profile_image, url取得 |
| インデックス取得 | tags | SELECT | 公開タグのname, url取得 |

※ Sodo Searchはクライアントサイドアプリケーションのため、直接のDB操作は行わない。Content API経由でデータを取得する。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| Error fetching posts | 取得エラー | 記事データ取得失敗 | コンソールエラー出力、空配列で継続 |
| Error fetching authors | 取得エラー | 著者データ取得失敗 | コンソールエラー出力、空配列で継続 |
| Error fetching tags | 取得エラー | タグデータ取得失敗 | コンソールエラー出力、空配列で継続 |

### リトライ仕様

- 自動リトライなし
- データ取得失敗時は空の結果を表示

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

- Sodo Searchはクライアントサイドのみで動作するため、トランザクション管理は不要
- Content APIはGET操作のみでデータ変更は行わない

## パフォーマンス要件

- インデックス構築：遅延実行でページ初期ロードに影響なし
- 検索レスポンス：タイピング中のリアルタイム結果表示
- メモリ使用：Flexsearchによる効率的なインデックス管理
- バンドルサイズ：Flexsearch + React + 最小限の依存関係

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

- Content API Key：公開APIキーのため、取得可能なデータは公開コンテンツのみ
- XSS対策：検索結果のハイライトはReactコンポーネントで安全にレンダリング
- iframeサンドボックス：ホストページとの分離

## 備考

- Sodo Searchは`apps/sodo-search/`ディレクトリに配置されたReactアプリケーション
- ビルド成果物はUMD形式で`umd/`ディレクトリに出力
- Tailwind CSSでスタイリング（別ファイルmain.cssとして出力）
- 多言語対応は`@tryghost/i18n`パッケージの'search'名前空間を使用
- React 17を使用

---

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

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

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、Sodo Searchで使用される主要なデータ構造を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | app-context.js | `apps/sodo-search/src/app-context.js` | AppContextの定義 |
| 1-2 | search-index.js | `apps/sodo-search/src/search-index.js` | SearchIndexクラスの構造 |

**読解のコツ**: AppContextにはshowPopup、searchIndex、indexComplete、searchValue等の状態が含まれる。SearchIndexクラスはFlexsearchのDocumentインデックスを3つ（posts、authors、tags）管理する。

#### Step 2: エントリーポイントを理解する

処理の起点となるファイル・関数を特定する。

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

**主要処理フロー（app.js）**:
1. **9-33行目**: コンストラクタでSearchIndexインスタンス作成、state初期化
2. **35-40行目**: componentDidMountでinitSetup呼び出し
3. **74-77行目**: componentDidUpdateでモーダル表示時にインデックス構築開始
4. **79-87行目**: setupSearchIndex()で非同期インデックス構築
5. **95-104行目**: initSetup()でURL・キーボード・ボタンのハンドラ設定
6. **167-183行目**: addKeyboardShortcuts()でCmd+Kショートカット登録
7. **185-211行目**: render()でAppContext.Providerを通じてコンテキスト提供

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

Flexsearchを使用した検索ロジックを理解する。

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

**主要処理フロー**:
- **1-57行目**: CJKトークン化ロジック（isCJK, tokenizeCjkByCodePoint）
- **59-102行目**: SearchIndexコンストラクタ（3つのFlexsearch.Documentインデックス作成）
- **104-131行目**: #populatePostIndex, #fetchPosts, #updatePostIndex
- **132-186行目**: 著者・タグのインデックス構築
- **188-192行目**: init()で全インデックス構築
- **210-226行目**: search()で3つのインデックスを検索して結果を返す

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

検索UIの実装を理解する。

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

**主要処理フロー（popup-modal.js）**:
- **73-126行目**: SearchBox - 検索入力フィールドとキーボードハンドリング
- **173-195行目**: TagListItem - タグ検索結果アイテム
- **221-248行目**: PostListItem - 記事検索結果アイテム
- **250-271行目**: getMatchIndexes - ハイライト位置計算
- **309-342行目**: HighlightedSection - 検索ハイライト表示
- **378-407行目**: PostResults - 記事結果一覧（ページネーション付き）
- **471-508行目**: SearchResultBox - 検索結果全体の制御
- **510-581行目**: Results - キーボードナビゲーション付き結果表示
- **616-694行目**: PopupModal - モーダル全体のレンダリング制御

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

```
index.js (init)
    │
    └─ App (app.js)
           │
           ├─ constructor
           │      └─ new SearchIndex({adminUrl, apiKey, dir})
           │
           ├─ componentDidMount
           │      └─ initSetup()
           │             ├─ handleSearchUrl()
           │             ├─ addKeyboardShortcuts()
           │             └─ setupCustomTriggerButton()
           │
           ├─ componentDidUpdate
           │      └─ setupSearchIndex() (showPopup && !indexStarted)
           │             └─ searchIndex.init()
           │                    ├─ #populatePostIndex()
           │                    │      ├─ #fetchPosts()
           │                    │      └─ #updatePostIndex()
           │                    ├─ #populateAuthorsIndex()
           │                    └─ #populateTagsIndex()
           │
           └─ render()
                  └─ AppContext.Provider
                         └─ PopupModal (popup-modal.js)
                                │
                                ├─ Frame
                                │      └─ PopupContent
                                │             └─ Search
                                │                    ├─ SearchBox
                                │                    │      └─ SearchClearIcon
                                │                    │      └─ Loading
                                │                    │      └─ CancelButton
                                │                    └─ SearchResultBox
                                │                           └─ Results
                                │                                  ├─ AuthorResults
                                │                                  │      └─ AuthorListItem
                                │                                  ├─ TagResults
                                │                                  │      └─ TagListItem
                                │                                  └─ PostResults
                                │                                         ├─ PostListItem
                                │                                         │      └─ HighlightedSection
                                │                                         └─ ShowMoreButton
                                └─ NoResultsBox (結果なし時)
```

### データフロー図

```
[入力]                    [処理]                         [出力]

トリガー操作 ─────────────┐
  (ボタン/Cmd+K/#/search) │
                          │
検索クエリ入力 ──────────┼─▶ App (app.js)
                          │   ├─ dispatch('update')    ─▶ state更新
                          │   └─ SearchIndex構築       ─▶ indexComplete
                          │
                          └─▶ setupSearchIndex()
                                 │
                                 ▼
                          search-index.js
                          ├─ #fetchPosts()
                          │      └─ Content API (/search-index/posts/)
                          ├─ #fetchAuthors()
                          │      └─ Content API (/search-index/authors/)
                          └─ #fetchTags()
                                 └─ Content API (/search-index/tags/)
                                 │
                                 ▼
                          Flexsearch.Document.add()
                                 │
                                 ▼
                          SearchResultBox
                          ├─ searchIndex.search(searchValue)
                          └─ Flexsearch検索実行
                                 │
                                 ▼
                          Results
                          ├─ AuthorResults
                          ├─ TagResults
                          └─ PostResults
                                 │
                                 ▼
                          [検索結果表示]
                                 │
                                 ▼
                          クリック/Enter
                                 │
                                 ▼
                          window.location.href = url
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.js | `apps/sodo-search/src/index.js` | ソース | アプリエントリーポイント |
| app.js | `apps/sodo-search/src/app.js` | ソース | メインアプリコンポーネント |
| app-context.js | `apps/sodo-search/src/app-context.js` | ソース | コンテキスト定義 |
| search-index.js | `apps/sodo-search/src/search-index.js` | ソース | Flexsearch検索インデックス |
| popup-modal.js | `apps/sodo-search/src/components/popup-modal.js` | ソース | 検索モーダルUI |
| frame.js | `apps/sodo-search/src/components/frame.js` | ソース | iframeラッパー |
| app.css | `apps/sodo-search/src/app.css` | スタイル | アプリスタイル |
| index.css | `apps/sodo-search/src/index.css` | スタイル | Tailwind入力ファイル |
| package.json | `apps/sodo-search/package.json` | 設定 | パッケージ定義 |
| vite.config.js | `apps/sodo-search/vite.config.js` | 設定 | ビルド設定 |
| tailwind.config.js | `apps/sodo-search/tailwind.config.js` | 設定 | Tailwind CSS設定 |
