Skip to content

Serving API

recotem serve は FastAPI アプリケーションを HTTP 上で公開します。全エンドポイントは /v1 名前空間に属します。カスタム動詞は AIP-136 の colon-verb 規約に従います — 例: /v1/recipes/{name}:recommend

認証

GET /v1/health を除く全エンドポイントは、プレーンテキストの API キーを持つ X-API-Key リクエストヘッダを必要とします。

キーは RECOTEM_API_KEYS にカンマ区切りの <kid>:sha256:<hex64> エントリのリストとして設定します。サーバーは送信されたプレーンテキストを、エントリに格納された scrypt 派生ハッシュと照合します (scrypt パラメータ: N=2, r=8, p=1, salt=recotem.api-key.v1)。キーの長さは 32〜256 文字でなければなりません。

有効な API キーを生成するには以下のコマンドを使用します:

bash
recotem keygen --type api

このコマンドは 43 文字の base64url 文字列を生成します。これがプレーンテキストのキーとして使用できます。対応する sha256:<hex64> ダイジェストも出力されるため、RECOTEM_API_KEYS に設定してください。

RECOTEM_API_KEYS が空で、かつ --insecure-no-auth が指定されていない場合:

  • RECOTEM_HOST の設定に関わらず、サーバーはバインドホストを 127.0.0.1 に強制します。
  • 全リクエストはキーなしで受け付けられます (クライアントはログ上 kid=anonymous としてタグ付けされます)。

WARNING

X-API-Key ヘッダの前後の空白はキーの一部として扱われるため、一致しません。送信前にクライアント側でトリムしてください。

共通ヘッダ

ヘッダ方向説明
X-API-Keyリクエスト認証トークン (プレーンテキスト)。GET /v1/health を除く全エンドポイントで必須。
X-Request-IDリクエスト / レスポンスクライアントが指定するリクエスト識別子。^[A-Za-z0-9_-]{1,128}$ に一致する必要があります。一致しない値または省略された値の場合、サーバーは新たに 12 桁の 16 進数識別子を生成します。実際に使用された値はレスポンスにエコーされます。
X-Recotem-Model-Versionレスポンスリクエストを処理したレシピのモデルバージョンハッシュ (sha256:<64-hex>)。全ての推薦レスポンスに付与されます。レスポンスボディの model_version フィールドと同じ値です。
X-Recotem-Items-Degradedレスポンス単一推薦エンドポイントのみ。メタデータの結合がフォールバックになった、またはドロップされたアイテムの総数が設定されます。レスポンスが完全にクリーンな場合は付与されません。バッチエンドポイントでは送信されません。

レシピ名の形式

パスパラメータとして使用するレシピ名は ^[A-Za-z0-9_-]{1,64}$ に一致する必要があります。一致しない名前のパスはルーターによって拒否されます — URL のパース方法によって、レスポンスは 404 Not Found または 422 Unprocessable Entity のどちらかになります。

エンドポイント

推薦

POST /v1/recipes/{name}:recommend

単一ユーザーに対する上位 K 件の推薦を取得します。

認証: 必須 (X-API-Key)。

パスパラメータ: name^[A-Za-z0-9_-]{1,64}$ に一致するレシピ名。

リクエストボディ (extra フィールドは禁止):

フィールド制約デフォルト説明
user_idstring必須、1〜256 文字学習データに存在するユーザー識別子。
limitinteger1〜100010返すアイテムの最大数。
exclude_itemsstring[] | null任意、最大 1000 件null結果から除外するアイテム ID。
json
{
  "user_id": "u1",
  "limit": 10,
  "exclude_items": ["item-99"]
}

レスポンスボディ (200 OK):

json
{
  "request_id": "a1b2c3d4e5f6",
  "recipe": "purchase_log",
  "model_version": "sha256:a3f2...e91d",
  "items": [
    {"item_id": "item-42", "score": 0.91, "title": "Example Item", "category": "books"},
    {"item_id": "item-17", "score": 0.84}
  ]
}

アイテムは score の降順に並んでいます。score フィールドは常に有限数です (NaN および Inf は内部で拒否されます)。各アイテムには常に item_idscore が含まれます。追加フィールドはレシピの item_metadata ブロックで設定されたアイテムメタデータから結合されます。RecommendItem はフィールドの追加を許容するため、メタデータ由来のフィールドが item_idscore とともに表示されます。

ステータスコード:

