ブログ API
AI が生成したブログ記事(blog)を管理するエンドポイントです。
ブログには次の2系統があり、どちらも同じ blog コレクションに保存されます。
出自(origin) |
生成元 | 起点となる API |
|---|---|---|
rewrite |
blog_rewriter(過去ブログのリライト) | POST /v1/old_blogs/{old_blog_id}/rewrite・POST /v1/blogs/generation |
generation |
blog_generation(レース回顧・予想の書き下ろし) | POST /v1/blogs/generate |
origin は保存しておらず、old_blog_id の有無から導出します。
ブログのステータス
| ステータス | 説明 |
|---|---|
| generating | 生成中。AI による生成・ビルドが実行中 |
| draft | 下書き。デフォルト |
| published | 公開 |
generating は POST /v1/blogs の入力としては指定できません(400 を返します)。
ブログスキーマ
| フィールド | データ型 | 備考 |
|---|---|---|
| id | string | _id |
| name | string | タイトル。本文 HTML にも見出しとして焼き込まれる |
| race_id | string | null | 紐づくレースの _id |
| race_type | enum | null | auto_racing / bicycle_racing / horse_racing |
| blog_type | string | null | シリーズ種別(例: ジョッキーインタビュー) |
| origin | enum | rewrite / generation。old_blog_id の有無から導出 |
| old_blog_id | string | null | リライト元の過去ブログの _id |
| source_url | string | null | リライト元の掲載元 URL |
| category_id | string | null | category._id |
| category | string | null | カテゴリ名。category_id から解決する。削除済み・不正な ID では null |
| body_html | string | HTML形式。blog_builder が構築する |
| status | enum | generating / draft / published |
| created_at | number | UNIXタイムスタンプ(ミリ秒) |
| updated_at | number | UNIXタイムスタンプ(ミリ秒) |
POST /v1/blogs/generate
概要
レースの情報からブログを新規生成します(元記事なし)。
kind によって書くものが変わります。
| kind | 内容 | 入力 | 必要な前提 |
|---|---|---|---|
recap(既定) |
レース回顧 | レース結果・レース動画・コース画像(競馬のみ) | 着順があること(不成立・中止は不可) |
prediction |
レース予想 | 出走表と AI予想印(race.prediction) |
予想データが登録済みであること |
ブログを generating 状態で作成し、SQS(blog-generation キュー)経由で
blog_generation Lambda へジョブを依頼します。
カテゴリは自動で紐づけます(回顧 → 「レース結果・回顧」、予想 → 「レース予想・検証」)。 カテゴリマスタに該当名が無い場合はカテゴリなしで作成します。
リクエストとレスポンス
ヘッダー
リクエストヘッダー
Content-Type: application/json
URI
リクエストボディ
| フィールド | データ型 | 必須 | 備考 |
|---|---|---|---|
| race_id | string | ◯ | レースの _id |
| kind | enum | recap / prediction。デフォルト: recap |
|
| name | string | タイトル。省略時はレース名から「◯◯のレース回顧」「◯◯のレース予想」を自動生成する |
レスポンス
202 Accepted
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| リクエストボディが不正 | 422 | Unprocessable Entity |
| レースが存在しない | 404 | Not Found |
kind=prediction で予想データが未登録 |
422 | Unprocessable Entity |
kind=recap で着順が無い(不成立・中止) |
422 | Unprocessable Entity |
| 内部サーバーエラー | 500 | Internal Server Error |
処理フロー
flowchart TD
Start([POST /v1/blogs/generate]) --> Race[race_id でレースを取得]
Race -->|存在しない| E404[404 Not Found]
Race -->|kind=prediction| PredCheck[prediction の有無を確認]
Race -->|kind=recap| VoidCheck[着順の有無を確認]
PredCheck -->|未登録| E422[422 Unprocessable Entity]
VoidCheck -->|着順なし| E422
PredCheck -->|OK| Category[カテゴリを解決]
VoidCheck -->|OK| Category
Category --> Save[blog を generating で保存]
Save --> SendSQS[SQS blog-generation キューへ送信]
SendSQS --> Success[202 Accepted]
ルーティング順序
/blogs/generate は /blogs/{blog_id} より先に宣言されています。
後に宣言すると generate が blog_id として解釈されてしまうためです。
POST /v1/blogs/generation
概要
過去ブログ(old_blog)を指定してリライトを開始します。
ブログ名・カテゴリを指定できる点が POST /v1/old_blogs/{old_blog_id}/rewrite との違いです。
ブログを generating 状態で作成し、SQS(blog-rewrite キュー)経由で
blog_rewriter Lambda へジョブを依頼します。
レース紐づき・シリーズ種別・掲載元 URL・埋め込みベクトルは old_blog から引き継ぎます。
リクエストとレスポンス
URI
リクエストボディ
| フィールド | データ型 | 必須 | 備考 |
|---|---|---|---|
| old_blog_id | string | ◯ | old_blog._id |
| category_id | string | category._id |
|
| name | string | タイトル。省略時は old_blog.name を使う |
レスポンス
202 Accepted
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| old_blog_id が存在しない | 400 | Bad Request |
| 内部サーバーエラー | 500 | Internal Server Error |
POST /v1/blogs
概要
ブログを手動で新規作成します。
URI
リクエストボディ
{
"name": "string",
"body_html": "string",
"race_id": "string",
"race_type": "horse_racing",
"old_blog_id": "string",
"category_id": "string",
"blog_type": "string",
"status": "draft"
}
| フィールド | データ型 | 必須 | 備考 |
|---|---|---|---|
| name | string | ◯ | |
| body_html | string | ◯ | HTML形式 |
| race_id | string | レースの _id |
|
| race_type | enum | auto_racing / bicycle_racing / horse_racing |
|
| old_blog_id | string | old_blog._id |
|
| category_id | string | category._id |
|
| blog_type | string | シリーズ種別 | |
| status | enum | draft / published。省略時は draft。generating は指定できない |
レスポンス
201 Created
ブログスキーマを返します。
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| リクエストボディが不正 | 422 | Unprocessable Entity |
status に generating を指定した |
400 | Bad Request |
| 内部サーバーエラー | 500 | Internal Server Error |
GET /v1/blogs
概要
ブログの一覧を取得します。
リクエストとレスポンス
URI
クエリパラメータ
| パラメータ | データ型 | 必須 | 備考 |
|---|---|---|---|
| id | string | ブログ ID の完全一致検索。ObjectId として不正な値は該当なしとして扱う | |
| name | string | タイトルの部分一致検索(大文字小文字を区別しない) | |
| race_id | string | レースでフィルタ | |
| race_type | enum | auto_racing / bicycle_racing / horse_racing でフィルタ |
|
| blog_type | string | シリーズ種別でフィルタ | |
| category_id | string | カテゴリでフィルタ | |
| origin | enum | rewrite / generation でフィルタ(old_blog_id の有無に変換される) |
|
| status | enum | generating / draft / published でフィルタ |
|
| page | number | 取得するページ番号。デフォルト: 1 |
|
| size | number | 1ページあたりの件数。デフォルト: 10 |
|
| order | enum | asc / desc(updated_at 基準)。デフォルト: desc |
レスポンス
200 OK
{
"list": [
{
"id": "string",
"name": "string",
"race_id": "string",
"race_type": "horse_racing",
"blog_type": null,
"origin": "rewrite",
"old_blog_id": "string",
"source_url": "string",
"category_id": "string",
"category": "レース結果・回顧",
"body_html": "string",
"status": "draft",
"created_at": 1234567890000,
"updated_at": 1234567890000
}
],
"current_page": 1,
"total_count": 10
}
カテゴリ名(category)は ID ごとに引くと N+1 になるため、一覧では一括で解決しています。
GET /v1/blogs/{blog_id}
概要
指定したブログを取得します。
URI
レスポンス
200 OK
ブログスキーマを返します。
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| ブログ記事が存在しない | 404 | Not Found |
| 内部サーバーエラー | 500 | Internal Server Error |
PUT /v1/blogs/{blog_id}
概要
指定したブログを更新します。
タイトル更新時は本文の見出しも差し替える
タイトル(name)は本文 HTML にも h2 見出しとして焼き込まれています。
name だけを更新すると本文の見出しが古いまま残るため、サーバー側で本文の見出しも
あわせて差し替えます。body_html を同時に更新する場合は、渡された body_html を正とします。
URI
リクエストボディ
{
"name": "string",
"body_html": "string",
"old_blog_id": "string",
"category_id": "string",
"blog_type": "string",
"status": "published"
}
| フィールド | データ型 | 必須 | 備考 |
|---|---|---|---|
| name | string | タイトル | |
| body_html | string | HTML形式 | |
| old_blog_id | string | old_blog._id |
|
| category_id | string | category._id |
|
| blog_type | string | シリーズ種別 | |
| status | enum | generating / draft / published |
指定されなかった(null の)フィールドは更新しません。
レスポンス
200 OK
更新後のブログスキーマを返します。
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| リクエストボディが不正 | 422 | Unprocessable Entity |
| category_id 指定時、カテゴリが存在しない | 400 | Bad Request |
| ブログ記事が存在しない | 404 | Not Found |
| 内部サーバーエラー | 500 | Internal Server Error |
DELETE /v1/blogs/{blog_id}
概要
指定したブログを論理削除します。
URI
レスポンス
204 No Content
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| ブログ記事が存在しない | 404 | Not Found |
| 内部サーバーエラー | 500 | Internal Server Error |
POST /v1/blogs/{blog_id}/thumbnail_generate
概要
ブログのサムネイル画像生成を開始します。
SQS(thumbnail-generation キュー)経由で thumbnail_generation Lambda へジョブを依頼します。
Lambda はレース動画から AI が選定した瞬間のフレームを切り出し、S3 へ保存したうえで、
ブログ本文の「10秒サマリー」直下へ <img class="nb-thumbnail"> として埋め込みます。
レースに紐づかないブログではサムネイルを生成できません(レース動画が無いため)。
URI
レスポンス
202 Accepted(ボディなし)
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| ブログ記事が存在しない | 404 | Not Found |
ブログ記事が生成中(status=generating) |
400 | Bad Request |
| ブログにレースが紐づいていない | 400 | Bad Request |
| 内部サーバーエラー | 500 | Internal Server Error |
POST /v1/blogs/{blog_id}/related_generate
概要
関連情報(類似ブログへのリンク)を生成し、本文 HTML へ挿入します。
ブログの埋め込みベクトルと、ベクトルを持つ他ブログをコサイン類似度で比較し、 距離が近い順に 2件 を記事下部の「関連情報」セクションとして挿入します。
同じ記事とみなされる候補は除外します。
| 除外条件 | 理由 |
|---|---|
同じ old_blog からリライトされたブログ |
ベクトルが同一になるため |
| タイトルが完全一致するブログ | 同一記事の重複登録 |
| 類似度が 0.99 以上のブログ | 内容が実質同一 |
生成中(status=generating)のブログは候補に含めません。
ビルド時に挿入されたレースカードは、再生成後も関連情報の先頭に残します。
本エンドポイントは非同期ではなく、その場で本文を更新して返します。
URI
レスポンス
200 OK
更新後のブログスキーマを返します。
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| ブログ記事が存在しない | 404 | Not Found |
ブログ記事が生成中(status=generating) |
400 | Bad Request |
| ブログにベクトルが設定されていない | 400 | Bad Request |
| 関連ブログの候補が存在しない | 400 | Bad Request |
| 内部サーバーエラー | 500 | Internal Server Error |
処理フロー
flowchart TD
Start([POST /v1/blogs/blog_id/related_generate]) --> Fetch[ブログを取得]
Fetch -->|存在しない| E404[404 Not Found]
Fetch -->|generating| E400[400 Bad Request]
Fetch -->|ベクトルなし| E400
Fetch -->|OK| Candidates[ベクトルを持つ他ブログを取得]
Candidates --> Select[同一記事を除外しコサイン類似度で上位2件を選出]
Select -->|候補なし| E400
Select -->|OK| Insert[関連情報セクションを body_html へ挿入]
Insert --> Success[200 OK]