# 機能設計書 32-商品検索

## 概要

本ドキュメントは、Shopwareストアフロントにおけるキーワードによる商品検索機能の設計について記述する。

### 本機能の処理概要

本機能は、顧客が入力したキーワードに基づいて商品を検索し、検索結果を一覧表示するための機能である。商品名、説明文、商品番号などを対象に全文検索を行い、関連度順に結果を返却する。

**業務上の目的・背景**：ECサイトにおいて、商品検索は顧客が目的の商品を見つけるための最も重要な機能の一つである。適切な検索機能を提供することで、顧客体験を向上させ、購入率の向上に直結する。検索結果がゼロの場合や、1件だけヒットして商品番号と完全一致した場合は、自動的に商品詳細ページへリダイレクトすることで、顧客の利便性を高める。

**機能の利用シーン**：
- ヘッダーの検索バーにキーワードを入力して検索ボタンをクリックした際
- 検索結果ページで追加のキーワードで再検索する際
- 検索結果ページでソートや絞り込み条件を変更した際

**主要な処理内容**：
1. 検索キーワードの受け取りとバリデーション
2. 商品検索ルート（ProductSearchRoute）の呼び出し
3. 検索結果の取得とリスティング形式への変換
4. 検索結果が商品番号と完全一致かつ1件のみの場合の自動リダイレクト
5. 検索ページのメタ情報設定（SEO対策）
6. 検索結果ページのレンダリング

**関連システム・外部連携**：
- Elasticsearch（オプション）：大規模な商品検索の高速化

**権限による制御**：特になし（匿名ユーザーも利用可能）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 5 | 検索結果ページ | 主画面 | キーワードによる商品検索（SearchController::search） |
| 6 | 検索サジェスト | 関連画面 | 検索入力時のオートコンプリート |

## 機能種別

データ検索・表示（Read操作）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| search | string | Yes | 検索キーワード | 空文字でないこと |
| order | string | No | ソート順（price-asc, price-desc, name-asc, name-desc等） | 許可されたソート順であること |
| p | int | No | ページ番号（デフォルト: 1） | 正の整数 |
| limit | int | No | 1ページあたりの表示件数 | 正の整数 |
| manufacturer | string | No | メーカーIDによるフィルター | UUID形式 |
| properties | array | No | プロパティによるフィルター | プロパティID配列 |
| min-price | float | No | 最低価格フィルター | 0以上の数値 |
| max-price | float | No | 最高価格フィルター | 0以上の数値 |
| shipping-free | bool | No | 送料無料フィルター | true/false |
| rating | int | No | 評価フィルター | 1-5の整数 |

### 入力データソース

- クエリパラメータ（search, order, p, limit, 各種フィルター）
- SalesChannelContext（言語、通貨、顧客グループ等のコンテキスト情報）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| page | SearchPage | 検索結果ページオブジェクト |
| page.listing | ProductListingResult | 商品一覧結果 |
| page.listing.elements | ProductCollection | 商品エンティティの配列 |
| page.listing.total | int | 総検索結果件数 |
| page.listing.aggregations | AggregationResultCollection | フィルター用集計データ |
| page.searchTerm | string | 検索キーワード |
| page.metaInformation | MetaInformation | SEO用メタ情報 |

### 出力先

- HTMLページ（@Storefront/storefront/page/search/index.html.twig）
- 商品番号一致の場合は商品詳細ページへリダイレクト
- 検索パラメータ欠落時はホームページへリダイレクト

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ SearchController::search()がリクエストを受信
2. SearchPageLoader呼び出し
   └─ 検索ページのロード処理を開始
3. GenericPageLoader呼び出し
   └─ 共通ページ情報（ヘッダー、フッター等）をロード
4. 検索条件構築
   └─ Criteriaオブジェクトを生成し、タイトルを設定
5. ProductSearchRoute呼び出し
   └─ 商品検索ルートで商品を検索
6. 検索結果設定
   └─ ProductListingResultをページに設定
7. 検索キーワード設定
   └─ 検索キーワードをページに保持
8. 商品番号一致チェック
   └─ 1件のみかつ商品番号と完全一致の場合はリダイレクト判定
9. イベントディスパッチ
   └─ SearchPageLoadedEventを発行
10. フック実行
    └─ SearchPageLoadedHookを実行
