TiDB CloudのAuto Embedding機能を使ってUnityで連想ゲームを作ってみた
ゲームソリューション部の えがわ です。
本ブログはClassmethod SaaSで加速するゲーム開発 Advent Calendar 2025の7日目のブログとなります。
今回はTiDB CloudのVector Search機能を使って、「言葉の意味」で判定する連想クイズゲームを作ってみました。
検証目的のためアプリケーションが直接TiDB Cloudと接続している完全なサーバーレス構成です。
ゲームについて
お題として表示される単語に対して、プレイヤーが連想した単語を入力すると、AIがその「意味の近さ」を判定してスコアをつけてくれるゲームです。
例えば、お題が「りんご」である場合、「みかん」と入力すれば高得点、「宇宙」と入力すれば低得点となります。
単純な文字の一致ではなく、意味(ベクトル)の近さで判定するのがポイントです。
環境
- Unity: 6000.3.0f1
- TiDB Cloud: TiDB Cloud Starter v7.5.6
- MySqlConnector: 2.3.7
注意: Auto Embedding機能はTiDB Cloud Starterクラスターでのみ利用可能です。
完成系

TiDB Cloud Vector Searchとは?
TiDB CloudはPingCAPが提供するフルマネージドのクラウドデータベースサービスであり、Vector Searchが実装されています。
Vector Searchの主な特徴
- ベクトル検索: 高次元ベクトルデータの類似度検索が可能
- 自動エンベディング: テキストを自動的にベクトルに変換する機能
- SQL統合: 通常のSQLクエリと組み合わせて使用可能
自動エンベディング機能とは
通常、テキストをベクトル化するには以下のような手順が必要です
- OpenAIやHugging Faceなどのモデルを用意
- APIキーを取得
- クライアント側でテキストをベクトルに変換
- 変換したベクトルをDBに送信
しかし、TiDB CloudのAuto Embedding機能を使えば、これらがすべて不要になります!
さらに、TiDB Cloudがホストするモデルは現在無料で使用できます(一定の使用制限あり)。
-- こんな風にSQL内で直接テキストをベクトル化して比較できます
SELECT VEC_EMBED_COSINE_DISTANCE(embedding, 'りんご') as distance
FROM word_dictionary
WHERE word = 'みかん';

