# 機能設計書 26-会話検索・フィルタリング

## 概要

本ドキュメントは、Chatwootにおける会話のメタ情報取得、検索、フィルタリング機能について記述する。この機能は、大量の会話の中からエージェントが必要な会話を効率的に見つけるための検索・絞り込み機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：カスタマーサポート業務において、担当する会話を素早く見つけることは業務効率に直結する。ステータス、担当者、インボックス、ラベル、チームなど複数の条件で会話を絞り込み、テキスト検索で特定のメッセージを含む会話を特定できることで、エージェントの生産性を向上させる。

**機能の利用シーン**：
- ダッシュボードでの会話一覧表示時のフィルタリング
- 検索ボックスでのテキスト検索
- カスタムフィルタによる高度な絞り込み
- 担当者種別（自分/未割り当て/全て）での切り替え

**主要な処理内容**：
1. ConversationFinderによる基本的なフィルタリング（ステータス、担当者、インボックス、チーム、ラベル）
2. FilterServiceによる高度なフィルタリング（カスタム属性、複合条件）
3. テキスト検索（メッセージ内容のILIKE検索）
4. ソートオプション（最終活動日時、作成日時、優先度、待機時間）
5. カウント情報の取得（mine/assigned/unassigned/all）

**関連システム・外部連携**：
- Searchkick（Elasticsearch統合、オプション）
- Kaminariによるページネーション

**権限による制御**：PermissionFilterServiceにより、ユーザーがアクセス可能なインボックスの会話のみが返される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | ダッシュボードホーム画面 | 主画面 | 会話のフィルタリングと検索 |
| 27 | 検索画面 | 主画面 | 会話・メッセージの横断検索 |
| 14 | カスタムビュー会話一覧 | 参照画面 | 保存済みフィルタの適用 |

## 機能種別

検索・フィルタリング

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| inbox_id | Integer | No | インボックスID | 存在チェック |
| assignee_type | String | No | 担当者種別 | me/unassigned/assigned/all |
| status | String | No | ステータス | open/resolved/pending/snoozed/all |
| team_id | Integer | No | チームID | 存在チェック |
| labels | Array | No | ラベル名配列 | - |
| q | String | No | 検索クエリ | - |
| source_id | String | No | ソースID | - |
| conversation_type | String | No | 会話種別 | mention/participating/unattended |
| sort_by | String | No | ソート条件 | SORT_OPTIONSのキー |
| page | Integer | No | ページ番号 | 1以上 |
| updated_within | Integer | No | 更新秒数以内 | 正の整数 |

### 入力データソース

- APIエンドポイント: `/api/v1/accounts/:account_id/conversations` (index, meta, search)
- APIエンドポイント: `/api/v1/accounts/:account_id/conversations/filter` (高度なフィルタ)

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| conversations | Array | 会話オブジェクトの配列 |
| count.mine_count | Integer | 自分担当の会話数 |
| count.assigned_count | Integer | 割り当て済み会話数 |
| count.unassigned_count | Integer | 未割り当て会話数 |
| count.all_count | Integer | 全会話数 |

### 出力先

- APIレスポンス: JSON形式の会話一覧とカウント情報

## 処理フロー

### 処理シーケンス

```
1. 基本フィルタリング（ConversationFinder）
   1-1. set_up: インボックス、チーム、担当者種別の設定
   1-2. find_all_conversations: 権限フィルタ適用
   1-3. filter_by_status: ステータスでフィルタ
   1-4. filter_by_team: チームでフィルタ
   1-5. filter_by_labels: ラベルでフィルタ
   1-6. filter_by_query: テキスト検索
   1-7. filter_by_source_id: ソースIDでフィルタ
   1-8. set_count_for_all_conversations: カウント計算
   1-9. filter_by_assignee_type: 担当者種別でフィルタ

2. 高度なフィルタリング（FilterService）
   2-1. validate_query_operator: クエリ演算子検証
   2-2. query_builder: フィルタ条件を構築
   2-3. base_relation: 権限フィルタ適用済みの基本クエリ

3. ソートとページネーション
   3-1. sort_by, sort_orderの適用
   3-2. Kaminariによるページネーション
```

### フローチャート

