# 機能設計書 53-全文検索

## 概要

本ドキュメントは、Fizzyアプリケーションにおける全文検索機能の詳細設計を記述する。

### 本機能の処理概要

カードとコメントの内容を全文検索し、キーワードにマッチする情報を素早く見つける機能である。

**業務上の目的・背景**：プロジェクト管理において、過去に作成したカードやコメントを素早く見つけ出すことは生産性向上に直結する。本機能により、ユーザーは膨大な情報の中から必要なカードやコメントをキーワード検索で即座に発見でき、情報の再利用や参照が容易になる。

**機能の利用シーン**：検索画面にて検索キーワードを入力し、関連するカードやコメントの一覧を取得する。特定のトピックに関する過去の議論を探す場合、プロジェクト名やキーワードで検索することで関連情報にアクセスできる。また、カード番号を直接入力することでカード詳細への直接アクセスも可能。

**主要な処理内容**：
1. ユーザーが検索クエリを入力
2. カード番号の場合は直接カード詳細を表示
3. テキスト検索の場合はSearch::Recordを使用して全文検索を実行
4. アクセス可能なボード内のカード・コメントに限定して結果を返す
5. 最近の検索クエリ履歴も同時に表示

**関連システム・外部連携**：MySQLのFULLTEXTインデックスを使用した検索を実行。

**権限による制御**：ユーザーがアクセス可能なボード内のカード・コメントのみが検索対象となる。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 32 | 検索画面 | 主画面 | カードとコメントの全文検索を実行 |
| 73 | カード検索プロンプト画面 | 関連画面 | カード検索用のオートコンプリートプロンプト |
| 77 | コマンドプロンプト画面 | 関連画面 | コマンド入力用のプロンプト |

## 機能種別

データ検索・参照

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| q | String | No | 検索クエリ | 最大2000文字、空白時は検索実行しない |

### 入力データソース

画面入力（検索フォーム）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| @query | String | 入力された検索クエリ |
| @card | Card | カード番号で直接ヒットした場合のカードオブジェクト |
| @page | Array | 検索結果のページネーションされたレコード |
| @recent_search_queries | Array | 最近の検索クエリ（最大10件） |

### 出力先

- 画面表示（検索結果一覧）

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ GET /search?q=キーワード
2. クエリパラメータ取得
   └─ 空白の場合はnilに設定
3. カード番号チェック
   └─ クエリがカード番号と一致するかチェック
4. カードヒット時
   └─ @cardにカードを設定（検索結果ではなくカードを直接表示）
5. テキスト検索時
   └─ Search::Record.searchで全文検索を実行
6. ページネーション
   └─ set_page_and_extract_portion_fromで結果をページング
7. 検索履歴取得
   └─ 最近の検索クエリを10件取得
8. レスポンス返却
   └─ 検索結果を画面に表示
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B{クエリが空?}
    B -->|Yes| C[クエリをnilに設定]
    B -->|No| D[クエリを設定]
    C --> E[検索結果なし表示]
    D --> F{カード番号と一致?}
    F -->|Yes| G[@cardにカード設定]
    F -->|No| H[全文検索実行]
    H --> I[ページネーション処理]
    I --> J[検索履歴取得]
    G --> K[カード詳細表示]
    J --> L[検索結果表示]
    K --> M[終了]
    L --> M
    E --> M
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-53-01 | アクセス権限フィルタ | ユーザーがアクセス可能なボード内のカード・コメントのみ検索対象 | 常時 |
| BR-53-02 | カード番号直接アクセス | クエリがカード番号と一致する場合、検索ではなくカード直接表示 | クエリがカード番号の場合 |
| BR-53-03 | クエリサニタイズ | 無効な検索文字を除去、アンバランスな引用符を処理 | 検索実行時 |
| BR-53-04 | 検索履歴表示 | 最近の検索クエリを最大10件表示 | 検索画面表示時 |

### 計算ロジック

**クエリサニタイズ処理**:
- 無効な検索文字（英数字と引用符以外）をスペースに置換
- 引用符の数が奇数の場合、引用符をスペースに置換

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 検索 | search_records_N | SELECT | 全文検索インデックスを使用した検索 |
| カード取得 | cards | SELECT | カード番号での直接取得 |
| 検索履歴取得 | search_queries | SELECT | 最近の検索クエリ取得 |

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

#### search_records_N（シャーディングテーブル）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | title, content | FULLTEXTインデックスで検索 | account_key, content, titleで検索 |
| SELECT | board_id | user.board_idsに含まれること | アクセス権限フィルタ |
| SELECT | account_id | user.account_idと一致 | アカウント制限 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 空結果 | 検索結果が0件 | 「結果なし」メッセージ表示 |

### リトライ仕様

リトライは不要

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

参照系処理のため、トランザクション制御は不要。

## パフォーマンス要件

- レスポンス時間: 500ms以内（大量データ時も）
- FULLTEXTインデックスを使用した高速検索
- search_records_Nテーブルはアカウントごとにシャーディング

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

- ログイン必須（ApplicationControllerで制御）
- ユーザーがアクセス可能なボード内のみ検索（board_idフィルタ）
- アカウント制限（account_idフィルタ）
- クエリのサニタイズによるインジェクション対策

## 備考