11. メタ情報設定
    └─ robotsをnoindex,followに設定、タイトル設定
12. テンプレートレンダリング
    └─ search/index.html.twigをレンダリングして返却
```

### フローチャート

```mermaid
flowchart TD
    A[開始: search] --> B{searchパラメータあり?}
    B -->|No| C[ホームページへリダイレクト]
    B -->|Yes| D[SearchPageLoader::load]
    D --> E[GenericPageLoader::load]
    E --> F[Criteria構築]
    F --> G[ProductSearchRoute::load]
    G --> H[検索結果取得]
    H --> I{結果件数 = 1?}
    I -->|Yes| J{商品番号と完全一致?}
    J -->|Yes| K[商品詳細ページへリダイレクト]
    I -->|No| L[SearchPageLoadedEvent発行]
    J -->|No| L
    L --> M[SearchPageLoadedHook実行]
    M --> N[メタ情報設定]
    N --> O[テンプレートレンダリング]
    O --> P[終了]
    C --> P
    K --> P
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-32-01 | 検索パラメータ必須 | searchパラメータが空の場合はホームページへリダイレクト | 常時 |
| BR-32-02 | 商品番号一致リダイレクト | 検索結果が1件かつ商品番号と検索キーワードが一致する場合、商品詳細へリダイレクト | 検索結果1件時 |
| BR-32-03 | SEOメタ情報 | 検索結果ページはnoindex,followを設定 | 常時 |
| BR-32-04 | 販売チャネル制限 | 現在の販売チャネルで有効な商品のみ検索対象 | 常時 |
| BR-32-05 | 顧客グループ制限 | 現在の顧客グループで閲覧可能な商品のみ検索対象 | 常時 |

### 計算ロジック

**商品番号一致判定**:
```php
$request->query->get('search') === mb_strtolower($product->getProductNumber())
```
- 検索キーワードと商品番号を小文字変換して比較
- 完全一致の場合のみリダイレクト対象

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 商品検索 | product | SELECT | キーワードにマッチする商品を取得 |
| 商品検索 | product_translation | SELECT | 商品の翻訳情報（名前、説明）を検索 |
| 集計取得 | manufacturer, property, product | SELECT | フィルター用の集計データを取得 |

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

#### product / product_translation

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, product_number, name, description, price, stock, available, manufacturer_id | 全文検索条件 AND active = true AND sales_channel条件 | Elasticsearch使用時はインデックス検索 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| MISSING_REQUEST_PARAMETER | パラメータ不足 | searchパラメータが空の場合 | ホームページへリダイレクト |

### リトライ仕様

特になし（読み取り専用処理のためリトライ不要）

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

読み取り専用処理のため、トランザクション制御なし

## パフォーマンス要件

- レスポンス時間: 1秒以内（通常時）
- Elasticsearch使用時は更に高速（100ms程度）
- ページネーションによる件数制限でクエリ負荷を軽減
- 集計クエリはキャッシュ活用

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

- XSS対策としてテンプレート側で検索キーワードをエスケープ
- SQLインジェクション対策としてDAL（Data Abstraction Layer）を使用
- 検索結果ページはnoindexを設定し、検索エンジンへの悪影響を防止

## 備考

- Elasticsearchが有効な場合は全文検索エンジンを使用
- 無効な場合はMySQLのLIKE検索にフォールバック

---

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

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

### 推奨読解順序

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

まず、検索機能で使用されるデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | SearchPage.php | `src/Storefront/Page/Search/SearchPage.php` | 検索ページの構造（listing, searchTerm） |
| 1-2 | ProductListingResult.php | `src/Core/Content/Product/SalesChannel/Listing/ProductListingResult.php` | リスティング結果の構造 |
| 1-3 | ProductSearchRouteResponse.php | `src/Core/Content/Product/SalesChannel/Search/ProductSearchRouteResponse.php` | 検索ルートのレスポンス構造 |

**読解のコツ**: SearchPageはPageを継承しており、listingフィールドでProductListingResultを保持する。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | SearchController.php | `src/Storefront/Controller/SearchController.php` | searchメソッドがエントリーポイント |

**主要処理フロー**:
1. **43-47行目**: ルート定義（/search）
2. **48行目**: searchメソッドの開始
3. **50-51行目**: SearchPageLoaderの呼び出し
4. **53-57行目**: 商品番号一致時のリダイレクト処理
5. **58-64行目**: RoutingException処理（パラメータ不足）
6. **66行目**: SearchPageLoadedHookの実行
7. **68行目**: テンプレートレンダリング

