./knowledge-base/docs/改修要件定義書_v1.0.md

# 時代考証データベース 改修要件定義書 v1.0

---

## 📋 目次

1. [改修概要](#1-改修概要)
2. [バックエンド改修](#2-バックエンド改修)
3. [フロントエンド改修](#3-フロントエンド改修)
4. [データモデル変更](#4-データモデル変更)
5. [影響範囲サマリ](#5-影響範囲サマリ)

---

## 1. 改修概要

### 1.1 背景・目的

現行システムは、アップロードされた書類を**1ページ単位**で解析してOpenSearchに保存している。
この仕組みは写真集・年表などには有効だが、ノンフィクション小説・辞書など文章がページをまたぐ資料では、
ページごとに切り取ると文脈が失われ検索精度が低下する問題がある。

本改修では以下を実現する:

1. **すべての書籍に対して統一した処理フローを適用する**(document_typeによる分岐を廃止)
2. **目次情報をEmbeddingに活用**し、各ページが「どの章・節の話か」を含んだベクトルを生成する
3. **前後1ページのオーバーラップ**によりページまたぎの文脈断絶を解消する
4. **UIを改善**する(資料ジャンルのアイコンボタン化・検索フィルタ追加・メタ情報の全表示)

### 1.2 基本方針

- **書籍のジャンル(写真集・小説等)で処理を分岐しない**。すべて同一フローで処理する
- ページの中身(文字が多い/画像が多い)はLLMが自律的に判断して対応する(`page_type`フィールドは継続)
- 既存のページビューUIは変更しない。DynamoDBのPageMetadataテーブルは継続利用する

---

## 2. バックエンド改修

### 2.1 全体フロー変更

#### 現行フロー

```
アップロード
  → ページごとにSQS送信
    → ImageAnalyzer(LLMでOCR・画像解析)
      → EmbeddingGenerator(Bedrockでベクトル化)
        → OpenSearch保存
```

#### 改修後フロー

```
アップロード
  → 【新規】TocExtractor(目次抽出 / 1書籍に1回だけ)
    → ページごとにSQS送信
      → ImageAnalyzer(LLMでOCR・画像解析 + chapter_context付加)
        → 全ページOCR完了を確認
          → EmbeddingGenerator(前後1ページ+chapter_contextを含むEmbedding生成)
            → OpenSearch保存
```

---

### 2.2 新規Lambda: TocExtractor(目次抽出)

#### 概要

- アップロード直後に**1書籍につき1回だけ**実行する
- 書籍先頭〜最大20ページの画像をLLMに一括送信し、目次ページを検出してテキストとして抽出する
- 抽出結果を**プレーンテキスト(plain text)**でS3に保存する

#### トリガー

- upload-handler Lambda から直接同期呼び出し(または専用SQSキューへのメッセージ送信)
- ImageAnalyzerはTocExtractorの完了後にのみSQSから処理を開始する

#### 処理詳細

| 項目        | 内容                                                                                                                                   |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| 入力        | `document_id`、S3上の先頭最大20ページ分の画像パス一覧                                                                                  |
| LLMへの指示 | 「目次ページを見つけて、書かれている内容をすべてそのままテキストで出力してください。目次が見つからない場合は空文字を返してください。」 |
| 出力形式    | プレーンテキスト(構造化しない。本に書いてある目次をそのまま文字起こしした文字列)                                                     |
| 保存先      | `s3://historical-research-prompts/{document_id}/toc.txt`                                                                               |

#### フォールバック

- 目次が検出できない場合:`toc.txt` を空文字で保存する
- TocExtractor自体が失敗した場合:chapter_contextなしで後続処理を継続する(処理を止めない)
- TocExtractorの失敗はDynamoDBの書籍レコードに `toc_extracted: false` として記録する

#### DynamoDB書籍レコード(新規追加)

TocExtractor実行後、書籍単位のレコードをDynamoDBに1件作成(またはPageMetadataとは別のテーブル):

```json
{
  "document_id": "book-xyz",
  "title": "昭和の生活写真集",
  "toc_s3_path": "s3://historical-research-prompts/book-xyz/toc.txt",
  "toc_extracted": true,
  "toc_extraction_failed": false,
  "created_at": "ISO8601"
}
```

---

### 2.3 ImageAnalyzer改修

#### 変更点①:chapter_contextの付加

各ページの解析時に `toc.txt` をS3から取得し、LLMへのプロンプトに追加する。

**追加するプロンプト(既存プロンプトの先頭に付加):**

```
この資料のタイトルは「{title}」(ジャンル: {document_genre})です。

以下は目次です:
---
{toc.txtの内容}
---
(目次が見つからなかった場合は「目次なし」と書かれています)

上記を参考に、以下の指示に従って添付ページを解析してください。
また、解析結果のJSONに "chapter_context" フィールドを追加し、
このページが目次のどの章・節にあたるか「第X章 > X.X 節名」の形式で記入してください。
判断できない場合は空文字を入れてください。
```

**解析結果JSONへの追加フィールド:**

```json
{
  "chapter_context": "第3章 西部戦線の攻防 > 3.2 ベルギー侵攻(1914年)"
}
```

#### 変更点②:OCR補助情報(要約)の扱い

- `ocr_supplemental_info`(要約情報)は引き続き生成・保存する
- ただし将来的な廃止候補として仕様書に記録しておく(本改修では削除しない)

#### 変更点③:page_type判定は継続

`page_type`(`text / image / mixed / table`)によるLLMへの指示の重み付けは現行通り維持する。すべての書籍に対して同じ処理フローを適用するため、`document_type`による分岐は廃止する。

---

### 2.4 EmbeddingGenerator改修

#### 変更点①:全ページOCR完了後にEmbedding処理を開始する

**現行:** 各ページのOCR完了直後にSQSへEmebdding用メッセージを送信する

**改修後:** 同一 `document_id` の全ページのOCRが完了(`status: analyzed`)したことを確認してから、
ドキュメント単位のEmbeddingバッチ処理を開始する

**採用方式:**

- ImageAnalyzer完了時にDynamoDBで `analyzed_count` をアトミックにインクリメントする(`ADD analyzed_count 1` による排他制御)
- `analyzed_count == total_pages` になった時点でEmbedding用SQSに `document_id` 単位のメッセージを1件送信する
- DynamoDB Streams(案B)は使用しない

#### 変更点②:Embeddingの生成テキストにオーバーラップを含める

各ページのEmbedding生成時に以下を結合したテキストをBedrockへ送信する:

```
【文脈情報(chapter_context)】
{chapter_context}

【前ページ(p.N-1)のOCRテキスト / 存在しない場合は省略】
{前ページの extracted_text または image_description}

【当該ページ(p.N)のOCRテキスト / 画像ページは image_description を使用】
{extracted_text または image_description}

【次ページ(p.N+1)のOCRテキスト / 存在しない場合は省略】
{次ページの extracted_text または image_description}
```

**OpenSearchへの保存内容:**

- `text_embedding`:上記結合テキストから生成したベクトル(1024次元)
- `extracted_text`:当該ページのOCRテキストのみ(表示用。前後ページは含めない)
- `chapter_context`:章コンテキスト文字列(新規フィールド)
- `embedding_source` は保存しない(生成専用)

#### 変更点③:DynamoDB上の進捗フィールド追加(analyzed_count管理用)

書籍レコードに以下を追加:

```json
{
  "total_pages": 120,
  "analyzed_count": 120,
  "embedding_status": "pending | in_progress | completed | failed"
}
```

---

### 2.5 SQSキュー構成

| キュー名                                   | 役割                        | 変更                                          |
| ------------------------------------------ | --------------------------- | --------------------------------------------- |
| `historical-research-toc-queue.fifo`       | 目次抽出用(新規)          | **新規追加**                                  |
| `historical-research-analysis-queue.fifo`  | ページ解析用                | 変更なし                                      |
| `historical-research-embedding-queue.fifo` | Embedding用(document単位) | メッセージ形式を変更(page_id → document_id) |

---

## 3. フロントエンド改修

### 3.1 資料ジャンル選択のアイコンボタン化

#### 対象画面

- **アップロード画面** (`UploadPage.tsx`):資料ジャンル選択(現行:`Select`プルダウン)
- **詳細ダイアログ 編集モード** (`SearchDialogs.tsx`):資料ジャンル選択(現行:`Select`プルダウン)

#### ジャンル一覧(改修後:7種類)

| ジャンル名               | 画像パス                         |
| ------------------------ | -------------------------------- |
| 写真集                   | `/img/photobook.png`             |
| ノンフィクション小説     | `/img/shousetu.jpg`              |
| 辞書                     | `/img/jisho.png`                 |
| 年表                     | `/img/makimono.png`              |
| メール・議事録           | `/img/mail.jpg`                  |
| 取材メモ(**新規追加**) | `/img/shuzaimemo.jpeg`           |
| その他(**新規追加**)   | テキストのみ(アイコン画像なし) |

#### UIレイアウト

- **3列 × 2行** のグリッドレイアウト(7番目の「その他」は3列目の3行目に単独で配置)
- 各ボタン:アイコン画像(上)+ ジャンル名テキスト(下)
- 「その他」のみアイコン画像なし(テキストのみのボタン)
- 選択中のジャンルは枠線または背景色でハイライト表示
- 未選択時のハイライトなし

#### コンポーネントの `size` prop

`DocumentGenreSelector` は **アップロード画面** ・ **詳細ダイアログ編集モード** ・ **書籍タイトルブラウズ** の3か所すべてで同一の `size="normal"`(96px)の大きいアイコンを使用する。`compact` バリアントは不要。

| size値                 | アイコン画像サイズ | 用途                                                             |
| ---------------------- | ------------------ | ---------------------------------------------------------------- |
| `normal`(デフォルト) | 96px × 96px        | アップロード画面・詳細ダイアログ編集モード・書籍タイトルブラウズ |

#### 実装方針

```tsx
// 共通コンポーネントとして切り出す
// UploadPage.tsx・SearchDialogs.tsx・BrowseLists.tsx から使用
// ファイル例: src/components/DocumentGenreSelector.tsx

export const DOCUMENT_GENRE_OPTIONS = [
  { value: "写真集", image: "/img/photobook.png" },
  { value: "ノンフィクション小説", image: "/img/shousetu.jpg" },
  { value: "辞書", image: "/img/jisho.png" },
  { value: "年表", image: "/img/makimono.png" },
  { value: "メール・議事録", image: "/img/mail.jpg" },
  { value: "取材メモ", image: "/img/shuzaimemo.jpeg" },
  { value: "その他", image: null }, // アイコンなし
];

interface Props {
  value: string;
  onChange: (value: string) => void;
  // size prop は不要。全箇所で normal(96px)を使用する
}
```

#### 変更ファイル

- `src/components/DocumentGenreSelector.tsx`(**新規作成**)
- `src/pages/UploadPage.tsx`:既存の`Select`をDocumentGenreSelectorに差し替え
- `src/components/SearchDialogs.tsx`:編集モードの`Select`をDocumentGenreSelectorに差し替え
- 定数 `DOCUMENT_GENRE_OPTIONS` の定義を `DocumentGenreSelector.tsx` に集約し、重複定義を削除

---

### 3.2 検索画面:キーワード検索への資料ジャンルフィルタ追加

#### 追加位置

既存の検索フォーム(`SearchPage.tsx`)に、番組名フィルタの近傍に「資料ジャンル」フィルタを追加する。

#### UIデザイン

- 表示形式:**テキストのみのChipボタン**(アイコン不要)
- 7種類のジャンル名Chipを横並びで表示(「すべて」を含めると8個)
- 選択中のChipはハイライト(`color="primary"` / `variant="filled"`)
- 「すべて」選択(デフォルト)で絞り込みなし
- 複数選択は**不可**(単一選択)

#### 実装例

```tsx
// 検索フォーム内に追加
<Grid item xs={12}>
  <Box
    sx={{ display: "flex", flexWrap: "wrap", gap: 0.75, alignItems: "center" }}
  >
    <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
      資料ジャンル:
    </Typography>
    <Chip
      label="すべて"
      onClick={() => setSelectedDocGenre("")}
      color={selectedDocGenre === "" ? "primary" : "default"}
      variant={selectedDocGenre === "" ? "filled" : "outlined"}
    />
    {DOCUMENT_GENRE_OPTIONS.map((g) => (
      <Chip
        key={g.value}
        label={g.value}
        onClick={() =>
          setSelectedDocGenre(selectedDocGenre === g.value ? "" : g.value)
        }
        color={selectedDocGenre === g.value ? "primary" : "default"}
        variant={selectedDocGenre === g.value ? "filled" : "outlined"}
      />
    ))}
  </Box>
</Grid>
```

#### 検索APIへの反映

`handleSearch()` 内で `selectedDocGenre` を `filters.document_genre` として渡す。
SearchAPI Lambda・OpenSearchのフィルタ条件に `document_genre` を追加する(後述 §2.6)。

---

### 3.3 検索画面:書籍タイトルブラウズへの資料ジャンル絞り込み追加

#### 現行の書籍タイトルブラウズ動作

「書籍タイトルで検索」カードをクリック → 書籍一覧が表示される(`BrowseBookTitleList`)

#### 改修後の動作(2段構造)

1. 「書籍タイトルで検索」カードをクリック
   → 書籍一覧が展開される(現行と同じ)
   **かつ、6種類の資料ジャンルアイコンボタンが書籍一覧の上に表示される**

2. ジャンルアイコンをクリック
   → そのジャンルに属する書籍だけに絞り込まれる

3. 再度同じアイコンをクリック、または「すべて」をクリック
   → 絞り込み解除

#### UIレイアウト(書籍タイトルブラウズ内)

```
┌─────────────────────────────────────────────────┐
│ 書籍タイトルを選んでください                         │
│                                                 │
│ [すべて]                                         │
│ [写真集(48px)] [小説(48px)] [辞書(48px)]          │
│ [年表(48px)]   [メール(48px)] [取材メモ(48px)]     │
│ [その他]                                         │
│                                                 │
│ ┌──────┐ ┌──────┐ ┌──────┐                     │
│ │ 書籍A │ │ 書籍B │ │ 書籍C │ ...                 │
└─────────────────────────────────────────────────┘
```

- `DocumentGenreSelector` に `size="compact"` を渡して使用する(別コンポーネントは作らない)
- 「すべて」ボタンはChip形式でアイコンエリアの外に別途配置する

#### 絞り込みのデータ取得方法

**採用方式:フロントエンドでフィルタリング(案A)**

- `fetchAllDocuments` APIが各書籍の `document_genre` を返すようにする
- フロントエンド側でジャンル選択に応じてリストを絞り込む(`useMemo` で導出)
- APIへの追加リクエストが不要なため画面の応答速度が高い

#### 変更ファイル

- `src/components/BrowseLists.tsx`:`BrowseBookTitleList` コンポーネントを改修
- `src/types/index.ts`:`DocumentSummary` 型に `document_genre?: string` フィールドを追加
- バックエンド(SearchAPI Lambda):`fetchAllDocuments` のレスポンスに `document_genre` を含める

---

### 3.4 詳細ダイアログ:全メタ情報の表示

#### 現行の詳細ダイアログで表示されていないフィールド

以下のフィールドを追加表示する(`SearchDialogs.tsx` の表示モード部分):

| フィールド        | セクション | 表示方法                                                                 |
| ----------------- | ---------- | ------------------------------------------------------------------------ |