# 機能設計書 81-高度な検索

## 概要

本ドキュメントは、Fat Free CRMにおける「高度な検索（Advanced Search）」機能の設計仕様を定義する。Ransack gemを活用した柔軟で複雑な検索条件をサポートする機能である。

### 本機能の処理概要

高度な検索機能は、ユーザーが複数の条件を組み合わせてエンティティ（取引先、キャンペーン、リード、連絡先、商談）を検索できる機能である。基本検索（テキストベース）に比べ、フィールド指定、条件演算子、AND/OR結合などを柔軟に設定できる。

**業務上の目的・背景**：営業活動において、特定の条件に合致する見込み客や取引先を効率的に抽出する必要がある。例えば「過去30日以内に作成された、特定の地域の、一定金額以上の商談」といった複合条件での検索は、基本検索では対応できない。高度な検索機能により、ユーザーは複雑な条件を視覚的に構築し、必要なデータを迅速に抽出できる。

**機能の利用シーン**：
- マーケティング担当者が特定条件に合致するリードを抽出してキャンペーン対象とする場合
- 営業マネージャーが特定のステージ・金額範囲の商談を一括で確認する場合
- 管理者が特定期間に更新されたエンティティを監査する場合

**主要な処理内容**：
1. 検索フォームの表示：Ransack UIを使用した動的検索フォームをレンダリング
2. 検索条件の解析：params[:q]から検索条件を抽出しRansackオブジェクトを構築
3. 検索実行：条件に基づいてActiveRecordクエリを生成し実行
4. 結果の表示：検索結果をページネーション付きで一覧表示

**関連システム・外部連携**：
- Ransack gem：検索条件の解析とSQLクエリ生成
- ransack_ui gem：検索フォームのUI生成
- Select2：オートコンプリート付き入力フィールド

**権限による制御**：検索結果はユーザーのアクセス権限に基づいてフィルタリングされる。CanCanを使用し、ユーザーがアクセス可能なエンティティのみが検索対象となる。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 7 | 取引先一覧画面 | 主画面 | 取引先の高度な検索 |
| 11 | キャンペーン一覧画面 | 主画面 | キャンペーンの高度な検索 |
| 15 | リード一覧画面 | 主画面 | リードの高度な検索 |
| 20 | 連絡先一覧画面 | 主画面 | 連絡先の高度な検索 |
| 24 | 商談一覧画面 | 主画面 | 商談の高度な検索 |

## 機能種別

検索・フィルタリング処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| q | Hash | No | Ransack検索パラメータ | Ransack predicateの検証 |
| q[c] | Array | No | 検索条件グループ | 有効なフィールド名・述語の組み合わせ |
| q[s] | String | No | ソート条件 | 有効なソート可能カラム名 |
| page | Integer | No | ページ番号 | 正の整数 |
| per_page | Integer | No | 1ページあたりの表示件数 | 1〜200の整数 |

### 入力データソース

- 画面入力：高度な検索フォーム（ransack_ui_search）
- URLパラメータ：検索条件を含むクエリストリング
- セッション：保存されたリスト（検索条件）の復元

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| @search_results_count | Integer | 検索結果の総件数 |
| @ransack_search | Ransack::Search | Ransack検索オブジェクト |
| entities | ActiveRecord::Relation | 検索結果のエンティティコレクション |

### 出力先

- 画面表示：一覧画面（index）に検索結果を表示
- JSON/XML：API経由でのデータ取得時
- CSV/XLS：エクスポート機能使用時

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ GET /accounts?q[c][...]=...
2. before_action: load_ransack_search
   └─ klass.ransack(params[:q]) でRansack::Searchオブジェクト生成
3. indexアクション実行
   └─ get_list_of_records呼び出し
4. 検索条件の適用
   └─ ransack_search.result(distinct: true) でクエリ生成
5. アクセス権限フィルタリング
   └─ entities.merge() で権限スコープ適用
6. ページネーション
   └─ paginate(page: current_page, per_page: per_page)
7. 結果表示
   └─ index.html.haml / index.js.haml でレンダリング
```

### フローチャート

```mermaid
flowchart TD
    A[一覧画面表示リクエスト] --> B{params[:q]存在?}
    B -->|Yes| C[Ransack検索オブジェクト生成]
    B -->|No| D[空の検索オブジェクト生成]
    C --> E[検索条件を適用]
    D --> E
    E --> F[ユーザー権限でフィルタリング]
    F --> G{セッションフィルタ?}
    G -->|Yes, q無し| H[ステータスフィルタ適用]
    G -->|No, q有り| I[高度検索優先]
    H --> J[ページネーション処理]
    I --> J
    J --> K[検索結果カウント]
    K --> L[結果表示]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-81-01 | 高度検索優先 | 高度検索実行時はセッションのステータスフィルタを無視 | params[:q]が存在する場合 |
| BR-81-02 | ソート指定 | 高度検索時はRansackのソート指定を使用 | params[:q]が存在する場合 |
| BR-81-03 | デフォルトソート | ソート未指定時はモデルのdefaultソート順を使用 | @ransack_search.sorts.empty?の場合 |
| BR-81-04 | 権限フィルタリング | 検索結果はユーザーのアクセス権限内のみ | 常時 |

### 計算ロジック

- **検索結果カウント**: `@search_results_count = scope.size`
- **ページネーション**: `scope.paginate(page: current_page, per_page: per_page)`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 高度検索 | accounts/campaigns/contacts/leads/opportunities | SELECT | 条件に合致するエンティティを取得 |
| 関連取得 | tags, addresses, comments等 | SELECT | 検索条件の関連テーブル参照 |

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

