# 機能設計書 194-検索サービス

## 概要

本ドキュメントは、Chatwootにおける検索サービス（SearchService）の設計を記載したものである。この機能は検索APIの中核となるビジネスロジックを実装し、会話、メッセージ、連絡先、記事の全文検索を提供する。

### 本機能の処理概要

**業務上の目的・背景**：大規模なカスタマーサポートデータベースから効率的に情報を検索するには、最適化された検索ロジックが必要である。SearchServiceは、検索クエリの処理、権限に基づくフィルタリング、複数の検索バックエンド（PostgreSQL GINインデックス、ILIKE、Elasticsearch）の使い分けを一元管理し、高性能な検索体験を提供する。

**機能の利用シーン**：検索API（No.193）から呼び出され、ユーザーの検索リクエストを処理する。エージェントが顧客情報を検索する際、システムは自動的に最適な検索方法を選択し、権限に基づいてフィルタリングされた結果を返却する。

**主要な処理内容**：
1. 検索タイプ（会話/メッセージ/連絡先/記事/全て）に応じた処理ルーティング
2. ユーザーのインボックスアクセス権限に基づくフィルタリング
3. 検索バックエンドの選択（GINインデックス、ILIKE、Elasticsearch）
4. 時間範囲、送信者、インボックスIDによる追加フィルタリング
5. ページネーション処理

**関連システム・外部連携**：PostgreSQL（GINインデックス）、Elasticsearch（Searchkick）との連携。

**権限による制御**：administrator権限またはすべてのインボックスにアクセス可能なユーザーはインボックスフィルタリングをスキップ。それ以外のユーザーは割り当てられたインボックスのみ検索可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 27 | 検索画面 | 主画面 | 検索ロジックの実行 |

## 機能種別

ビジネスロジック（検索処理）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| current_user | User | Yes | 現在のユーザーオブジェクト | - |
| current_account | Account | Yes | 現在のアカウントオブジェクト | - |
| params | Hash | Yes | 検索パラメータ（q, page, since, until, from, inbox_id） | - |
| search_type | String | Yes | 検索タイプ（'all', 'Conversation', 'Contact', 'Message', 'Article'） | - |

### 入力データソース

- SearchController経由での呼び出し

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| conversations | Array | 検索にマッチした会話一覧（search_type='all'または'Conversation'時） |
| contacts | Array | 検索にマッチした連絡先一覧（search_type='all'または'Contact'時） |
| messages | Array | 検索にマッチしたメッセージ一覧（search_type='all'または'Message'時） |
| articles | Array | 検索にマッチした記事一覧（search_type='all'または'Article'時） |

### 出力先

- 呼び出し元（SearchController）への戻り値

## 処理フロー

### 処理シーケンス

```
1. perform メソッド実行
   └─ search_type に応じた分岐
2. 会話検索 (filter_conversations)
   ├─ アクセス可能なインボックスでフィルタ
   ├─ conversationsとcontactsのJOIN
   ├─ display_id, name, email, phone_number, identifierでILIKE検索
   ├─ advanced_search有効時は時間フィルタ適用
   └─ ページネーション
3. メッセージ検索 (filter_messages)
   ├─ 検索方法の選択
   │   ├─ search_with_gin有効: GINインデックス検索
   │   ├─ advanced_search有効: Elasticsearch（フォールバック付き）
   │   └─ それ以外: ILIKE検索
   ├─ 過去3ヶ月以内のメッセージのみ対象
   └─ ページネーション
4. 連絡先検索 (filter_contacts)
   ├─ name, email, phone_number, identifierでILIKE検索
   ├─ advanced_search有効時は時間フィルタ適用
   └─ ページネーション
5. 記事検索 (filter_articles)
   ├─ text_searchスコープ使用
   ├─ advanced_search有効時は時間フィルタ適用
   └─ ページネーション
```

### フローチャート