```mermaid
flowchart TD
    A[検索/フィルタリクエスト] --> B{エンドポイント}
    B -->|index/meta/search| C[ConversationFinder]
    B -->|filter| D[FilterService]

    C --> E[set_inboxes]
    E --> F[find_all_conversations]
    F --> G[PermissionFilterService]
    G --> H[各種filter_by_*]
    H --> I[カウント計算]
    I --> J[ソート・ページネーション]

    D --> K[validate_query_operator]
    K --> L[query_builder]
    L --> M[base_relation + PermissionFilter]
    M --> N[カウント計算]
    N --> J

    J --> O[結果返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-26-01 | デフォルトステータス | status未指定時は'open'を使用 | 常時 |
| BR-26-02 | 権限フィルタ | ユーザーがアクセス可能なインボックスの会話のみ返却 | 常時 |
| BR-26-03 | デフォルトソート | sort_by未指定時は'last_activity_at_desc'を使用 | 常時 |
| BR-26-04 | ページサイズ | CONVERSATION_RESULTS_PER_PAGE環境変数（デフォルト25） | ページネーション時 |
| BR-26-05 | ラベルフィルタ | 複数ラベル指定時はOR条件（any: true） | labels指定時 |
| BR-26-06 | テキスト検索 | incoming/outgoingメッセージのみ対象 | q指定時 |

### 計算ロジック

**カウント計算**:
- mine_count: @conversations.assigned_to(current_user).count
- unassigned_count: @conversations.unassigned.count
- all_count: @conversations.count
- assigned_count: all_count - unassigned_count

**ソートオプション**:
```ruby
{
  'last_activity_at_asc' => ['sort_on_last_activity_at', 'asc'],
  'last_activity_at_desc' => ['sort_on_last_activity_at', 'desc'],
  'created_at_asc' => ['sort_on_created_at', 'asc'],
  'created_at_desc' => ['sort_on_created_at', 'desc'],
  'priority_asc' => ['sort_on_priority', 'asc'],
  'priority_desc' => ['sort_on_priority', 'desc'],
  'waiting_since_asc' => ['sort_on_waiting_since', 'asc'],
  'waiting_since_desc' => ['sort_on_waiting_since', 'desc']
}
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 会話検索 | conversations | SELECT | 条件に基づく会話取得 |
| 会話検索 | messages | SELECT（JOIN） | テキスト検索時にJOIN |
| 会話検索 | taggings | SELECT（JOIN） | ラベルフィルタ時にJOIN |
| 会話検索 | contact_inboxes | SELECT（JOIN） | source_idフィルタ時にJOIN |

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

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | status | 指定ステータスで絞り込み | enum値 |
| SELECT | assignee_id | 担当者種別で絞り込み | null/特定ID/存在有無 |
| SELECT | inbox_id | 指定インボックスで絞り込み | 複数指定可 |
| SELECT | team_id | 指定チームで絞り込み | - |
| SELECT | last_activity_at | ソート、updated_within条件 | - |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | InvalidAttribute | フィルタ条件の属性が不正 | エラーメッセージを返却 |
| - | InvalidOperator | フィルタ条件の演算子が不正 | エラーメッセージを返却 |
| - | InvalidQueryOperator | クエリ演算子が不正 | エラーメッセージを返却 |
| - | InvalidValue | フィルタ条件の値が不正 | エラーメッセージを返却 |

### リトライ仕様

リトライ不要（同期処理）

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

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

## パフォーマンス要件

- ページサイズ: 25件（環境変数で調整可能）
- 複合インデックス活用: (account_id, inbox_id, status, assignee_id)
- Eager Loading: taggings, inbox, assignee, contact, team, contact_inbox
- N+1回避: includesによる事前ロード

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

- PermissionFilterServiceによる権限フィルタリング
- 管理者以外は自分がアクセス可能なインボックスのみ
- SQLインジェクション対策（パラメータ化クエリ）

## 備考

- v3.5.0で一部ソートオプション（'latest', 'sort_on_*'）は削除予定
- Enterprise版ではConversationFinderが拡張される

---

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

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

### 推奨読解順序

#### Step 1: 基本的なFinderを理解する