#### 各エンティティテーブル（accounts等）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | 全カラム | Ransackが生成するWHERE条件 | DISTINCTで重複除去 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 無効な検索条件 | 不正なフィールド名やpredicateの指定 | Ransackが無視して処理続行 |
| - | アクセス権限エラー | 権限のないエンティティへのアクセス | CanCanによるフィルタリング |

### リトライ仕様

検索処理にリトライ機能はない。エラー発生時はユーザーに検索条件の修正を促す。

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

検索処理は読み取りのみのため、トランザクション管理は不要。

## パフォーマンス要件

- 検索結果の表示：3秒以内
- ページネーションにより1リクエストあたり最大200件に制限
- インデックスを活用した効率的なクエリ実行

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

- **認証**：ログインユーザーのみアクセス可能（authenticate_user!）
- **認可**：CanCanによるエンティティレベルのアクセス制御
- **SQLインジェクション対策**：RansackによるパラメータのサニタイズとActiveRecordクエリビルダーの使用
- **許可されたpredicate**：config/initializers/ransack.rbで使用可能な検索述語を制限

## 備考

- Ransackの設定はconfig/initializers/ransack.rbで管理
- 各モデルで`has_ransackable_associations`により検索可能な関連を定義
- `ransack_can_autocomplete`でオートコンプリート連携を有効化

---

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

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

### 推奨読解順序

#### Step 1: Ransack設定を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | ransack.rb | `config/initializers/ransack.rb` | Ransackの全体設定、使用可能なpredicate、Ajax設定を確認 |

**読解のコツ**: `default_predicates`で許可される検索条件（cont, eq, gt, lt等）を確認。`ajax_options`でオートコンプリートURLの設定を確認。

#### Step 2: モデルのRansack設定を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | account.rb | `app/models/entities/account.rb` | `has_ransackable_associations`と`ransack_can_autocomplete`の設定 |
| 2-2 | lead.rb | `app/models/entities/lead.rb` | `text_search`スコープでのRansack使用例 |

**主要処理フロー**:
- **55行目**: `scope :text_search`でRansackを使用した簡易検索
- **73行目**: `has_ransackable_associations`で検索可能な関連を定義
- **74行目**: `ransack_can_autocomplete`でオートコンプリート有効化

#### Step 3: コントローラーの検索処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | entities_controller.rb | `app/controllers/entities_controller.rb` | 検索処理のメインロジック |

**主要処理フロー**:
- **13行目**: `before_action :load_ransack_search`で検索オブジェクト初期化
- **130-134行目**: `ransack_search`メソッドでRansack::Searchオブジェクト取得
- **143行目**: `advanced_search = params[:q].present?`で高度検索判定
- **146行目**: `scope = entities.merge(ransack_search.result(distinct: true))`で検索実行
- **148-152行目**: 高度検索時はセッションフィルタを無視
- **157-161行目**: 高度検索時はRansackのソートを使用

#### Step 4: ビューの検索フォームを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | _search.html.haml | `app/views/entities/_search.html.haml` | 基本/高度検索の切り替えUI |

**主要処理フロー**:
- **5-7行目**: タブ切り替えUIの生成
- **12-13行目**: 基本検索フォームの表示制御
- **15-16行目**: `ransack_ui_search`で高度検索フォーム生成

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

```
ブラウザ（検索リクエスト）
    │
    ├─ routes.rb
    │      └─ GET /:controller?q[...]=...
    │
    └─ EntitiesController
           │
           ├─ before_action :load_ransack_search
           │      └─ klass.ransack(params[:q])
           │
           ├─ #index
           │      └─ get_list_of_records
           │             │
           │             ├─ ransack_search
           │             │      └─ @ransack_search.result(distinct: true)
           │             │
           │             ├─ scope.state(filter) # 高度検索時はスキップ
           │             │
           │             └─ scope.paginate(page:, per_page:)
           │
           └─ View Rendering
                  └─ entities/_search.html.haml
                         └─ ransack_ui_search
```

### データフロー図

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

params[:q]      ───────▶  Ransack.ransack(params[:q])  ───▶  Ransack::Search
                               │
                               ▼
Ransack::Search  ───────▶  .result(distinct: true)     ───▶  ActiveRecord::Relation
                               │
                               ▼
A/R Relation     ───────▶  .merge(entities)            ───▶  スコープ付きRelation
（権限スコープ）                  │
                               ▼
                          .paginate(page:, per_page:)  ───▶  ページネーション済み結果
                               │
                               ▼
                          View Rendering               ───▶  HTML/JS/JSON
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| ransack.rb | `config/initializers/ransack.rb` | 設定 | Ransackの全体設定 |
| entities_controller.rb | `app/controllers/entities_controller.rb` | ソース | 検索処理の親コントローラー |
| accounts_controller.rb | `app/controllers/entities/accounts_controller.rb` | ソース | 取引先固有の検索処理 |
| account.rb | `app/models/entities/account.rb` | ソース | 取引先モデル（Ransack設定含む） |
| campaign.rb | `app/models/entities/campaign.rb` | ソース | キャンペーンモデル |
| contact.rb | `app/models/entities/contact.rb` | ソース | 連絡先モデル |
| lead.rb | `app/models/entities/lead.rb` | ソース | リードモデル |
| opportunity.rb | `app/models/entities/opportunity.rb` | ソース | 商談モデル |
| _search.html.haml | `app/views/entities/_search.html.haml` | テンプレート | 検索フォームUI |
| search.js.coffee | `app/assets/javascripts/search.js.coffee` | JavaScript | 検索フォームの動的処理 |
| advanced_search.css.scss | `app/assets/stylesheets/advanced_search.css.scss` | スタイル | 高度検索フォームのスタイル |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（advanced_search） |
