0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Databricks] Genie のベンチマーク結果を API で取得して永続保存する

0
Last updated at Posted at 2026-07-19

はじめに

Databricks AI/BI Genie(旧 Genie Space)の Benchmark 機能は、テスト質問セットに対する Genie の応答精度を定量評価できます。しかし、UI 上の詳細比較(生成 SQL と正解 SQL の差分)は実行から約1週間で expire し、閲覧できなくなります。

「先週の結果と比較したい」「改善前後の推移を可視化したい」といったニーズには、UI だけでは対応できません。

本記事では、Databricks SDK for Python の eval 系 API を使い、ベンチマーク結果をプログラムで取得し、Unity Catalog テーブルに永続保存する方法を解説します。Free Edition で検証済みのコードを掲載しています。

前提条件

  • Databricks ワークスペース(Free Edition 可)
  • Genie Space でベンチマーク実行済み(少なくとも1回の Run が存在すること)
  • databricks-sdk 0.121.0 以上(eval 系メソッドが含まれるバージョン)

UI の制約:詳細比較が1週間で消える

Benchmark 実行後、UI の Evaluation タブでは各質問の正誤・生成 SQL・正解 SQL を確認できます。しかし以下の制約があります。

  • 実行から約1週間経過すると、個別質問の詳細比較が「Details expired」となり閲覧不可になります
  • Run 一覧(正答率のサマリ)は残りますが、「どの SQL が生成されたか」の詳細は消えます
  • information_schema やシステムテーブルにはベンチマーク結果は格納されていません

つまり、改善履歴を追跡するには、結果を自分で取得して保存する仕組みが必要です。

解決策:eval 系 API による3段階取得

Databricks SDK には Genie のベンチマーク結果を取得する eval 系メソッドが3つ用意されています。

メソッド 取得内容
genie_list_eval_runs 実行一覧(正答率サマリ)
genie_list_eval_results 各質問の正誤ステータス・正解SQL
genie_get_eval_result_details 生成SQLとその実行結果

この3つを順に呼ぶことで、UI で見える情報(+ UI が隠す内部情報)を全て取得できます。

実装

Step 1: SDK のインストール

%pip install -U databricks-sdk
dbutils.library.restartPython()

バージョン 0.121.0 以上が必要です。pip show databricks-sdk で確認してください。

Step 2: クライアント初期化

from databricks.sdk import WorkspaceClient

w = WorkspaceClient()

Databricks ノートブック内では WorkspaceClient() が自動的に環境の認証情報を解決します。引数不要です。

Step 3: Eval Runs の取得

SPACE_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"  # 対象スペースのID

resp = w.genie.genie_list_eval_runs(space_id=SPACE_ID)
for run in resp.eval_runs:
    print(run.as_dict())

レスポンス構造:

{
  "eval_runs": [
    {
      "created_timestamp": 1752826598551,
      "eval_run_id": "...",
      "eval_run_status": "DONE",
      "last_updated_timestamp": 1752826645262,
      "num_correct": 4,
      "num_needs_review": 0,
      "num_questions": 4,
      "run_by_user": 123456789012345
    }
  ]
}

注意点として、戻り値は GenieListEvalRunsResponse オブジェクトであり、直接イテレーションできません。.eval_runs プロパティ経由でリストにアクセスします。list(w.genie.genie_list_eval_runs(...))TypeError になります。

Step 4: Eval Results の取得

import json

run_id = resp.eval_runs[0].eval_run_id
results = w.genie.genie_list_eval_results(
    space_id=SPACE_ID,
    eval_run_id=run_id
)
for r in results.eval_results:
    print(r.question, "", r.status)

レスポンス構造:

{
  "eval_results": [
    {
      "benchmark_answer": "SELECT ... FROM ...",
      "benchmark_question_id": "...",
      "created_by_user": 123456789012345,
      "question": "What are the top selling products?",
      "result_id": "...",
      "space_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "status": "DONE"
    }
  ]
}

benchmark_answer フィールドに正解 SQL(Ground truth SQL)が入っています。これは UI の Benchmark 設定で登録した SQL そのものです。