```mermaid
flowchart TD
    A[perform] --> B{search_type?}
    B -->|'all'| C[全タイプ検索]
    B -->|'Message'| D[filter_messages]
    B -->|'Conversation'| E[filter_conversations]
    B -->|'Contact'| F[filter_contacts]
    B -->|'Article'| G[filter_articles]

    C --> D
    C --> E
    C --> F
    C --> G

    D --> H{search_with_gin?}
    H -->|Yes| I[filter_messages_with_gin]
    H -->|No| J{advanced_search?}
    J -->|Yes| K[advanced_search_with_fallback]
    J -->|No| L[filter_messages_with_like]

    K --> M{Elasticsearch成功?}
    M -->|Yes| N[Elasticsearch結果]
    M -->|No| O[SQL検索にフォールバック]

    I --> P[結果返却]
    L --> P
    N --> P
    O --> P
    E --> P
    F --> P
    G --> P
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-194-01 | インボックスフィルタリング | 管理者または全インボックスアクセス可能なユーザー以外はアクセス可能なインボックスでフィルタ | 会話・メッセージ検索時 |
| BR-194-02 | メッセージ期間制限 | メッセージ検索は過去3ヶ月以内のみ対象 | メッセージ検索時 |
| BR-194-03 | 時間フィルタ上限 | since/untilパラメータは90日以内に制限（キャップ） | advanced_search有効時 |
| BR-194-04 | GINインデックス優先 | search_with_gin機能有効時はGINインデックス検索を優先 | メッセージ検索時 |
| BR-194-05 | Elasticsearchフォールバック | Elasticsearch接続失敗時はSQL検索にフォールバック | advanced_search有効時 |

### 計算ロジック

**時間フィルタのキャップ計算**:
```ruby
def cap_since_time(since_param)
  max_lookback = 90.days.ago
  requested_time = Time.zone.at(since_param.to_i)
  [requested_time, max_lookback].max
end

def cap_until_time(until_param)
  max_future = 90.days.from_now
  requested_time = Time.zone.at(until_param.to_i)
  [requested_time, max_future].min
end
```

**送信者フィルタのパース**:
```ruby
# from_param形式: "contact:123" or "agent:456"
# -> sender_type: "Contact" or "User"
# -> sender_id: 123 or 456
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 会話検索 | conversations, contacts | SELECT | JOIN検索 |
| メッセージ検索 | messages | SELECT | 全文検索 |
| 連絡先検索 | contacts | SELECT | ILIKE検索 |
| 記事検索 | articles | SELECT | 全文検索 |
| アクセス権確認 | account_users, inbox_members | SELECT | インボックスアクセス権取得 |

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

#### messages（GINインデックス検索）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | content | `content @@ to_tsquery(tsquery)` | GINインデックス使用 |
| WHERE | created_at | `>= 3.months.ago` | 期間制限 |
| WHERE | inbox_id | `IN (accessable_inbox_ids)` | アクセス権制限 |

#### messages（ILIKE検索）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | content | `ILIKE '%search_query%'` | 部分一致 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | Faraday::ConnectionFailed | Elasticsearch接続失敗 | SQL検索にフォールバック |
| - | Searchkick::Error | Searchkickエラー | SQL検索にフォールバック |
| - | Elasticsearch::Transport::Transport::Error | Elasticsearchトランスポートエラー | SQL検索にフォールバック |

### リトライ仕様

- Elasticsearch接続失敗時は自動的にSQL検索にフォールバック（リトライなし）

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

- 読み取り専用のため、明示的なトランザクション管理なし

## パフォーマンス要件

- GINインデックスによる高速全文検索
- ページネーションによる結果セット制限（15件/ページ）
- 3ヶ月以内のメッセージのみ検索対象とすることでスキャン範囲を限定

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

- インボックスアクセス権限に基づくデータフィルタリング
- パラメータ化クエリによるSQLインジェクション対策

## 備考

- `prepend_mod_with('SearchService')`によりEnterprise版での拡張が可能
- advanced_search機能はElasticsearchとの統合を含む高度な検索機能を提供

---

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

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

### 推奨読解順序

#### Step 1: サービスクラスの構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | search_service.rb | `app/services/search_service.rb` | 検索サービスの全体構造 |

**主要処理フロー**:
1. **2行目**: `pattr_initialize`による初期化 - 必須パラメータの定義
2. **4-6行目**: `account_user`メソッド - 現在のユーザーのアカウント権限取得
3. **8-21行目**: `perform`メソッド - 検索タイプに応じた処理ルーティング
4. **25-27行目**: `accessable_inbox_ids`メソッド - アクセス可能なインボックスID取得
5. **29-31行目**: `search_query`メソッド - 検索クエリの取得と整形

