以前公開した記事「AIがポケカのデッキを考えてくれるアプリを作ってみた!Geminiを活用した次世代の構築体験」は、ありがたいことに当ブログで最も多くの方に読んでいただいている人気記事になりました。
しかし、前回の記事の最後に私は正直にこう書きました。
「正直なところ、まだまだ私が手動で作ったデッキの方が強そう(笑)」
そうなんです。Geminiにメインポケモンを渡して「60枚のデッキを作って!」と丸投げしても、一見それっぽいリストは出てくるものの、現在の対戦環境(メタゲーム)の定石や細かいカード配分の妙までは再現できず、「ジムバトルや大会でそのまま勝てる強いデッキ」には程遠いのが現実でした。
そこで今回はアプローチを大きく見直し、**「デッキ作りの一番面倒な『カード探し』をAIで超強力にアシストする」**という方向でアプリをバージョンアップしました!
1. ポケカの「カード探し」、難しすぎ問題
ポケカのデッキを自分で組んだり調整したりするとき、こんな風に思ったことはありませんか?
- 「雷タイプで、ベンチのポケモンにエネルギー加速できるやつ誰だっけ…?」
- 「山札からエネルギーを持ってこれるグッズやサポートを一覧で見たい」
- 「相手の手札を流したり、ベンチを呼び出せるカードを別の候補に変えたい」
いざ公式サイトのカード検索を使おうとすると、**「完全一致のキーワード検索」**が基本です。
検索ワード:「エネルギー加速」 ➔ ヒット件数:0件
(※カードのテキストには「自分の山札から基本エネルギーを○枚選び、ベンチポケモンにつける」と書かれているためヒットしない)
テキストの微妙な言い回し(「山札から」「トラッシュから」「手札に加える」「つける」など)を正確に覚えていないと検索に引っかかりません。
この**「頭の中にあるふんわりしたやりたいこと(意図)」と「カードの公式テキスト」のギャップ**こそが、デッキ構築で最も時間がかかるボトルネックでした。
2. 実現した機能:自然言語で探せる「AIセマンティック検索」
今回のバージョンアップでは、ユーザーが日常会話のような曖昧な言葉を入力しても、意図を汲み取ってドンピシャなカードを見つけ出してくれる**セマンティック検索(意味検索)**を実装しました。
実際の検索例
-
「エネルギー持ってくる系のカード」と入力した場合
- 大地の器(山札から基本エネルギーを2枚手札に)
- エネルギー転送(山札から基本エネルギーを1枚手札に)
- キャンプファイヤー、ペパーなどのサーチ系サポート
-
「かみなりタイプでエネルギー加速できるカード」と入力した場合
- ミライドンex(特性「タンデムユニット」で雷たねを展開)
- エレキジェネレーター(山札上5枚から雷エネをベンチへ加速)
- モココ(特性「エレキダイナモ」でトラッシュから雷エネ加速)
- ピカチュウex、テツノカイナex
-
「相手の手札を流して妨害する」と入力した場合
- ナンジャモ(お互いの手札を山札の下に戻してサイド枚数分引く)
- ツツジ(相手の手札を3枚に制限)
- アンフェアスタンプ(ACE SPECの超強力手札干渉)
「エネルギー加速」という単語がカード内に一文字も入っていなくても、「エネルギーを場に供給して育てる効果である」とAIが意味を理解してヒットします。
3. 技術的な仕組み:ベクトルDBを使わない軽量ベクトル演算
セマンティック検索やRAG(検索拡張生成)と聞くと、PineconeやQdrant、Chromaなどの「ベクトルデータベース(Vector DB)」を思い浮かべる方が多いと思います。
しかし、個人開発のWebアプリで外部のベクトルDBを導入すると、インフラコストがかさんだり、運用の管理コストが増えてしまいます。
そこで今回は、**「ベクトルDBを一切使わず、静的ファイル+シンプルなベクトル演算のみ」**で完結する仕組みを採用しました。
flowchart TD
subgraph 事前準備 ["【事前バッチ処理】カードデータのベクトル化"]
A["ポケカカードデータ<br>(名前・タイプ・ワザ・特性テキスト)"] --> B["Gemini Text Embedding API"]
B --> C["768次元のEmbeddingベクトルを生成"]
C --> D["軽量なJSON/バイナリデータとして保存"]
end
subgraph 検索時 ["【ユーザー検索時】ミリ秒単位の高速照合"]
E["ユーザーの入力<br>『かみなりタイプでエネ加速』"] --> F["Gemini Embedding API"]
F --> G["検索クエリのベクトル化"]
G --> H["コサイン類似度の総当たり計算<br>(メモリ上・全カード一瞬で走査)"]
D -. 読み込み .-> H
H --> I["類似度スコア上位のカードを表示!"]
end
なぜベクトルDBが不要なのか?
答えはシンプルで、**「スタンダードレギュレーションのカード枚数は数千枚程度だから」**です。
数十万〜数千万件の大規模データであれば専用のベクトルインデックス(HNSWなど)が必要ですが、数千件程度であれば、768次元の浮動小数点数の配列としてメモリに展開しておき、内積(コサイン類似度)を総当たりでループ計算しても数ミリ秒〜数十ミリ秒で完了します。
実装コード(コアロジック)
Geminiの text-embedding-004 を使ったベクトルの生成と、コサイン類似度の計算ロジックは非常にシンプルです。
// 2つのベクトル間のコサイン類似度を計算する関数
function cosineSimilarity(vecA: number[], vecB: number[]): number {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < vecA.length; i++) {
dotProduct += vecA[i] * vecB[i];
normA += vecA[i] * vecA[i];
normB += vecB[i] * vecB[i];
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
return denominator === 0 ? 0 : dotProduct / denominator;
}
// ユーザーの検索クエリに最も近いカードを抽出
export async function searchCards(queryText: string, cardEmbeddings: CardData[], limit = 10) {
// 1. ユーザー入力をGemini Embedding APIでベクトル化
const queryEmbedding = await getGeminiEmbedding(queryText);
// 2. 全カードとのコサイン類似度を算出
const scoredCards = cardEmbeddings.map(card => ({
...card,
score: cosineSimilarity(queryEmbedding, card.embedding)
}));
// 3. スコア降順に並び替えて上位を返す
return scoredCards
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
事前に全カードのテキストを結合(カード名 + タイプ + HP + 特性 + ワザテキスト)してベクトル化しておけば、実行時はユーザーの入力クエリを1回Embeddingするだけで瞬時に類似カードが算出されます。
4. 実際に使ってわかったメリット
① 思考を止めずにカード候補が探せる
デッキを作っているとき、「ここに手札補充できるグッズを入れたいな」「サブアタッカーとして1エネで動けるやついないかな」と思い浮かんだ言葉をそのまま打ち込むだけで、知らなかった隠れた優良カードに出会えます。
② 「完全おまかせ」よりも人間とAIの役割分担が綺麗
- AIの役割: 膨大なカードプールから、人間の意図に合致するカードを漏れなくリストアップする
- 人間の役割: その中から環境や好みに合わせて採用枚数を決め、60枚の完成度を高める
この役割分担にしたことで、前回のような「AIが組んだ支離滅裂なデッキ」にならず、実用的で勝率の高いデッキが短時間で作れるようになりました。
5. まとめと今後の展望
第1弾で「全部をAIにやらせようとして失敗した」経験から、今回は「人間が一番困っているカード探しの解像度をAIで上げる」という方向にシフトしました。
結果として、インフラ費用をかけずに個人開発の範囲で実用的なツールに進化させることができました。
今後は、このセマンティック検索をベースにしながら:
- 選択したメインカードに対して「相乗効果(シナジー)が高いカード」を自動リコメンドする機能
- デッキ内の配分バランス(たねポケモン、ボール系、サポートの比率)をチェックするアドバイザー機能
などを順次追加していく予定です!
ポケカプレイヤーの皆さんも、個人開発でAIやEmbeddingを活用したいエンジニアの皆さんも、ぜひ参考にしてみてください!