Step 5: Eval Result Details の取得

detail = w.genie.genie_get_eval_result_details(
    space_id=SPACE_ID,
    eval_run_id=run_id,
    result_id=results.eval_results[0].result_id
)
print(json.dumps(detail.as_dict(), indent=2))

レスポンス構造:

{
  "actual_response": [
    {
      "response": "SELECT p.product_name, SUM(s.quantity) ... FROM ...",
      "response_type": "SQL",
      "sql_execution_result": {
        "manifest": {
          "chunks": [{"chunk_index": 0, "row_count": 10}],
          "schema": {
            "column_count": 2,
            "columns": [
              {"name": "product_name", "type_name": "STRING"},
              {"name": "total_quantity", "type_name": "LONG"}
            ]
          }
        }
      }
    }
  ]
}

actual_response[0].response に Genie が生成した SQL が入っています。これが UI の「Generated SQL」に相当し、expire 後に見られなくなる情報です。

完成版ノートブック

上記の3ステップを統合し、関数化したノートブックです。2つのセルで「API 取得 → Unity Catalog 保存」が完結します。

Cell 1: ベンチマーク結果の取得

from databricks.sdk import WorkspaceClient
from datetime import datetime


def collect_benchmark_results(w, space_id):
    """指定スペースの全 Run × 全質問のベンチマーク結果を取得する"""
    records = []
    runs_resp = w.genie.genie_list_eval_runs(space_id=space_id)
    if not runs_resp.eval_runs:
        return records

    for run in runs_resp.eval_runs:
        run_id = run.eval_run_id
        results_resp = w.genie.genie_list_eval_results(
            space_id=space_id, eval_run_id=run_id
        )
        for result in results_resp.eval_results:
            detail = w.genie.genie_get_eval_result_details(
                space_id=space_id,
                eval_run_id=run_id,
                result_id=result.result_id,
            )
            generated_sql = ""
            if detail.actual_response:
                generated_sql = detail.actual_response[0].response or ""

            records.append({
                "space_id": space_id,
                "run_id": run_id,
                "run_timestamp": datetime.fromtimestamp(
                    run.created_timestamp / 1000
                ).isoformat(),
                "run_status": str(run.eval_run_status),
                "num_correct": run.num_correct,
                "num_questions": run.num_questions,
                "question": result.question,
                "status": str(result.status),
                "ground_truth_sql": result.benchmark_answer,
                "generated_sql": generated_sql,
                "result_id": result.result_id,
            })
    return records


w = WorkspaceClient()
SPACE_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

all_records = collect_benchmark_results(w, SPACE_ID)
print(f"取得完了: {len(all_records)}")

Cell 2: Unity Catalog テーブルへの保存

from pyspark.sql.types import StructType, StructField, StringType, IntegerType
from datetime import datetime as dt

SCHEMA_DEF = StructType([
    StructField("space_id", StringType(), False),
    StructField("run_id", StringType(), False),
    StructField("run_timestamp", StringType(), False),
    StructField("run_status", StringType(), True),
    StructField("num_correct", IntegerType(), True),
    StructField("num_questions", IntegerType(), True),
    StructField("question", StringType(), False),
    StructField("status", StringType(), True),
    StructField("ground_truth_sql", StringType(), True),
    StructField("generated_sql", StringType(), True),
    StructField("result_id", StringType(), False),
    StructField("collected_at", StringType(), False),
])


def save_to_table(spark, records, full_table):
    """初回はテーブル作成、2回目以降は MERGE で重複排除しながら追記する"""
    for rec in records:
        rec["collected_at"] = dt.now().isoformat()

    df = spark.createDataFrame(records, schema=SCHEMA_DEF)

    if not spark.catalog.tableExists(full_table):
        df.write.saveAsTable(full_table)
        print(f"テーブル作成完了: {full_table}")
    else:
        df.createOrReplaceTempView("new_results")
        spark.sql(f"""
            MERGE INTO {full_table} AS target
            USING new_results AS source
            ON target.run_id = source.run_id
               AND target.question = source.question
               AND target.space_id = source.space_id
            WHEN NOT MATCHED THEN INSERT *
        """)
        print(f"MERGE完了: {full_table}")