#### Step 3: ページローダーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | SearchPageLoader.php | `src/Storefront/Page/Search/SearchPageLoader.php` | 検索ページロード処理 |

**主要処理フロー**:
- **37-61行目**: load()メソッド - 検索ページ生成のメイン処理
- **39-40行目**: GenericPageLoaderで共通情報取得
- **43-48行目**: ProductSearchRouteで商品検索
- **50行目**: 検索結果をページに設定
- **52-54行目**: 検索キーワードをページに設定
- **63-69行目**: setMetaInformation() - SEO設定

#### Step 4: 検索ルートを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | AbstractProductSearchRoute.php | `src/Core/Content/Product/SalesChannel/Search/AbstractProductSearchRoute.php` | Store-API検索ルートの契約 |
| 4-2 | ProductSearchRoute.php | `src/Core/Content/Product/SalesChannel/Search/ProductSearchRoute.php` | 実際の検索処理実装 |

#### Step 5: 商品番号一致リダイレクト処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | SearchController.php | `src/Storefront/Controller/SearchController.php` | handleFirstHitメソッド |

**主要処理フロー**:
- **154-170行目**: handleFirstHit() - 商品番号一致時のリダイレクト判定
- **156-158行目**: 検索結果が1件より多い場合はnull返却
- **160-163行目**: 最初の商品を取得、ProductEntity以外はnull
- **165-167行目**: 商品番号と検索キーワードの一致チェック

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

```
SearchController::search()
    │
    ├─ SearchPageLoader::load()
    │      │
    │      ├─ GenericPageLoader::load() [共通ページ情報]
    │      │
    │      ├─ ProductSearchRoute::load() [商品検索]
    │      │      └─ ProductListingResult
    │      │
    │      └─ EventDispatcher::dispatch() [SearchPageLoadedEvent]
    │
    ├─ handleFirstHit() [商品番号一致チェック]
    │      └─ redirectToRoute() [リダイレクト]
    │
    ├─ hook() [SearchPageLoadedHook]
    │
    └─ renderStorefront() [テンプレートレンダリング]
```

### データフロー図

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

search ───────────┐
order ────────────┤
p (page) ─────────┼──▶ SearchPageLoader ───────▶ SearchPage
limit ────────────┤         │                        │
manufacturer ─────┤         ▼                        ├─ listing
properties ───────┤    ProductSearchRoute            │   ├─ elements
min-price ────────┤         │                        │   ├─ total
max-price ────────┤         ▼                        │   └─ aggregations
shipping-free ────┤    product テーブル              ├─ searchTerm
rating ───────────┘    (Elasticsearch)               └─ metaInformation
                            │                              │
                            ▼                              ▼
                    ProductListingResult          search/index.html.twig
                                                          │
                                                          ▼
                                                    HTMLレスポンス
                                                    または商品詳細リダイレクト
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| SearchController.php | `src/Storefront/Controller/SearchController.php` | コントローラー | HTTPリクエスト処理 |
| SearchPageLoader.php | `src/Storefront/Page/Search/SearchPageLoader.php` | ページローダー | 検索ページデータ取得 |
| SearchPage.php | `src/Storefront/Page/Search/SearchPage.php` | DTO | 検索ページデータ構造 |
| AbstractProductSearchRoute.php | `src/Core/Content/Product/SalesChannel/Search/AbstractProductSearchRoute.php` | 抽象クラス | Store-API検索ルート契約 |
| ProductSearchRoute.php | `src/Core/Content/Product/SalesChannel/Search/ProductSearchRoute.php` | Store-APIルート | 商品検索処理 |
| ProductListingResult.php | `src/Core/Content/Product/SalesChannel/Listing/ProductListingResult.php` | DTO | リスティング結果 |
| index.html.twig | `src/Storefront/Resources/views/storefront/page/search/index.html.twig` | テンプレート | 検索結果ページビュー |
| SearchPageLoadedHook.php | `src/Storefront/Page/Search/SearchPageLoadedHook.php` | フック | 拡張ポイント |
| SearchPageLoadedEvent.php | `src/Storefront/Page/Search/SearchPageLoadedEvent.php` | イベント | イベント通知 |