- search_records_Nテーブルは0〜15の16個にシャーディングされている
- カードとコメントの両方がsearch_recordsに登録される
- 検索結果はcreated_atの降順でソート

---

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

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

### 推奨読解順序

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

まず、検索用のデータモデルとシャーディング構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | record.rb | `app/models/search/record.rb` | Search::Recordモデル、シャーディング、検索スコープ |
| 1-2 | query.rb | `app/models/search/query.rb` | Search::Queryモデル、クエリサニタイズ |

**読解のコツ**:
- `Search::Record.for(account_id)`でシャーディングされたテーブルを選択
- FULLTEXTインデックスを使用した全文検索
- Search::Queryでクエリのサニタイズを実施

**主要処理フロー（record.rb）**:
- **9-17行目**: upsert!メソッド（検索レコードの作成・更新）
- **19-22行目**: card_joinメソッド（カードとの結合）
- **25-32行目**: for_queryスコープ（クエリとユーザー権限でフィルタ）
- **35-42行目**: searchスコープ（検索実行とプリロード）

**主要処理フロー（query.rb）**:
- **10-17行目**: wrapクラスメソッド（クエリのラップ）
- **21-32行目**: sanitizeメソッド（クエリのサニタイズ）
- **35-36行目**: remove_invalid_search_characters（無効文字除去）
- **39-44行目**: remove_unbalanced_quotes（アンバランスな引用符処理）

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

検索リクエストを受け取るコントローラーの処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | searches_controller.rb | `app/controllers/searches_controller.rb` | showアクションの処理フロー |

**主要処理フロー**:
- **4行目**: showアクション開始
- **5行目**: クエリパラメータの取得（空白時はnil）
- **7-8行目**: カード番号での直接ヒットチェック
- **10行目**: 全文検索の実行とページネーション
- **11行目**: 検索履歴の取得（最大10件）

**読解のコツ**:
- `Current.user.accessible_cards.find_by_id(@query)`でカード番号直接アクセス
- `Current.user.search(@query)`でUser::Searcherモジュールの検索を呼び出し

#### Step 3: ユーザーの検索機能を理解する

User::Searcherコンサーンの検索ロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | searcher.rb | `app/models/user/searcher.rb` | searchメソッドとremember_searchメソッド |

**主要処理フロー**:
- **5行目**: search_queriesアソシエーション定義
- **8-9行目**: searchメソッド（Search::Record.searchを呼び出し）
- **12-16行目**: remember_searchメソッド（検索履歴の保存・更新）

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

```
SearchesController#show
    │
    ├─ params[:q].blank? ? nil : params[:q]
    │      └─ @query に検索クエリを設定
    │
    ├─ Current.user.accessible_cards.find_by_id(@query)
    │      └─ カード番号での直接ヒットチェック
    │
    ├─ [カードヒット時] @card = card
    │
    └─ [テキスト検索時]
           │
           ├─ Current.user.search(@query)
           │      │
           │      └─ User::Searcher#search
           │             │
           │             └─ Search::Record.for(account_id).search(terms, user: self)
           │                    │
           │                    ├─ Search::Query.wrap(query)
           │                    │      └─ サニタイズ処理
           │                    │
           │                    └─ for_query(query, user: user)
           │                           └─ matching + where(board_id: user.board_ids)
           │
           ├─ set_page_and_extract_portion_from(...)
           │      └─ ページネーション処理
           │
           └─ Current.user.search_queries.order(updated_at: :desc).limit(10)
                  └─ @recent_search_queries に検索履歴を設定
```

### データフロー図

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

q（検索クエリ）───▶ クエリ取得              ───▶ @query
                       │
                       ▼
                 カード番号チェック
                       │
            ┌──────────┴──────────┐
            ▼                     ▼
    カードヒット時           テキスト検索時
            │                     │
            ▼                     ▼
    @card = card         Search::Record.search
                               │
                               ▼
                         ページネーション  ───▶ @page
                               │
                               ▼
                         検索履歴取得     ───▶ @recent_search_queries
                               │
                               ▼
                         画面表示         ───▶ ブラウザ
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| searches_controller.rb | `app/controllers/searches_controller.rb` | コントローラー | 検索リクエストの処理 |
| record.rb | `app/models/search/record.rb` | モデル | 検索レコード、FULLTEXTインデックス使用 |
| query.rb | `app/models/search/query.rb` | モデル | 検索クエリのサニタイズ |
| searcher.rb | `app/models/user/searcher.rb` | コンサーン | ユーザーの検索機能 |
| searchable.rb | `app/models/card/searchable.rb` | コンサーン | カードの検索可能化 |
| searchable.rb | `app/models/comment/searchable.rb` | コンサーン | コメントの検索可能化 |
| show.html.erb | `app/views/searches/show.html.erb` | ビュー | 検索結果画面 |
| _results.html.erb | `app/views/searches/_results.html.erb` | パーシャル | 検索結果リスト |
| _result.html.erb | `app/views/searches/_result.html.erb` | パーシャル | 検索結果アイテム |
| _form.html.erb | `app/views/searches/_form.html.erb` | パーシャル | 検索フォーム |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義 |
| schema.rb | `db/schema.rb` | スキーマ | search_records_Nテーブル定義 |
