Skip to content

SQL データベースからレコメンド

インタラクションデータがすでにリレーショナルデータベースにあるなら、レコメンド構築のためにエクスポート用パイプラインを用意する必要はありません。Recotem の sql ソースは SQLAlchemy 2 を通じて PostgreSQL・MySQL/MariaDB・SQLite に直接接続し、あなたが指定した SELECT を実行してモデルを学習し、/v1/recipes/{name}:recommend の HTTP エンドポイントとして配信します。

このガイドでは SQL レコメンド のセットアップを一通り解説します。DSN でデータベースを指し示し、orders クエリをユーザー×アイテムのインタラクションに整形し、レシピを書き、ライブなレコメンドを配信するまでです。同じレシピは接続文字列を変えるだけで MySQL や SQLite でも動きます。

必要なもの

  • インタラクションを記録したテーブルを持つデータベース(PostgreSQL・MySQL/MariaDB・SQLite)。たとえば「どのユーザーがどのアイテムに触れたか」を記録する orderspurchasesevents などのテーブルです。
  • 対象テーブルに対して SELECT 権限を持つ読み取り専用のデータベースユーザー。
  • 対応するドライバ extra を指定してインストールした Recotem:
bash
pip install "recotem[postgres]"   # PostgreSQL(psycopg 経由)
pip install "recotem[mysql]"      # MySQL / MariaDB(PyMySQL 経由)
pip install "recotem[sqlite]"     # SQLite(標準ライブラリ — 追加ドライバ不要)

これらの extra が無いと、recotem train はどの extra を入れるべきかを示す DataSourceError で終了します。

Recotem をデータベースに向ける

接続文字列(DSN)はレシピには決して書き込みません。レシピは環境変数の名前を指定するだけで、Recotem は学習時にその変数から DSN を読み取ります。変数名は ^RECOTEM_RECIPE_[A-Z0-9_]+$ に一致する必要があり、他のプレフィックスはレシピ読み込み時に拒否されます。

bash
export RECOTEM_RECIPE_DB_DSN="postgresql+psycopg://reco_ro:pass@db.internal:5432/shop?sslmode=require"

ダイアレクトに合わせて DSN の形式を選びます:

ダイアレクトDSN
PostgreSQLpostgresql+psycopg://user:pass@host:5432/db?sslmode=require
MySQL / MariaDBmysql+pymysql://user:pass@host:3306/db?ssl=true
SQLite(ファイル)sqlite:///absolute/path/to/file.db
SQLite(読み取り専用)sqlite:///file:absolute/path/to/file.db?mode=ro&uri=true

本番では TLS を使う

PostgreSQL では sslmode=require(さらに厳格には verify-caverify-full)を、MySQL/MariaDB では ssl=true を設定してください。Recotem は TLS を強制しませんが、DSN が平文に見える場合は sql_dsn_tls_not_configured の警告を出します。

設計として読み取り専用

データベースユーザーには SELECT のみの権限を付与してください。これが本来の信頼境界です。多層防御として、Recotem はクエリ実行前にセッションレベルの読み取り専用コマンドも発行します(PostgreSQL では SET TRANSACTION READ ONLY、MySQL/MariaDB では SET SESSION TRANSACTION READ ONLY、SQLite では PRAGMA query_only = ON)。このコマンドが失敗した場合、処理を続行せず DataSourceError で学習を中断します。決して黙ってスキップされることはありません。

プライベートホスト / SSRF ガード

デフォルトでは、プライベート・ループバック・リンクローカルな IP に解決される DSN ホストは拒否されます。ガードはドライバが解釈するあらゆる経路指定形式(netloc、?host=?hostaddr=?service=?unix_socket=、およびホスト情報を一切持たないネットワーク DSN)を検査します。これにより、悪意あるレシピが内部サービスやクラウドのメタデータエンドポイントへ横移動するのを防ぎます。

データベースが Docker Compose や Kubernetes のサービス名、あるいは Unix ソケット経由で到達する場合は、明示的にオプトインします:

bash
export RECOTEM_SQL_ALLOW_PRIVATE=1

WARNING

RECOTEM_SQL_ALLOW_PRIVATE=1 はプライベート/ループバックの宛先を許可すると同時に、DNS リバインディングの再チェックも無効化します。エンドツーエンドで信頼できるホストにのみ設定してください。ガードの全挙動は SQL ソースリファレンス を参照してください。

インタラクションのクエリを整形する

sql ソースは (ユーザー, アイテム) のインタラクションごとに 1 行を必要とします。最低限、ユーザー列とアイテム列を選択します:

sql
SELECT user_id, item_id
FROM orders
WHERE status = 'paid'

注文のタイムスタンプを加えると、各ユーザーの直近の行動を検証用にホールドアウトする、時系列を考慮した現実的な分割が可能になります。これは「次に買うもの」を推薦するのに最も近いオフラインの近似です。実行ごとに変わる値には SQLAlchemy の名前付きバインドパラメータ(:name)を使ってください。値を自分で SQL 文字列に埋め込んではいけません。query 内の ${...} 展開は SQL インジェクションを封じるために明示的に禁止されているため、バインドパラメータが正しい手段です。