モデルやAPIキーの管理が不要で、SQLだけで完結するのが非常に便利です。
ゲームの仕様
ルール
- 画面に「お題」となる単語が表示される
- プレイヤーはそのお題から連想される単語を入力
- 入力された単語の意味的な近さ(コサイン類似度)によってスコアが決まる
- 全5問を行い、合計スコアを競う
スコア判定
| ランク | 類似度 | メッセージ |
|---|---|---|
| Perfect | 0.80以上 | 心、通じてる! |
| Great | 0.65〜0.79 | いい線いってる! |
| Good | 0.50〜0.64 | まあまあかな |
| Bad | 0.50未満 | 遠すぎる... |
TiDB Cloud側の準備
1. クラスターの作成
TiDB Cloudのコンソールから、Starterクラスターを作成します。
2. データベースとテーブルの作成
以下のSQLでテーブルを作成します。
今回はCohere embed-multilingual-v3.0モデルを使用します。
- ベクトル次元: 1024次元
- TiDB Cloudがホストしており無料で使用可能
- APIキー不要
- 多言語対応
-- 単語辞書テーブル(自動エンベディング対応)
CREATE TABLE word_dictionary (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
word VARCHAR(191) NOT NULL,
-- EMBED_TEXT()関数で自動的にテキストをベクトルに変換
-- Cohere embed-multilingual-v3.0は1024次元のベクトルを生成
embedding VECTOR(1024) GENERATED ALWAYS AS (
EMBED_TEXT("tidbcloud_free/cohere/embed-multilingual-v3.0", word, '{"input_type": "search_document", "input_type@search": "search_query"}')
) STORED,
UNIQUE KEY uk_word (word),
-- Vector Indexを同時に作成してパフォーマンス向上
VECTOR INDEX ((VEC_COSINE_DISTANCE(embedding)))
);
-- テスト用のサンプルデータ挿入
-- データ挿入時に自動的にベクトルが生成される
INSERT INTO word_dictionary (word) VALUES
('りんご'), ('みかん'), ('バナナ'), ('ぶどう'),
('犬'), ('猫'), ('鳥'), ('魚'),
('車'), ('電車'), ('飛行機'), ('自転車'),
('王様'), ('女王'), ('王子'), ('騎士'),
('学校'), ('先生'), ('生徒'), ('教室'),
('海'), ('山'), ('川'), ('森'),
('赤'), ('青'), ('黄色'), ('緑');
EMBED_TEXT()関数の説明
EMBED_TEXT()関数は、テキストを自動的にベクトルに変換するTiDB Cloudの機能です。
EMBED_TEXT("model_name", text_content[, additional_json_options])
- 第1引数: 使用する埋め込みモデルの名前
- 第2引数: ベクトル化したいテキスト
- 第3引数(オプション): モデル固有の追加設定(JSON形式)
Cohereモデルでは、第3引数でinput_typeを指定します:
"search_document": データベースに保存するドキュメント用"search_query": 検索クエリ用(VEC_EMBED_COSINE_DISTANCEで自動使用)
GENERATED ALWAYS ASと組み合わせることで、データ挿入時に自動的にベクトルが生成・保存されます。
Vector Indexについて
テーブル作成時にVECTOR INDEXを定義することで、大量のデータでも高速な類似検索が可能になります。
- インデックス定義:
VEC_COSINE_DISTANCE()(VEC_EMBED_なし) - クエリ実行:
VEC_EMBED_COSINE_DISTANCE()(VEC_EMBED_あり)
Unity側の実装
プロジェクトは以下の3つのクラスで構成されています。
- TiDBConnector.cs: データベース接続とクエリ管理
- GameManager.cs: ゲーム進行の管理
- UIManager.cs: UI表示の管理
MySqlConnectorのインストール
UnityからMySQLに接続するため、MySqlConnectorをインストールします。
NuGetForUnityを使用する場合
- NuGetForUnityをインストール
- NuGet > Manage NuGet Packages から
MySqlConnectorを検索してインストール
TiDBConnector.cs - データベース接続
TiDB Cloudへの接続と、自動エンベディング機能を使った類似度計算を行います。
using System;
using System.Threading.Tasks;
using UnityEngine;
using MySqlConnector;
namespace VectorMind
{
public class TiDBConnector : MonoBehaviour
{
// 接続情報(本番環境ではバックエンドサーバーを経由してください)
private const string HOST = "your-cluster.region.prod.aws.tidbcloud.com";
private const int PORT = 4000;
private const string USER = "your-username";
private const string PASSWORD = "your-password";
private const string DATABASE = "test";
public static TiDBConnector Instance { get; private set; }
public bool IsConnected { get; private set; } = false;
private MySqlConnection _connection;
private string ConnectionString =>
$"Server={HOST};Port={PORT};User={USER};Password={PASSWORD};Database={DATABASE};SslMode=Required;";
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
// TiDBに接続
public async Task<bool> ConnectAsync()
{
try
{
_connection = new MySqlConnection(ConnectionString);
await _connection.OpenAsync();
IsConnected = true;
Debug.Log("[TiDB] 接続成功!");
return true;
}
catch (Exception ex)
{
Debug.LogError($"[TiDB] 接続エラー: {ex.Message}");
return false;
}
}
// ランダムなお題を取得
public async Task<string> GetRandomWordAsync()
{
try
{
const string sql = "SELECT word FROM word_dictionary ORDER BY RAND() LIMIT 1";
using var cmd = new MySqlCommand(sql, _connection);
var result = await cmd.ExecuteScalarAsync();
return result?.ToString();
}
catch (Exception ex)
{
Debug.LogError($"[TiDB] お題取得エラー: {ex.Message}");
return null;
}
}
// 2つの単語のベクトル類似度を計算(自動エンベディング使用)
public async Task<float?> GetSimilarityScoreAsync(string targetWord, string inputWord)
{
// 同じ単語の場合は0点
if (targetWord.Equals(inputWord.Trim(), StringComparison.OrdinalIgnoreCase))
{
return 0f;
}
try
{
// VEC_EMBED_COSINE_DISTANCEで自動的にテキストをベクトル化して比較
const string sql = @"
SELECT VEC_EMBED_COSINE_DISTANCE(embedding, @input) as distance
FROM word_dictionary
WHERE word = @target";
using var cmd = new MySqlCommand(sql, _connection);
cmd.Parameters.AddWithValue("@target", targetWord);
cmd.Parameters.AddWithValue("@input", inputWord.Trim());
var result = await cmd.ExecuteScalarAsync();
if (result != null && result != DBNull.Value)
{
float distance = Convert.ToSingle(result);
// コサイン距離を類似度に変換(類似度 = 1 - 距離)
float similarity = Mathf.Max(0f, 1f - distance);
return similarity;
}
return null;
}
catch (Exception ex)
{
Debug.LogError($"[TiDB] 類似度計算エラー: {ex.Message}");
return null;
}
}
}
}
ポイントは VEC_EMBED_COSINE_DISTANCE 関数です。この関数に文字列を渡すだけで、TiDB側で自動的にベクトル化して比較してくれます。
GameManager.cs - ゲーム進行管理
ゲームの状態管理とスコア計算を行います。
using System;
using System.Threading.Tasks;
using UnityEngine;
namespace VectorMind
{
public enum GameState
{
Title, Connecting, Question, Answering, Judging, Result, GameOver
}
public enum ResultRank
{
Perfect, // 0.80以上
Great, // 0.65〜0.79
Good, // 0.50〜0.64
Bad // 0.50未満
}
public class GameManager : MonoBehaviour
{
private const int TOTAL_QUESTIONS = 5;
private const float PERFECT_THRESHOLD = 0.80f;
private const float GREAT_THRESHOLD = 0.65f;
private const float GOOD_THRESHOLD = 0.50f;
public static GameManager Instance { get; private set; }
public GameState CurrentState { get; private set; }
public int CurrentQuestionNumber { get; private set; }
public int TotalScore { get; private set; }
public string CurrentQuestion { get; private set; }
// イベント
public event Action<string> OnQuestionReady;
public event Action<int, ResultRank, string> OnAnswerJudged;
public event Action<int, string> OnGameOver;
// ゲーム開始
public async void StartGame()
{
CurrentQuestionNumber = 0;
TotalScore = 0;
// DB接続
bool connected = await TiDBConnector.Instance.ConnectAsync();
if (!connected) return;
await NextQuestion();
}
// 次の問題
private async Task NextQuestion()
{
CurrentQuestionNumber++;
if (CurrentQuestionNumber > TOTAL_QUESTIONS)
{
EndGame();
return;
}
// ランダムな単語を取得
string word = await TiDBConnector.Instance.GetRandomWordAsync();
CurrentQuestion = word;
OnQuestionReady?.Invoke(word);
}
// 回答を提出
public async void SubmitAnswer(string inputWord)
{
// 類似度を取得(自動エンベディング使用)
float? similarity = await TiDBConnector.Instance
.GetSimilarityScoreAsync(CurrentQuestion, inputWord);
if (similarity == null) return;
// スコア計算
int score = Mathf.RoundToInt(similarity.Value * 100);
ResultRank rank = GetResultRank(similarity.Value);
string message = GetResultMessage(rank);
TotalScore += score;
OnAnswerJudged?.Invoke(score, rank, message);
await Task.Delay(2000);
await NextQuestion();
}
private ResultRank GetResultRank(float similarity)
{
if (similarity >= PERFECT_THRESHOLD) return ResultRank.Perfect;
if (similarity >= GREAT_THRESHOLD) return ResultRank.Great;
if (similarity >= GOOD_THRESHOLD) return ResultRank.Good;
return ResultRank.Bad;
}
private string GetResultMessage(ResultRank rank)
{
return rank switch
{
ResultRank.Perfect => "心、通じてる!",
ResultRank.Great => "いい線いってる!",
ResultRank.Good => "まあまあかな",
ResultRank.Bad => "遠すぎる...",
_ => ""
};
}
private void EndGame()
{
string rankLetter = GetFinalRank(TotalScore);
OnGameOver?.Invoke(TotalScore, rankLetter);
}
private string GetFinalRank(int totalScore)
{
float percentage = totalScore / 500f; // 500点満点
if (percentage >= 0.80f) return "S";
if (percentage >= 0.65f) return "A";
if (percentage >= 0.50f) return "B";
return "C";
}
}
}
UIManager.cs - UI管理
UIの表示切り替えとユーザー入力の処理を行います。
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace VectorMind
{
public class UIManager : MonoBehaviour
{
[Header("パネル")]
[SerializeField] private GameObject titlePanel;
[SerializeField] private GameObject gamePanel;
[SerializeField] private GameObject resultPanel;
[Header("ゲーム画面")]
[SerializeField] private TextMeshProUGUI questionNumberText;
[SerializeField] private TextMeshProUGUI totalScoreText;
[SerializeField] private TextMeshProUGUI questionWordText;
[SerializeField] private TMP_InputField answerInputField;
[SerializeField] private Button submitButton;
[SerializeField] private TextMeshProUGUI feedbackText;
[Header("リザルト画面")]
[SerializeField] private TextMeshProUGUI finalScoreText;
[SerializeField] private TextMeshProUGUI finalRankText;
private void Start()
{
// イベント登録
GameManager.Instance.OnQuestionReady += HandleQuestionReady;
GameManager.Instance.OnAnswerJudged += HandleAnswerJudged;
GameManager.Instance.OnGameOver += HandleGameOver;
submitButton.onClick.AddListener(() =>
GameManager.Instance.SubmitAnswer(answerInputField.text));
}
private void HandleQuestionReady(string word)
{
questionWordText.text = word;
questionNumberText.text = $"{GameManager.Instance.CurrentQuestionNumber} / 5";
answerInputField.text = "";
answerInputField.ActivateInputField();
}
private void HandleAnswerJudged(int score, ResultRank rank, string message)
{
feedbackText.text = $"{rank}!\n{message}\n+{score}点";
totalScoreText.text = $"スコア: {GameManager.Instance.TotalScore}";
}
private void HandleGameOver(int totalScore, string rankLetter)
{
gamePanel.SetActive(false);
resultPanel.SetActive(true);
finalScoreText.text = $"{totalScore} 点";
finalRankText.text = rankLetter;
}
}
}
エディターの画面はこんな感じです。