ConversationFinderの構造とフィルタリングロジックを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | conversation_finder.rb | `app/finders/conversation_finder.rb` | 基本フィルタリング |

**読解のコツ**:
- **4-20行目**: SORT_OPTIONSでソート条件を定義
- **39-56行目**: `perform`メソッドがエントリーポイント
- **60-71行目**: `set_up`で各種フィルタを順次適用

**主要処理フロー**:
1. **73-79行目**: `set_inboxes`でユーザーのアクセス可能インボックスを設定
2. **97-107行目**: `find_all_conversations`で基本クエリ構築と権限フィルタ
3. **109-119行目**: `filter_by_assignee_type`で担当者種別フィルタ
4. **134-142行目**: `filter_by_query`でテキスト検索
5. **187-198行目**: `conversations`でソートとページネーション

#### Step 2: 高度なフィルタリングを理解する

FilterServiceによる複合条件フィルタリングを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | filter_service.rb | `app/services/conversations/filter_service.rb` | 高度なフィルタ |
| 2-2 | filter_service.rb | `app/services/filter_service.rb` | 基底クラス |

**主要処理フロー**:
- **9-24行目**: `perform`でクエリ構築とカウント計算
- **26-36行目**: `base_relation`で権限フィルタ適用
- **49-51行目**: `conversations`でソートとページネーション

#### Step 3: 権限フィルタを理解する

PermissionFilterServiceによる権限制御を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | permission_filter_service.rb | `app/services/conversations/permission_filter_service.rb` | 権限フィルタ |

#### Step 4: コントローラーでの使用を理解する

APIエンドポイントでの呼び出し方を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | conversations_controller.rb | `app/controllers/api/v1/accounts/conversations_controller.rb` | API呼び出し |

**主要処理フロー**:
- **11-15行目**: `index`でConversationFinder使用
- **17-20行目**: `meta`でカウントのみ取得
- **22-26行目**: `search`で検索実行
- **50-59行目**: `filter`でFilterService使用

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

```
ConversationsController
    │
    ├─ #index / #meta / #search
    │      │
    │      └─ ConversationFinder#perform
    │             │
    │             ├─ #set_up
    │             │      ├─ set_inboxes
    │             │      ├─ set_team
    │             │      ├─ find_all_conversations
    │             │      │      └─ PermissionFilterService#perform
    │             │      ├─ filter_by_status
    │             │      ├─ filter_by_team
    │             │      ├─ filter_by_labels
    │             │      ├─ filter_by_query
    │             │      └─ filter_by_source_id
    │             │
    │             ├─ #set_count_for_all_conversations
    │             ├─ #filter_by_assignee_type
    │             └─ #conversations (ソート・ページネーション)
    │
    └─ #filter
           │
           └─ Conversations::FilterService#perform
                  │
                  ├─ validate_query_operator
                  ├─ query_builder
                  ├─ base_relation
                  │      └─ PermissionFilterService#perform
                  └─ conversations (ソート・ページネーション)
```

### データフロー図

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

パラメータ ───▶ ConversationFinder ───▶ 会話一覧 + カウント
(inbox_id,         │
 status,           ▼
 assignee_type,  set_inboxes
 team_id,          │
 labels,           ▼
 q)            find_all_conversations
                   │
                   ▼
            PermissionFilterService
                   │
                   ▼
            filter_by_* (複数適用)
                   │
                   ▼
            set_count_for_all_conversations
                   │
                   ▼
            ソート・ページネーション
                   │
                   ▼
            { conversations: [...], count: {...} }
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| conversation_finder.rb | `app/finders/conversation_finder.rb` | Finder | 基本フィルタリング |
| filter_service.rb | `app/services/conversations/filter_service.rb` | サービス | 高度なフィルタリング |
| filter_service.rb | `app/services/filter_service.rb` | サービス | フィルタ基底クラス |
| permission_filter_service.rb | `app/services/conversations/permission_filter_service.rb` | サービス | 権限フィルタ |
| conversations_controller.rb | `app/controllers/api/v1/accounts/conversations_controller.rb` | コントローラー | API呼び出し |
| sort_handler.rb | `app/models/concerns/sort_handler.rb` | Concern | ソート処理 |