CATALOG = "dev"  # ワークスペースの既存カタログを指定
SCHEMA = "genie_benchmarks"
TABLE = "eval_results"
FULL_TABLE = f"{CATALOG}.{SCHEMA}.{TABLE}"

spark.sql(f"CREATE SCHEMA IF NOT EXISTS {CATALOG}.{SCHEMA}")
save_to_table(spark, all_records, FULL_TABLE)
display(spark.sql(f"SELECT * FROM {FULL_TABLE}"))

MERGE の ON 条件(run_id + question + space_id)で同一質問の重複挿入を防ぐため、定期ジョブで繰り返し実行しても安全です。

ワークスペース横断版

複数ワークスペースの Genie Space を一括で取得する場合は、WorkspaceClient に接続先を明示指定します。Cell 1 の collect_benchmark_results 関数をそのまま再利用できます。

WORKSPACES = [
    {
        "host": "https://dbc-xxxxxxxxxx-xxxx.cloud.databricks.com",
        "token": dbutils.secrets.get(scope="genie", key="ws1_token"),
        "space_ids": ["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],
    },
    {
        "host": "https://adb-xxxxxxxxxxxx.azuredatabricks.net",
        "token": dbutils.secrets.get(scope="genie", key="ws2_token"),
        "space_ids": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
    },
]

all_records = []
for ws_config in WORKSPACES:
    w = WorkspaceClient(
        host=ws_config["host"],
        token=ws_config["token"],
    )
    for space_id in ws_config["space_ids"]:
        all_records.extend(collect_benchmark_results(w, space_id))

print(f"全ワークスペース合計: {len(all_records)}")

トークンは dbutils.secrets から取得しています。Secret Scope の設定方法は 公式ドキュメント を参照してください。

ジョブ化による定期収集

ノートブックを Databricks ジョブとしてスケジュールすれば、expire 前に自動収集できます。

# ジョブ設定の推奨値
# - スケジュール: 毎日1回(cron: 0 0 9 * * ?)
# - クラスタ: Serverless
# - リトライ: 1回
# - アラート: 失敗時にメール通知

1日1回の実行で十分です。ベンチマーク結果は最大1週間保持されるため、日次収集で取りこぼしは発生しません。

まとめ

課題 解決策
UI の詳細比較が1週間で消える eval API で取得して保存
information_schema に無い SDK の genie.genie_* メソッドで取得
複数ワークスペースを横断したい WorkspaceClient(host, token) で接続先を切替
履歴を蓄積したい Unity Catalog Delta テーブルに MERGE

Genie Space のベンチマーク機能は改善サイクルを回すための重要なツールですが、結果の永続化は自前で実装する必要があります。本記事のコードを定期ジョブ化することで、改善履歴の可視化や回帰検知に活用できます。

関連記事

補足:API の注意点

  • eval 系 API は Beta(2026年7月時点)です。2026年3月19日に Beta として公開されて以降、GA 昇格のアナウンスはありません。メソッド名やレスポンス構造が変更される可能性があります
  • レスポンスオブジェクトは直接イテレーションできません(list() で囲むと TypeError)。必ず .eval_runs / .eval_results プロパティ経由でアクセスしてください
  • genie_get_eval_result_details は1質問ずつしか取得できないため、質問数が多い場合はループが必要です
  • genie_list_eval_runs / genie_list_eval_results には page_size / page_token パラメータがあります。質問数が多い場合はページネーションが必要です
  • genie_create_eval_run(space_id, benchmark_question_ids) でベンチマーク実行自体もプログラムからトリガーできます
  • run.eval_run_statusresult.status は Python の enum オブジェクト(EvaluationStatusType)です。json.dumps でシリアライズする場合は str() で文字列に変換してください
  • Free Edition の Serverless SQL Warehouse でも動作確認済みです

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?