コード条件エラーコード
200成功
401X-API-Key が欠落MISSING_API_KEY
401キーがどのエントリとも一致しないINVALID_API_KEY
404user_id が学習時に存在しなかったUNKNOWN_USER
422リクエストボディのスキーマバリデーション失敗VALIDATION_ERROR
503レシピがロードされていないRECIPE_UNAVAILABLE

UNKNOWN_USER はサーバーエラーではありません

未知のユーザーに対する 404 は、学習時に存在しなかった新規ユーザーでは想定通りの動作です。アプリケーション層でこれを処理してください — 例えば人気ベースの推薦にフォールバックするなど。

curl の例:

bash
curl -s -X POST http://localhost:8080/v1/recipes/purchase_log:recommend \
  -H "X-API-Key: <plaintext>" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "u1", "limit": 10}' | jq .

POST /v1/recipes/{name}:recommend-related

1 件以上のシードアイテムに関連するアイテムを取得します。

認証: 必須 (X-API-Key)。

リクエストボディ:

フィールド制約デフォルト説明
seed_itemsstring[]必須、1〜100 件シードとして使用するアイテム ID。
limitinteger1〜100010返すアイテムの最大数。
exclude_itemsstring[] | null任意null結果から除外するアイテム ID。
json
{
  "seed_items": ["item-42", "item-17"],
  "limit": 10
}

レスポンスボディ (200 OK): :recommend と同じ形状。

ステータスコード:

コード条件エラーコード
200成功
401認証失敗MISSING_API_KEY / INVALID_API_KEY
404シードアイテムが全てモデルに未知UNKNOWN_SEED_ITEMS
404シードは既知だがランキング後に候補が残らないNO_CANDIDATES
422スキーマバリデーション失敗VALIDATION_ERROR
503レシピがロードされていないRECIPE_UNAVAILABLE

curl の例:

bash
curl -s -X POST http://localhost:8080/v1/recipes/purchase_log:recommend-related \
  -H "X-API-Key: <plaintext>" \
  -H "Content-Type: application/json" \
  -d '{"seed_items": ["item-42"], "limit": 5}' | jq .

POST /v1/recipes/{name}:batch-recommend

単一リクエストで複数ユーザーの推薦を取得します。Algolia スタイルのバッチエンベロープを使用します。

認証: 必須 (X-API-Key)。

リクエストボディ:

フィールド制約デフォルト説明
requestsRecommendRequest[]1〜256 件ユーザーごとの推薦リクエスト。各要素は :recommend ボディと同じ形状。
include_metadatabooleanfalsefalse の場合、バルクパフォーマンスのためメタデータ結合フィールドが items から省略されます。単一ユーザーエンドポイントと同じアイテム形状を得るには true に設定してください。
json
{
  "requests": [
    {"user_id": "u1", "limit": 5},
    {"user_id": "u2", "limit": 5, "exclude_items": ["item-99"]}
  ],
  "include_metadata": false
}

レスポンスボディ (200 OK):

json
{
  "request_id": "a1b2c3d4e5f6",
  "recipe": "purchase_log",
  "model_version": "sha256:a3f2...e91d",
  "results": [
    {
      "index": 0,
      "status": "ok",
      "items": [{"item_id": "item-42", "score": 0.91}]
    },
    {
      "index": 1,
      "status": "error",
      "error": {"code": "UNKNOWN_USER", "message": "user not seen during training"}
    }
  ]
}

resultsindex フィールドによって requests の元の順序を保持します。失敗した要素は status: "error"error オブジェクトを持ちます。同じバッチ内の他の要素は引き続き処理されます。

バッチ固有のルール:

  • requests 配列は 1〜256 件でなければなりません。この範囲外の配列はリクエスト全体に対して 422 を返します。
  • requests[].limit の合計は 5000 を超えてはなりません。合計がこの上限を超える要素は要素単位の VALIDATION_ERROR 結果を受け取ります。以降の要素は引き続き処理されます。
  • スキーマエラーを持つ個別の要素はバッチ全体を失敗させません。その要素は要素単位の VALIDATION_ERROR 結果を受け取り、HTTP レスポンス全体は 200 のままです。
  • X-Recotem-Items-Degraded はバッチレスポンスでは送信されません。
  • 503 が返されるのはレシピ自体が利用不可 (未ロード) の場合のみです。UNKNOWN_USER などの要素単位のエラーは HTTP ステータスコードに影響しません。

