article_builder Lambda 概要
SQS(article-builder キュー)のメッセージをトリガーに、article_generation Lambda が生成した
「レース結果10秒サマリー」を React コンポーネントで HTML 化し、DocumentDB の article コレクションへ
body_html として保存する Lambda 関数です。保存時に status を generating から draft に遷移させます。
公開状態(published)への遷移は本 Lambda では行いません(担当者の操作により別途公開されます)。
記事生成を「生成(article_generation)」と「HTML 構築・保存(article_builder)」の2段構成に 分離することで、AI 生成処理とレンダリング・DB 書き込みを疎結合にしています。
body_html の位置づけ
body_html はサマリーボックス単体(スタイルを内包した self-contained な HTML フラグメント)です。
レース結果ページ本体(結果表・払戻金・コーナー通過順位など)は jester-user-web が描画し、
その中へこのサマリーを埋め込みます。
racing_type に応じて配色テーマが切り替わります。
| racing_type | テーマ | 配色 |
|---|---|---|
horse_racing |
keiba |
緑 |
bicycle_racing |
keirin |
青 |
auto_racing |
autorace |
赤 |
技術スタック
本 Lambda は React コンポーネントを直接利用するため、バックエンドで TypeScript(Node.js)実装の 2つの Lambda のうちの1つです(もう1つは blog_builder。他の Lambda は Python 3.12)。
| 項目 | 内容 |
|---|---|
| ランタイム | Node.js 22 |
| 言語 | TypeScript |
| レンダリング | react-dom/server の renderToStaticMarkup() でコンポーネントを HTML 文字列に変換 |
| バリデーション | zod(articleBuilderPayloadSchema) |
| DB クライアント | mongodb(公式ドライバ。Beanie は使わない) |
| バンドル | esbuild。デプロイ時に handler.ts を単一バンドル(dist/index.js)へ固める |
レンダリング方式のポイント
- Lambda 実行時にビルドは行わない。バンドルはデプロイ時に完了しており、実行時は
renderToStaticMarkup()を呼ぶだけ - 出力は純粋な HTML のみで JavaScript を含まない(ハイドレーションなし)。そのためコンポーネントは表示専用である必要がある
- CSS はバンドル時にインライン化され、
<style>として HTML に内包される
トリガー
| 送信元 | タイミング |
|---|---|
| article_generation Lambda | 記事データの生成が完了した時 |
SQS イベントペイロード(受信)
{
"article_id": "string",
"race_id": "string",
"racing_type": "horse_racing",
"content": {
"headline": "string",
"summary": ["string", "string", "string"]
},
"accuracy_test_data": {}
}
| フィールド | データ型 | 必須 | 備考 |
|---|---|---|---|
| article_id | string | ◯ | 更新対象の article テーブルの _id(generating 状態) |
| race_id | string | ◯ | レースの _id |
| racing_type | enum | ◯ | auto_racing / bicycle_racing / horse_racing。配色テーマの決定に使う |
| content | object | ◯ | 生成した10秒サマリー(headline / summary) |
| accuracy_test_data | object | 精度検証用データ。旧メッセージ互換のため任意 |
処理フロー
- SQS イベントを受信し、
articleBuilderPayloadSchemaでバリデーションする article_idでarticleを取得する。存在しない場合は例外にせず終了する(破棄済み記事へのメッセージ再投入対策)contentをRaceSummaryへ変換し、renderRaceSummaryHtml()で HTML を生成するbody_html/status=draft/updated_at(およびaccuracy_test_data)を保存する
flowchart TD
ArticleGen([article_generation Lambda]) -->|SQS| Start
Start[SQS トリガー受信] --> Parse[zod でペイロードを検証]
Parse -->|invalid| Error[例外を throw]
Parse -->|valid| Fetch[article_id で article を検索]
Fetch -->|存在しない| Skip[ログを出して正常終了]
Fetch -->|存在する| Render[RaceSummary を renderToStaticMarkup で HTML 化]
Render -->|失敗| Error
Render -->|成功| Save[body_html を保存し status を draft に更新]
Save -->|失敗| Error
Save -->|成功| Success[正常終了]
Error -->|最終試行| Discard[generating の article を物理削除して終了]
DocumentDB への保存内容
article テーブルの該当レコード(article_id)を以下のように更新します。
成果物の保存先は DocumentDB の body_html のみです(S3 等への出力は行いません)。
| フィールド | 更新内容 |
|---|---|
body_html |
コンポーネントから構築した記事 HTML |
status |
generating → draft |
updated_at |
現在時刻(UNIX タイムスタンプ・ミリ秒) |
accuracy_test_data |
ペイロードに含まれる場合のみ更新 |
エラーハンドリング
| 状況 | 挙動 |
|---|---|
| 最終試行以外での失敗 | 例外を投げ直し、SQS の再配信に復旧を委ねる |
| 最終試行での失敗 | status=generating の article を物理削除し、例外を投げ直さずに終了する |
物理削除するのは status=generating のレコードだけなので、
生成済み(draft / published)を誤って消すことはありません。
DB 規約は論理削除ですが、本文が空で generating のまま残るレコードは公開されない不完全な生成物であり、
一覧に残ると再生成の妨げになるため物理削除しています。
DocumentDB 接続の自己復旧
DB が一時的に落ちると topology が閉じ、ウォームコンテナはそのクライアントを掴み続けて以降ずっと失敗します。 接続系エラーを検出した場合は 1度だけクライアントを破棄して張り直し、同じ実行内でやり直します。 これにより SQS の再配信(最大 90 分待ち)を待たずに復旧できます。
使用 AWS サービス・ライブラリ
| サービス / ライブラリ | 用途 |
|---|---|
| Amazon SQS | トリガー受信(article_generation Lambda からの生成結果) |
| Amazon DocumentDB | article の更新(body_html の保存・status の遷移) |
| React / react-dom | RaceSummary コンポーネントの HTML 文字列化(renderToStaticMarkup) |
| zod | SQS ペイロードのバリデーション |
| esbuild | デプロイ時のバンドル |