はじめに
JRA-VAN DataLabの競馬データベースは非常に巨大です。64テーブル、1,800以上のカラムがあり、レース情報、馬情報、騎手成績、オッズ、払戻金など、あらゆるデータが格納されています。
しかし、このデータを活用しようとすると、いくつかの壁にぶつかります:
- SQLの文法を覚える必要がある
- テーブル構造やカラム名を把握しなければならない
- 分析ツールの使い方を学ぶ必要がある
そこで今回、MCP(Model Context Protocol) を使って、自然言語で競馬データを分析できるシステムを構築しました。
「東京芝2400mの人気別成績を教えて」
このように聞くだけで、AIが適切なSQLを生成し、結果を返してくれます。
本記事では、このMCPサーバーの設計と、1,800カラムのスキーマ情報をどのようにしてAIに理解させたかを解説します。
リポジトリ:
MCPとは
MCP(Model Context Protocol) は、2024年11月にAnthropic社が発表したオープンプロトコルです。AIモデルが外部ツールやデータソースにアクセスするための標準規格として設計されています。
┌─────────────┐ MCP Protocol ┌─────────────┐
│ Claude │ ◄──────────────────► │ MCP Server │
│ (AI Model) │ JSON-RPC 2.0 │ (Tools) │
└─────────────┘ └─────────────┘
MCPには以下のような特徴があります:
- プロトコルの標準化 - 各ツールが独自のAPI設計をする必要がありません
- 双方向通信 - AIからの呼び出しだけでなく、サーバーからの通知も可能です
- スキーマ情報の提供 - ツールの使い方をAIに伝える仕組みが用意されています
今回はこのMCPを使って、競馬データベースへのアクセス層を構築しました。
アーキテクチャ
全体構成
┌──────────────────┐
│ Claude Desktop │
│ / Claude Code │
└────────┬─────────┘
│ MCP Protocol (stdio)
┌────────▼─────────┐
│ jvlink-mcp │ ← Python + FastMCP
│ server │
└────────┬─────────┘
│ SQL
┌────────▼─────────┐
│ SQLite / │ ← 64テーブル, 1,800+カラム
│ PostgreSQL │
└──────────────────┘
技術スタック
| レイヤー | 技術 |
|---|---|
| AIモデル | Claude 3.5 Sonnet |
| MCPクライアント | Claude Desktop / Claude Code |
| MCPサーバー | Python 3.10+ / FastMCP |
| データベース | SQLite(開発)/ PostgreSQL(本番) |
| データソース | JRA-VAN DataLab(COM API) |
課題:1,800カラムをどうAIに理解させるか
ここが今回のプロジェクトで最も苦労したポイントです。
JRA-VANのデータベースには KakuteiJyuni、SyussoTosu、ChakuKaisuJyo0ChakuKaisu1 といった日本語ローマ字のカラム名が大量にあります。AIにとっては意味不明な文字列です。
単純にテーブル構造だけ渡しても、AIは適切なカラムを選ぶことができません。
解決策:パターンベースの説明生成
全カラムに対して説明を返す関数を実装しました。
def generate_column_description(table_name: str, column_name: str) -> str:
col = column_name
# 基本カラム
if col == "KakuteiJyuni":
return "確定着順(最終確定順位、0=着外)"
if col == "Ninki":
return "人気順位(確定後、1=1番人気)"
if col == "SyussoTosu":
return "出走頭数"
# 配列パターン(着度数など)
# ChakuKaisuJyo0ChakuKaisu1 → 札幌2着回数
jyo_match = re.match(r'ChakuKaisuJyo(\d+)ChakuKaisu(\d+)', col)
if jyo_match:
jyo_idx = int(jyo_match.group(1))
place_idx = int(jyo_match.group(2))
jyo_names = ["札幌", "函館", "福島", "新潟", "東京",
"中山", "中京", "京都", "阪神", "小倉"]
place_names = ["1着", "2着", "3着", "4着", "5着", "着外"]
return f"{jyo_names[jyo_idx]}{place_names[place_idx]}回数"
# ... 1,800パターン以上
正規表現でパターンマッチングし、動的に説明を生成しています。これにより:
- 札幌1着回数、東京3着回数といった組み合わせカラムにも対応できます
- TanOdds0〜TanOdds17(単勝オッズ1番馬〜18番馬)のような配列にも対応できます
- 新しいカラムが追加されても、パターンが合えば自動で説明が生成されます
最終的に1,800カラムの100%をカバーすることができました。
提供ツール
MCPサーバーが公開するツールは以下の通りです:
@mcp.tool()
def execute_sql(query: str) -> str:
"""任意のSELECT文を実行"""
@mcp.tool()
def get_horse_info(horse_name: str) -> dict:
"""馬名から詳細情報を取得"""
@mcp.tool()
def get_jockey_stats(jockey_name: str, year: int = None) -> dict:
"""騎手の成績を取得"""
@mcp.tool()
def get_race_results(year: int, month: int, day: int,
jyo_cd: str, race_num: int) -> dict:
"""レース結果を取得"""
@mcp.tool()
def list_tables() -> list:
"""テーブル一覧を取得"""
@mcp.tool()
def describe_table(table_name: str) -> dict:
"""テーブル構造と各カラムの説明を取得"""
execute_sql で自由度を確保しつつ、get_horse_info などの高レベルAPIで使いやすさも両立させています。
実行例
例1:コース別の人気別成績
入力:
東京競馬場の芝2400mで、人気別の成績を教えて
AIが生成したSQL:
SELECT
se.Ninki as 人気,
COUNT(*) as 出走,
SUM(CASE WHEN se.KakuteiJyuni = 1 THEN 1 ELSE 0 END) as 勝利,
SUM(CASE WHEN se.KakuteiJyuni <= 3 THEN 1 ELSE 0 END) as 複勝
FROM NL_SE se
JOIN NL_RA ra ON se.Year = ra.Year
AND se.MonthDay = ra.MonthDay
AND se.JyoCD = ra.JyoCD
AND se.RaceNum = ra.RaceNum
WHERE ra.JyoCD = '05' -- 東京
AND ra.Kyori = 2400 -- 2400m
AND ra.TrackCD LIKE '1%' -- 芝
AND se.KakuteiJyuni > 0
AND se.Ninki > 0
GROUP BY se.Ninki
ORDER BY se.Ninki
出力:
| 人気 | 出走 | 勝利 | 3着内率 |
|---|---|---|---|
| 1番人気 | 30 | 14 | 76.7% |
| 2番人気 | 30 | 7 | 70.0% |
| 3番人気 | 30 | 2 | 40.0% |
ここで注目してほしいのは、ユーザーは「東京」「芝」「2400m」といった自然な言葉で質問しているだけという点です。競馬場コードが「05」であることも、芝コースのトラックコードが「1x」で始まることも、知る必要がありません。AIがスキーマ情報をもとに適切なSQLを組み立ててくれます。
例2:騎手の勝率ランキング
入力:
2024年以降の騎手別勝率を教えて。騎乗数100回以上で上位10人。
出力:
| 騎手名 | 騎乗数 | 勝利 | 勝率 |
|---|---|---|---|
| 渡邊竜也 | 240 | 86 | 35.8% |
| モレイラ | 122 | 35 | 28.7% |
| ルメール | 536 | 145 | 27.1% |
| 川田将雅 | 525 | 112 | 21.3% |
例3:回収率シミュレーション
入力:
単勝オッズ1.5倍以下の馬の勝率と回収率を計算して
出力:
| 指標 | 値 |
|---|---|
| 対象レース | 87件 |
| 勝率 | 81.6% |
| 単勝回収率 | 89% |
「勝率は高いが回収率はマイナス」という競馬の本質がデータで確認できます。
自然言語インターフェースのメリット
実際に使ってみて感じた、自然言語で検索できることのメリットをまとめます。
1. あいまいな入力でも検索できる
従来のSQLや検索ツールでは、正確なカラム名やコード値を知っている必要がありました。しかし自然言語なら、あいまいな表現でも意図を汲み取ってくれます。
「ダービーのコースの成績」
→ 東京芝2400mと解釈して検索
「ルメールの重馬場の成績」
→ 馬場状態コードを自動で判定
「去年のG1で1番人気が負けたレース」
→ 複数テーブルをJOINして該当レースを抽出
ユーザーが「重馬場」と言えば、それが馬場状態コード「3」であることをAIが理解します。「ダービーのコース」と言えば「東京芝2400m」と解釈します。
2. ツールの使い方を覚える必要がない
BIツールやデータ分析ソフトは、それぞれ独自のUIや操作方法を持っています。新しいツールを導入するたびに学習コストがかかります。
自然言語インターフェースなら、日本語で質問するだけです。「どのボタンを押せばいいか」「どのメニューを開けばいいか」を考える必要がありません。
3. 思いついた疑問をすぐに検証できる
データ分析で重要なのは、仮説を素早く検証することです。「こういう傾向があるのでは?」と思ったら、すぐに聞いてみることができます。
「前走1着馬の次走成績は?」
「外枠の馬は不利?」
「休み明けの馬の成績は?」
SQLを書く時間を省けるので、より多くの仮説を短時間で検証できます。
パフォーマンス
SQLiteでの実行時間の目安は以下の通りです:
| クエリ種別 | 実行時間 |
|---|---|
| 単純SELECT | < 100ms |
| JOINあり集計 | 100-500ms |
| 全テーブルスキャン | 1-3s |
インデックスは主要な検索パターンに対して設定済みです。PostgreSQLを使えばさらに高速化できます。
セットアップ
1. データインポート
pip install git+https://github.com/miyamamoto/jrvltsql.git
python scripts/quickstart.py # 対話形式でセットアップ
2. MCPサーバーインストール
mcpb install miyamamoto/jvlink-mcp-server
3. 環境変数
# SQLiteの場合
DB_TYPE=sqlite
DB_PATH=/path/to/keiba.db
# PostgreSQLの場合
DB_TYPE=postgresql
DB_HOST=localhost
DB_NAME=keiba
DB_USER=user
DB_PASSWORD=password
今後の展望
- 予測モデル連携 - MCPツールとして機械学習モデルを呼び出し
まとめ
MCPを使うことで、64テーブル・1,800カラムの競馬データベースを自然言語で分析できるシステムを構築しました。
ポイントは以下の3つです:
- スキーマ説明の100%カバー - パターンマッチングで1,800カラムすべてに説明を付与しました
- 高レベルAPI + SQL直接実行 - 使いやすさと自由度を両立させています
- SQLite/PostgreSQL両対応 - 開発から本番まで同じコードで動作します
競馬データに限らず、業務システムのデータベースにも同様のアプローチが適用できると思います。
リンク
注意: JRA-VAN DataLabの利用には会員登録(有料)が必要です。