curl の例:

bash
curl -s -X POST http://localhost:8080/v1/recipes/purchase_log:batch-recommend \
  -H "X-API-Key: <plaintext>" \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      {"user_id": "u1", "limit": 5},
      {"user_id": "u2", "limit": 5}
    ],
    "include_metadata": false
  }' | jq .

POST /v1/recipes/{name}:batch-recommend-related

単一リクエストで複数シードのアイテム関連推薦を取得します。

認証: 必須 (X-API-Key)。

リクエストボディ: :batch-recommend と同じエンベロープで、各要素は :recommend-related ボディの形状に従います。

json
{
  "requests": [
    {"seed_items": ["item-42"], "limit": 5},
    {"seed_items": ["item-17", "item-8"], "limit": 10}
  ],
  "include_metadata": false
}

レスポンスボディ (200 OK): :batch-recommend と同じエンベロープ。

バッチルール: 上記の :batch-recommend と同一。

curl の例:

bash
curl -s -X POST http://localhost:8080/v1/recipes/purchase_log:batch-recommend-related \
  -H "X-API-Key: <plaintext>" \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      {"seed_items": ["item-42"], "limit": 5}
    ]
  }' | jq .

レシピディスカバリ

GET /v1/recipes

現在ロードされている全レシピを一覧表示します。

認証: 必須 (X-API-Key)。

起動時にアーティファクトまたは YAML のロードに失敗したレシピのスタブエントリは除外されます — それらは GET /v1/health/details に表示されます。

レスポンスボディ (200 OK):

json
{
  "recipes": [
    {
      "name": "purchase_log",
      "model_version": "sha256:a3f2...e91d",
      "loaded_at": "2026-05-21T00:00:00Z",
      "supported_verbs": [
        "recommend",
        "recommend-related",
        "batch-recommend",
        "batch-recommend-related"
      ],
      "kind": "user-item"
    }
  ]
}
フィールド説明
namestringレシピ名 (レシピ YAML ファイルのステム)。
model_versionstringアーティファクトの sha256:<64-hex> ダイジェスト。
loaded_atstring (ISO 8601)アーティファクトがメモリにロードされたタイムスタンプ。
supported_verbsstring[]このレシピがサポートする colon-verb。レシピの kind に依存します。
kind"user-item" | "item-item"モデルがユーザー対アイテムまたはアイテム対アイテムの推薦を生成するかどうか。"item-item" レシピは recommend および batch-recommend をサポートしません。

curl の例:

bash
curl -s http://localhost:8080/v1/recipes \
  -H "X-API-Key: <plaintext>" | jq .

GET /v1/recipes/

単一のロード済みレシピの詳細メタデータを取得します。

認証: 必須 (X-API-Key)。

レスポンスボディ (200 OK):

GET /v1/recipes の全フィールドに加えて:

フィールド説明
config_digeststring | nullレシピ YAML の sha256:<hex>。利用不可の場合は null。
algorithmsstring[]チューニング中に評価された全アルゴリズムクラス。
best_algorithmstring最良として選択されたアルゴリズムクラス。
best_classstring | null最良アルゴリズムの完全修飾クラス名。
best_paramsobject | null最良アルゴリズムのハイパーパラメータ。
best_scorenumber | null最良モデルのバリデーションスコア。NaN および Inf は null に正規化されます。
metric"ndcg" | "map" | "recall" | "hit" | nullチューニング時に使用した評価指標。
cutoffinteger | nullチューニング時のオフライン評価指標の計算に使用したカットオフ K。これはリクエストごとの limit とは無関係であり、学習時にレシピがどのようにスコアリングされたかを表すのみです。
tuningobject | nullチューニングメタデータ (tried_algorithmsn_trialsn_completed)。
data_statsobject | null学習データの統計情報 (n_rowsn_usersn_items)。
recotem_versionstring | nullこのアーティファクトを学習した recotem のバージョン。
irspack_versionstring | null学習時に使用した irspack のバージョン。
recipe_hashstring | null学習時のレシピ設定の 64 文字の小文字 16 進ダイジェスト (sha256: プレフィックスなし。config_digest とは異なる形式)。
trained_atstring (ISO 8601) | null学習が完了したタイムスタンプ。

上記のオプションフィールドは、それらを記録していない旧アーティファクトでは null になります。

ステータスコード:

コード条件エラーコード
200レシピがロード済み
404レシピ名がレジストリに存在しないRECIPE_NOT_FOUND
503レシピは存在するがロードされていないRECIPE_UNAVAILABLE

curl の例:

bash
curl -s http://localhost:8080/v1/recipes/purchase_log \
  -H "X-API-Key: <plaintext>" | jq .

ヘルスとメトリクス

GET /v1/health

全体の liveness および readiness ステータス。Kubernetes の liveness プローブおよび readiness プローブに対応しています。

認証: なし (認証不要)。

レスポンスボディ:

json
{"status": "ok", "total": 3, "loaded": 3}
フィールド説明
status"ok" | "degraded"全ての設定済みレシピがロードされている場合 "ok"。いずれかのレシピが未ロードの場合 "degraded"total == 0 の場合、ステータスは常に "ok"
totalintegerレジストリ内のレシピエントリの総数。
loadedinteger正常にロードされ、配信準備ができているレシピ数。

ステータスコード:

コード条件
200全レシピがロード済み。
5031 件以上のレシピが未ロード。

Kubernetes readiness プローブ

503 レスポンスは Pod を Service エンドポイントから除外します。これは意図的な動作です — 全ての推薦リクエストが 503 を返す Pod にはトラフィックを送るべきではありません。readiness プローブと liveness プローブの両方に GET /v1/health を使用してください。

curl の例:

bash
curl -s http://localhost:8080/v1/health | jq .

GET /v1/health/details

ロードエラーとアーティファクト識別子を含むレシピごとのヘルス詳細。

認証: 必須 (X-API-Key)。

レシピごとの詳細は、公開されるべきでないアーティファクトのキー識別子 (kid) を含むため、認証が必要です。認証不要のプローブ用ステータスには GET /v1/health を使用してください。

レスポンスボディ:

json
{
  "status": "ok",
  "recipes": {
    "purchase_log": {
      "loaded": true,
      "trained_at": "2026-05-21T00:00:00Z",
      "best_class": "IALSRecommender",
      "kid": "prod-2026-q2"
    },
    "product_recs": {
      "loaded": false,
      "error": "signature mismatch"
    }
  }
}

起動時にロードに失敗したレシピのスタブを含め、レジストリ内の全レシピがここに表示されます。オプションフィールド (trained_atbest_classkiderror) は対応する値が設定されている場合のみ存在します。

ステータスコード: GET /v1/health と同じ — いずれかのレシピが loaded: false または error フィールドを持つ場合は 503

curl の例:

bash
curl -s http://localhost:8080/v1/health/details \
  -H "X-API-Key: <plaintext>" | jq .

GET /v1/metrics

Prometheus メトリクスの公開 (オプトイン)。

認証: 必須 (X-API-Key)。

利用可能条件: 以下の両方の条件が満たされた場合のみこのルートが登録されます:

  1. RECOTEM_METRICS_ENABLED が真の値 (1trueyeson) に設定されている。
  2. recotem[metrics] エクストラがインストールされている (pip install "recotem[metrics]")。

このエンドポイントは OpenAPI スキーマから除外されています。

Prometheus スクレイパーの設定

多くの Prometheus ターゲットとは異なり、/v1/metricsX-API-Key を必要とします。スクレイパーにヘッダを送信するよう設定してください:

yaml
# prometheus.yml スクレイプ設定 (Prometheus 2.45+)
scrape_configs:
  - job_name: recotem
    metrics_path: /v1/metrics
    static_configs:
      - targets: ["localhost:8080"]
    http_headers:
      X-API-Key:
        values: ["<plaintext>"]

利用可能なメトリクス:

メトリクスラベル
recotem_v1_requests_totalCounterrecipe, verb, status
recotem_v1_request_latency_secondsHistogramrecipe, verb
recotem_v1_batch_sizeHistogramrecipe, verb
recotem_v1_batch_element_errors_totalCounterrecipe, verb, code
recotem_v1_metadata_degraded_items_totalCounterrecipe, verb, kind
recotem_v1_validation_errors_outside_verb_totalCounter
recotem_model_loadedGaugerecipe
recotem_artifact_load_failures_totalCounterrecipe, reason
recotem_active_recipesGauge
recotem_swap_totalCounterrecipe, result
recotem_artifact_stat_failures_totalCounterrecipe
recotem_watcher_unhandled_errors_totalCounter
recotem_metadata_index_build_errors_totalCounterrecipe
recotem_metadata_serialization_errors_totalCounterrecipe, verb
recotem_recipe_rescan_errors_totalCounterrecipe
recotem_recommender_layout_unexpected_totalCounterrecipe
recotem_watcher_state_divergence_totalCounter
recotem_bigquery_storage_fallback_totalCounterreason
recotem_recipes_dir_scan_failures_totalCountererror_class