使用可能な埋め込みモデル
今回使用したCohere multilingual-v3.0以外にも、TiDB Cloudは複数の埋め込みモデルに対応しています
TiDB Cloudがホストするモデル(APIキー不要、現在無料)
| モデル | モデル名 | 特徴 |
|---|---|---|
| Cohere multilingual-v3.0 | tidbcloud_free/cohere/embed-multilingual-v3.0 | 今回使用、多言語対応 |
| Cohere english-v3.0 | tidbcloud_free/cohere/embed-english-v3.0 | 英語特化 |
| Amazon Titan Embed Text v2 | tidbcloud_free/amazon/titan-embed-text-v2 | 多言語対応 |
BYOK(Bring Your Own Key)モデル(自分のAPIキーが必要)
- Jina AI: jina-embeddings-v4など
- OpenAI: text-embedding-3-small、text-embedding-3-largeなど
- Gemini: gemini-embedding-001など
- HuggingFace: bge-m3、multilingual-e5-largeなど
- NVIDIA NIM: bge-m3など
プロジェクトのニーズに応じて、適切なモデルを選択できます。詳細はTiDB Cloud公式ドキュメントをご確認ください。
さいごに
今回はTiDB CloudのVector Search機能を使って、AI連想クイズゲームを作成してみました。
Auto Embeddingにより、モデルの管理やAPIキーの取得が不要で、SQLだけで意味検索が実装できるのは非常に便利です。
この機能を使えば:
- 商品レコメンデーション
- ドキュメント検索
- チャットボット
- セマンティック検索
など、様々なAIアプリケーションを簡単に構築できます。
この記事がどなたかの参考になれば幸いです。







