コンテンツにスキップ

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 精度検証用データ。旧メッセージ互換のため任意

処理フロー

  1. SQS イベントを受信し、articleBuilderPayloadSchema でバリデーションする
  2. article_id で article を取得する。存在しない場合は例外にせず終了する(破棄済み記事へのメッセージ再投入対策)
  3. content を RaceSummary へ変換し、renderRaceSummaryHtml() で HTML を生成する
  4. 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 デプロイ時のバンドル