sql
SELECT user_id, item_id, ordered_at
FROM orders
WHERE status = 'paid'
  AND ordered_at >= :since

バインドパラメータ :sincequery_parameters から供給されます。query とは異なり、query_parameters${RECOTEM_RECIPE_*} 展開の対象になります。

レシピを書く

レシピは 1 つの YAML ファイルです。name + source + schema + training + output で構成します。上記の orders クエリに対応した、実行可能な完全なレシピを示します。

yaml
name: order_recs

source:
  type: sql
  dsn_env: RECOTEM_RECIPE_DB_DSN
  query: |
    SELECT user_id, item_id, ordered_at
    FROM orders
    WHERE status = 'paid'
      AND ordered_at >= :since
  query_parameters:
    since: ${RECOTEM_RECIPE_SINCE}
  connect_timeout_seconds: 10
  statement_timeout_seconds: 300

schema:
  user_column: user_id
  item_column: item_id
  time_column: ordered_at

cleansing:
  drop_null_ids: true
  dedup: keep_last
  min_rows: 5000
  min_users: 100
  min_items: 50

training:
  algorithms: [IALS, RP3beta, TopPop]
  metric: ndcg
  cutoff: 20
  n_trials: 40
  timeout_seconds: 1800
  split:
    scheme: time_user
    heldout_ratio: 0.1
    seed: 42

output:
  path: ./artifacts/order_recs.recotem
  versioning: append_sha

上記の選択について補足します:

  • schema.time_column: ordered_at は、分割が scheme: time_user を使うため必須です。ordered_at は本物のタイムスタンプ列なので time_unit は不要です(時刻列が数値の Unix タイムスタンプの場合は time_unit: s を追加します)。
  • algorithms には暗黙的フィードバック向けの学習器を 3 つ挙げています。Recotem は Optuna 探索でそれらを横断し、ndcg@20 で最良のものを残します。TopPop は安価な人気度ベースラインで、「単に売れ筋を出すだけ」に勝てないモデルを検知するための歯止めになります。他に CosineKNNDenseSLIMTruncatedSVDBPRFM も選べます。
  • cleansing.min_rows / min_users / min_items はデータの事前条件です。クエリの返す行数が少なすぎる場合、弱いモデルを作る前に学習を早期失敗させます。

各フィールドは レシピリファレンス に、SQL 固有の詳細(タイムアウト、行数上限、エラーコード)は SQL ソースリファレンス にすべて記載されています。

学習して配信する

まず、アーティファクトを改ざん検知可能にする署名鍵と、サーバー用の API キーを生成します:

bash
recotem keygen --type signing
recotem keygen --type api

export RECOTEM_SIGNING_KEYS="dev:<hex64>"
export RECOTEM_API_KEYS="key1:sha256:<hex64>"

長い実行の前に接続を検証します。recotem validate は DSN に対して SELECT 1 を発行し、ドライバ・認証情報・ホストがすべて動作することを確認します:

bash
export RECOTEM_RECIPE_DB_DSN="postgresql+psycopg://reco_ro:pass@db.internal:5432/shop?sslmode=require"
export RECOTEM_RECIPE_SINCE="2026-01-01"

recotem validate recipes/order_recs.yaml
recotem train recipes/order_recs.yaml

学習は署名済みアーティファクトを ./artifacts/order_recs.recotem に書き出します。recotem serve をレシピのディレクトリに向けると、各レシピが生成したアーティファクトを読み込みます:

bash
recotem serve --recipes ./recipes/ --port 8080

レコメンドを取得する

生成した API キーを付けて、レシピの :recommend エンドポイントを呼び出します。user_id は学習時に登場したものである必要があります:

bash
curl -s -X POST http://localhost:8080/v1/recipes/order_recs:recommend \
  -H "X-API-Key: <plaintext>" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "u1", "limit": 10}' | jq .
json
{
  "request_id": "a1b2c3d4e5f6",
  "recipe": "order_recs",
  "model_version": "sha256:a3f2...e91d",
  "items": [
    {"item_id": "item-42", "score": 0.91},
    {"item_id": "item-17", "score": 0.84}
  ]
}

アイテムは score の降順で並びます。新規顧客に対する 404 UNKNOWN_USER は想定内なので、アプリ側で人気度ベースのレコメンドにフォールバックして処理してください。リクエスト/レスポンスの完全な仕様、関連アイテムやバッチのverb、エラーコードは Serving API リファレンス にあります。

定期的な再学習をスケジュールする

orders テーブルは増え続けるため、モデルはスケジュールに沿って再学習すべきです。recotem train は明確な終了コード契約を持つ通常のプロセスなので、どのスケジューラでも動きます。シークレットを安全に読み込み、非ゼロの終了コードに反応する cron や systemd タイマーの設定は cron / systemd デプロイ を参照してください。

複数ホストにまたがる場合

Recotem のレシピ単位のロックはホストローカル(flock)です。スケジュールされた学習ジョブが複数のマシンで実行され得る場合は、スケジューラレベルのミューテックス(Kubernetes では CronJob の concurrencyPolicy: Forbid)を追加し、2 つの実行が同じアーティファクトを同時に書かないようにしてください。

次のステップ