**読解のコツ**: `pattr_initialize`は`attr_initialize`のprivate版で、初期化パラメータをプライベートなインスタンス変数として設定する。`!`サフィックスは必須パラメータを示す。

#### Step 2: 各検索メソッドを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | search_service.rb | `app/services/search_service.rb` | 会話検索ロジック（33-47行目） |
| 2-2 | search_service.rb | `app/services/search_service.rb` | メッセージ検索ロジック（49-105行目） |
| 2-3 | search_service.rb | `app/services/search_service.rb` | 連絡先検索ロジック（164-175行目） |
| 2-4 | search_service.rb | `app/services/search_service.rb` | 記事検索ロジック（177-182行目） |

**主要処理フロー（メッセージ検索）**:
- **49-57行目**: `filter_messages` - 検索方法の選択ロジック
- **59-64行目**: `advanced_search_with_fallback` - Elasticsearchとフォールバック
- **66-68行目**: `should_run_advanced_search?` - advanced_search有効判定
- **72-96行目**: `filter_messages_with_gin` - GINインデックス検索
- **98-105行目**: `filter_messages_with_like` - ILIKE検索
- **107-111行目**: `message_base_query` - ベースクエリ（3ヶ月制限）

#### Step 3: フィルタリングロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | search_service.rb | `app/services/search_service.rb` | 時間フィルタ（184-205行目） |
| 3-2 | search_service.rb | `app/services/search_service.rb` | 送信者フィルタ（121-134行目） |
| 3-3 | search_service.rb | `app/services/search_service.rb` | インボックスフィルタ（136-158行目） |

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

```
SearchService#perform
    │
    ├─ filter_conversations
    │      ├─ accessable_inbox_ids
    │      ├─ apply_time_filter (advanced_search時)
    │      └─ page/per (ページネーション)
    │
    ├─ filter_messages
    │      ├─ use_gin_search → filter_messages_with_gin
    │      │                      ├─ message_base_query
    │      │                      ├─ apply_message_filters
    │      │                      └─ GIN tsquery検索
    │      │
    │      ├─ should_run_advanced_search? → advanced_search_with_fallback
    │      │                                  └─ advanced_search (Elasticsearch)
    │      │                                         ↓ (失敗時)
    │      │                                  filter_messages_with_like
    │      │
    │      └─ filter_messages_with_like
    │             ├─ message_base_query
    │             ├─ apply_message_filters
    │             └─ ILIKE検索
    │
    ├─ filter_contacts
    │      ├─ apply_time_filter (advanced_search時)
    │      └─ page/per (ページネーション)
    │
    └─ filter_articles
           ├─ text_search (スコープ)
           ├─ apply_time_filter (advanced_search時)
           └─ page/per (ページネーション)
```

### データフロー図

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

current_user      ───▶ SearchService
current_account   ───▶     │
params            ───▶     ▼
search_type       ───▶ perform
                           │
                  ┌────────┴────────┐
                  ▼                  ▼
        accessable_inbox_ids    search_query
                  │                  │
                  └────────┬─────────┘
                           ▼
              検索タイプ別処理分岐
                           │
            ┌──────┬───────┼───────┬──────┐
            ▼      ▼       ▼       ▼      ▼
          会話   メッセージ 連絡先  記事   全て
            │      │       │       │      │
            ▼      ▼       ▼       ▼      ▼
        フィルタリング & ページネーション
                           │
                           ▼
                      { 検索結果 }
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| search_service.rb | `app/services/search_service.rb` | ソース | 検索ロジック実装 |
| search_controller.rb | `app/controllers/api/v1/accounts/search_controller.rb` | ソース | 検索API（呼び出し元） |
| message.rb | `app/models/message.rb` | ソース | Messageモデル（searchkick設定） |
| article.rb | `app/models/article.rb` | ソース | Articleモデル（text_searchスコープ） |
| contact.rb | `app/models/contact.rb` | ソース | Contactモデル |
| conversation.rb | `app/models/conversation.rb` | ソース | Conversationモデル |
