./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` の表示モード部分):
| フィールド | セクション | 表示方法 |
| ----------------- | ---------- | ------------------------------------------------------------------------ |