verb ラベルは recommendrecommend-relatedbatch-recommendbatch-recommend-related の値を取ります。recotem_v1_requests_totalstatus ラベルは okunknown_userunknown_seed_itemsno_candidatesunavailablerecipe_not_foundvalidation_errorerror の 8 値を取ります。recotem_artifact_load_failures_totalreason ラベルは readparsehmacheader_jsondeserializemetadatayamlunexpecteddir_scantimeout の値を取ります。

curl の例:

bash
curl -s http://localhost:8080/v1/metrics \
  -H "X-API-Key: <plaintext>"

エラーフォーマット

全てのエラーレスポンスは、最低限 detail (人間が読める形式) と code (機械が読める UPPER_SNAKE_CASE 形式) を持つフラットな JSON ボディを使用します。

標準エラーボディ:

json
{"detail": "recipe purchase_log is not loaded", "code": "RECIPE_UNAVAILABLE"}

バリデーションエラーボディ (422 のみ): request_id と構造化された errors 配列を含みます。

json
{
  "request_id": "a1b2c3d4e5f6",
  "detail": "Request validation failed",
  "code": "VALIDATION_ERROR",
  "errors": [
    {"loc": ["body", "limit"], "msg": "ensure this value is less than or equal to 1000", "type": "value_error.number.not_le"}
  ]
}

内部エラーボディ (500 のみ): サーバーログとの照合のために request_id を含みます。

json
{"detail": "internal error", "code": "INTERNAL_ERROR", "request_id": "a1b2c3d4e5f6"}

エラーコード

コードHTTP発生条件
RECIPE_UNAVAILABLE503レシピはレジストリに存在するが、そのアーティファクトがロードされていない。
RECIPE_NOT_FOUND404レシピ名がレジストリに全く存在しない。
UNKNOWN_USER404user_id が学習の idmap に存在しなかった。
UNKNOWN_SEED_ITEMS404seed_items の全アイテムがモデルに未知。
NO_CANDIDATES404シードアイテムは既知だが、ランキングステージを経て候補が残らなかった。
VALIDATION_ERROR422 (HTTP) / 要素単位 (バッチ)リクエストまたは要素ボディのスキーマバリデーション失敗。
MISSING_API_KEY401X-API-Key ヘッダが存在しない。
INVALID_API_KEY401X-API-Key が設定済みのどのキーとも一致しない。
INTERNAL_ERROR500 (HTTP) / 要素単位 (バッチ)リクエスト処理中に未処理の例外が発生した。

ミドルウェア

TrustedHostMiddleware

RECOTEM_ALLOWED_HOSTS (デフォルト: 127.0.0.1,localhost) は Host ヘッダの許可リストを制御します。このリストにない Host ヘッダを持つリクエストは 400 Bad Request を受け取ります。これは GET /v1/health を含む全エンドポイントに適用されます。

Kubernetes では、kubelet プローブはデフォルトで Host: localhost を送信します — localhost が常にデフォルトの許可リストに含まれているのはそのためです。Ingress 経由で公開する場合は、RECOTEM_ALLOWED_HOSTS に Ingress のホスト名を明示的に追加してください。

CORS

RECOTEM_ALLOWED_ORIGINS (デフォルト: 空 = 全て拒否) は CORS 許可リストを設定します。空の場合、全ての CORS プリフライトリクエストが拒否されます。ブラウザベースのクライアントを許可するには、オリジンのカンマ区切りリストを指定してください。

yaml
RECOTEM_ALLOWED_ORIGINS: "https://app.example.com,https://admin.example.com"

OpenAPI ドキュメント

インタラクティブドキュメントは /docs (Swagger UI) および /redoc で利用できます。生のスキーマは /openapi.json で参照できます。

開発環境専用

これら 3 つのエンドポイントは RECOTEM_ENVdevelopmentdev、または test に設定されている場合のみ利用可能です。それ以外の全ての環境では無効化されます。本番環境のデプロイメントではこれらに依存しないでください。