3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Snowflake World Tour 2025 のコミュニティイベントで200人規模のインタラクティブクイズアプリを作った話

Last updated at Posted at 2025-09-12

はじめに

 2025年9月12日に開催されたSnowflake World Tour 2025 Tokyo(Day2)のコミュニティイベント(CM-201)において,200人規模の参加者が同時にアクセスできるインタラクティブなクイズアプリケーションの実装を行った.

 本イベントでは,参加者全員がQRコードからスマートフォンでアクセスし,リアルタイムでクイズに参加しながらSnowflakeの最新機能を体験できることを要件とした.今回は鬼滅の刃をモチーフにしたストーリーの中で,Snowflakeの最新機能を実際に使いながら問題を解く,エンターテインメント性と技術体験を融合させた,ユーザーフレンドリなアプリケーションを実装した.

image.png

システム構成と役割

全体アーキテクチャ

 開発したアプリケーションは2つのフェーズで構成されている.

第一章 複雑空城 柱の試練

第二章 複雑空城 最終決戦 クロスワードパズル

第一章 Cortex Analystを活用した自然言語クイズの実装

 第一章で私が担当した「人口統計の鬼 〜データ分析の呼吸〜」(GitHub)では,Cortex Analystを使用した自然言語処理によるデータ分析体験を実装した.

image.png

設計思想

 このクイズの最大の特徴は,参加者がSnowflakeの機能を実際に体験しながら問題を解くという点である.通常,データベースからSQLで処理して都道府県別の人口を出す必要があるが,自然言語で問い合わせるだけで答えを導けるようにした.ただし,問い合わせの仕方を間違えると正解にたどり着けない仕組みにすることで,適度な難易度を保った.

データソースとセマンティックモデル

 TruestarのPODB(PREPPER_OPEN_DATA_BANK__JAPANESE_CITY_DATA.E_PODB.J_CI_FD20)の人口統計情報を使用し,Cortex Analyst用のセマンティックモデルを定義した.

# semantic_model_J_CI_FD20.yaml の構造
name: J_CI_FD20_model
tables:
  - name: J_CI_FD20
    description: "日本の市区町村別人口統計データ"
    base_table:
      database: SNOWFLAKE_LEARNING_DB
      schema: CORTEX_ANALYST_DEMO
      table: J_CI_FD20

Cortex Analystの実装詳細

 参考記事をベースに,REST APIを使用してCortex Analystと通信する実装を行った.

import streamlit as st
import pandas as pd
import requests
import snowflake.connector

# Cortex Analyst設定
DATABASE = "<DATABASE_NAME>"
SCHEMA = "<SCHEMA_NAME>"
STAGE = "<STAGE_NAME>"
FILE = "semantic_model_J_CI_FD20.yaml"

def get_snowflake_connection():
    """Snowflake接続を取得(Streamlit Secrets対応)"""
    if hasattr(st, 'secrets') and 'snowflake' in st.secrets:
        return snowflake.connector.connect(
            user=st.secrets.snowflake.user,
            password=st.secrets.snowflake.password,
            account=st.secrets.snowflake.account,
            warehouse=st.secrets.snowflake.get('warehouse', '<WAREHOUSE_NAME>'),
            role=st.secrets.snowflake.get('role', '<ROLE>'),
            database=DATABASE,
            schema=SCHEMA
        )

def send_cortex_message(prompt: str, connector):
    """Cortex Analystにメッセージを送信"""
    request_body = {
        "messages": [{"role": "user", "content": [{"type": "text", "text": prompt}]}],
        "semantic_model_file": f"@{DATABASE}.{SCHEMA}.{STAGE}/{FILE}",
    }
    
    # ホスト情報とトークンを取得
    host = getattr(connector, 'host', '<ACCOUNT_DOMAIN>')
    token = connector.rest.token if hasattr(connector, 'rest') else None
    
    resp = requests.post(
        url=f"https://{host}/api/v2/cortex/analyst/message",
        json=request_body,
        headers={
            "Authorization": f'Snowflake Token="{token}"',
            "Content-Type": "application/json",
        },
        timeout=30
    )
    
    if resp.status_code < 400:
        return resp.json()
    return None

# Streamlitアプリケーション
st.header(":blue[人口統計の鬼] 〜データ分析の呼吸〜", divider="blue")

# ヒントシステム(最大2回まで)
MAX_HINTS = 2
if f'hint_count' not in st.session_state:
    st.session_state.hint_count = 0

if st.session_state.hint_count < MAX_HINTS:
    hint_question = st.text_input(
        "Cortex Analystの刀への質問",
        placeholder="例: 2020年の人口ランキング15位から25位を表示して"
    )
    
    if st.button("ヒント取得", type="primary"):
        st.session_state.hint_count += 1
        with st.spinner("Cortex Analystが分析中..."):
            response = send_cortex_message(hint_question, connector)
            if response:
                # SQLクエリと結果を表示
                for item in response["message"]["content"]:
                    if item["type"] == "sql":
                        df = pd.read_sql(item["statement"], connector)
                        st.dataframe(df)

難易度調整の工夫

 クイズは「2020年の人口20位の都道府県を当てる」という内容で,正解は「岡山県」である.以下の工夫により適度な難易度を実現した.

  1. 自然言語の罠 - 単純に「20位の都道府県」と聞いても,市区町村単位のデータから正確に集計できない場合がある
  2. ヒント制限 - ヒントは2回までという制限により,戦略的な質問が必要
  3. 学習要素 - 失敗を通じて,適切なデータ分析の問い方を学べる

第二章 クロスワードアプリケーションの実装詳細

 第二章は,第一章から難易度を上げ,Snowflakeに纏わるクロスワード(GitHub)とし,より複雑な問題として設計した.

image.png

ストーリーとの統合

 クロスワードを解くと,Snowflake Summit 2025のキーメッセージである「SIMPLICITY」が浮かび上がり,その言葉により最終鬼を撃退し世界が救われるというストーリーになっている.このキーワードはSnowflake Summit 2025の基調講演で強調されたメッセージである.

image.png

クロスワードクラスの設計

class SnowflakeCrossword:
    def __init__(self):
        # グリッドサイズ
        self.rows = 12
        self.cols = 16
        
        # 特別なセル(赤マス)の位置と番号
        self.special_cells = {
            (1, 9): '4', (2, 11): '7', (6, 9): '2', (7, 13): '9',
            (8, 3): '3', (9, 1): '6', (9, 9): '5', (5, 1): '1',
            (11, 9): '8', (1, 15): '10'
        }
        
        # 答えの定義(実際の問題と答え)
        self.answers = {
            'across': {
                '': {
                    'row': 1, 'col': 9, 'answer': 'PRIVACY',
                    'hint': '個人情報を守りながら統計的な分析を実現するための保護技術',
                    'label': ''
                },
                '': {
                    'row': 2, 'col': 6, 'answer': 'RBAC',
                    'hint': 'ユーザーができることをそのユーザの「ロール(役割)」で管理する仕組み',
                    'label': ''
                },
                # ... 他の横問題
            },
            'down': {
                '': {
                    'row': 0, 'col': 9, 'answer': 'SPCS',
                    'hint': 'Snowflake上でコンテナ化されたジョブ・サービスを直接実行できるプラットフォーム',
                    'label': ''
                },
                # ... 他の縦問題
            }
        }

ヒントシステムの実装

 
8回間違えると解放される「奥義」システムを実装した.

image.png

def initialize_hint_system():
    """ヒントシステムの初期化"""
    if 'hint_uses' not in st.session_state:
        st.session_state.hint_uses = 0
    
    if 'total_mistakes' not in st.session_state:
        st.session_state.total_mistakes = 0
    
    if 'hint_enabled' not in st.session_state:
        st.session_state.hint_enabled = False

def generate_hint_for_answer(self, answer, reveal_ratio=0.3):
    """答えの3割の文字を表示するヒントを生成"""
    answer_length = len(answer)
    reveal_count = max(1, math.ceil(answer_length * reveal_ratio))
    
    # 最初の文字は必ず表示
    reveal_indices = [0]
    
    # 残りの表示位置をランダムに選択
    if reveal_count > 1 and answer_length > 1:
        remaining_indices = list(range(1, answer_length))
        random.shuffle(remaining_indices)
        reveal_indices.extend(remaining_indices[:reveal_count-1])
    
    # マスクされた答えを作成
    masked_answer = []
    for i in range(answer_length):
        if i in reveal_indices:
            masked_answer.append(answer[i])
        else:
            masked_answer.append("_")
    
    return "".join(masked_answer)

# メイン処理での実装
if st.session_state.total_mistakes >= 8 and not st.session_state.hint_enabled:
    st.session_state.hint_enabled = True
    st.success("⚡ **奥義解放!** ヒントの呼吸が使用可能になった!(全体で四度まで)")

 奥義の使用は4回までという制限を設け,無限にヒントが使えないようにした.これにより,参加者は奥義の使用タイミングを慎重に考える必要がある.

超秘奥義コマンドの実装

 時間制限がある中で確実に鬼を撃退できるよう,最終手段として「SIMPLICITY」を入力すると全回答が表示される機能を実装した.

image.png

# 超秘奥義セクション
with st.expander("💀 超秘奥義(管理者用)"):
    secret_input = st.text_input("パスワード", key="secret_key", type="password",
                                placeholder="管理者パスワードを入力")
    if secret_input == "SIMPLICITY":
        if st.button("全回答表示", key="reveal_all_button", type="primary"):
            puzzle.reveal_all_answers()
            st.rerun()

def reveal_all_answers(self):
    """すべての答えを表示"""
    for direction in ['across', 'down']:
        for key in self.answers[direction]:
            self.reveal_answer(direction, key)

 この機能により,参加者全員が答えを確認し,後で振り返りができるようになっている.

UIの工夫

 200人が同時にスマートフォンでアクセスすることを考慮し,モバイル対応のCSSを実装した.

# カスタムCSS - モバイル対応
st.markdown("""
<style>
    /* モバイル用の調整 */
    @media (max-width: 768px) {
        .crossword-cell {
            width: 35px;
            height: 35px;
            font-size: 16px;
        }
        
        .crossword-container {
            min-width: 600px;
        }
        
        /* スクロールヒントを表示 */
        .crossword-wrapper::after {
            content: "← スワイプで表示 →";
            position: absolute;
            bottom: -20px;
            left: 50%;
            transform: translateX(-50%);
            font-size: 12px;
            color: rgba(255,255,255,0.6);
        }
    }
    
    /* 雪の結晶アニメーション */
    @keyframes snowfall {
        0% {
            transform: translateY(-100px) rotate(0deg);
            opacity: 0.8;
        }
        100% {
            transform: translateY(calc(100vh + 100px)) rotate(360deg);
            opacity: 0.2;
        }
    }
</style>
""", unsafe_allow_html=True)

 前面投影用のUIと全ユーザが手元で解けるようにモバイルにも対応したUIの2つを実装し,異なる利用シーンに対応した.

赤マスの文字収集機能

def get_red_cells_content(self):
    """赤セルの内容を番号順に取得"""
    sorted_cells = sorted(self.special_cells.items(), key=lambda x: int(x[1]))
    result = []
    all_filled = True
    
    for (row, col), num in sorted_cells:
        content = st.session_state.grid[row][col]
        if content:
            result.append(content)
        else:
            result.append("_")
            all_filled = False
    
    display_text = "".join(result)
    
    if all_filled:
        return f"🔥 {display_text} 🔥"
    else:
        return display_text

 赤マスに入る文字を順番に並べることで「SIMPLICITY」が浮かび上がる仕組みである.

検討の過程

Cortex AgentとCortex Intelligenceの検討

 当初,奥義の実装にCortex AgentやCortex Intelligenceの活用を検討したが,以下の理由により採用を見送った.

  • Cortex Intelligence - UIが分離してしまい,統一感のある体験を提供できない
  • Cortex Agent - ヒントが単純な文字表示であり,高度なAI機能は不要と判断
  • シンプルさの重視 - 「SIMPLICITY」というテーマに沿い,必要十分な機能に絞る

 結果的に,シンプルな実装で安定性と使いやすさを優先することができた.

開発の裏側

工夫したポイント

  1. SNOWVILLAGEの宣伝 - コミュニティのワードを最初から埋め込み,宣伝効果も狙った
  2. 段階的な難易度設計 - 第一章から第二章へ,徐々に複雑になる問題設計
  3. フェイルセーフの実装 - 時間切れを防ぐための超秘奥義コマンド
  4. 振り返り可能な設計 - 答えを確認できるようにし,後日の学習につなげる

開発スケジュール

 アプリの実装は夜な夜な行い,前日深夜には問題文の修正を実施した.当日も細かな修正を行い,時間ギリギリまで調整を続けた.このような短期集中開発により,イベントに間に合わせることができた.

余談(後日談)

鬼滅の刃にハマるまでの経緯

 本イベントの企画の第一回目のミーティング(7月末)で,ストーリーを作る際に「鬼滅の刃をモチーフにしよう」という話が出た.しかし恥ずかしながら,当時私は鬼滅の刃を一切知らない状況であった(反骨精神から見ないようにしていたという謎のこだわりがあった).

 打ち合わせ中の会話に全く追いつけず,「全集中?柱?呼吸?」と頭の中が疑問符だらけになったこともあり,満を期してアニメを見始めたところ,見事に沼にハマってしまった.アニメ一気見から無限城編の映画鑑賞,その後が気になって漫画も一気読みという典型的なパターンである.

 結果的に鬼滅愛が芽生え,クイズのテイストも原作をオマージュできる形に仕上げることができた.「〜の呼吸」というネーミングも自然に出てくるようになったのは,この経験があったからこそである.

本番15分前のトラブル対応

 イベント開始の15分前,第一章のデータ分析の鬼の問題でエラーが出る事象が発生した.

 本番環境はデモ環境をベースに,24つのテーブルごとに環境を分けて,Snowflake側のユーザも24つに分けていた(Snowflakeは各チームのステータス保持用とデータ分析の鬼の問題を解く上で利用).事前のデモ環境では問題なく動作していたため,急いで問題の切り分けを実施した.

 最終的に,Snowflakeのユーザがデータ分析の鬼の問題で利用するテーブルへのアクセス権がないことが原因だと判明.権限周りのトラブルは日常でもよく発生する事象であるが,本番直前での発生は非常に焦った.皆様も権限設定には十分ご注意いただきたい.

第一章での動作不良への対応

 第一章を解いてもらう際,アプリが動作しなくなる事象がちらほら発生した.おそらくモバイル側(クライアント端末のスペックか帯域)の問題と推測されたが,その場でできることは限られていたため,動作する端末の人を中心に解いてもらうことで対応した.(第一章のアプリの全体設計および実装に関わったkodama氏hiyama氏,感謝です.)

 途中はらはらする場面もあったが,結果的にほぼ定刻通りで無事に各問題12チームクリアを6問分達成できた.参加者の皆様のご協力に感謝申し上げる.

クロスワードの仕様変更による混乱

 第二章のクロスワードで,間違った文字を入力した場合でもクロスワードに反映できるように修正していたが,これが少し混乱を招くことになった.

 間違えた問題数はカウントアップされるものの,文字がそのまま入力できてしまうため,正解と勘違いする事象が発生した.実は,ver1のアプリでは不正解を入れるとクロスワードに反映されない仕様だったが,よりクロスワードらしさを再現するために,間違った文字でも入力できるように変更していた.

 参考までに,ver1はこのような実装であった.最終版と見比べると,UIが大幅に改善されているのがわかる.実際のクロスワードのように「間違えても書き込める」という仕様は,リアルさを追求した結果であったが,イベントで利用するアプリケーションにおけるUXの難しさを改めて実感した.(クイズの内容や透過クロスワードの提案,レビューをいただいたsakatoku氏,感謝です.)

まとめ

 今回の開発を通じて,コミュニティメンバーにもフィードバックをもらいながら,200人規模の参加者が同時にアクセスできるインタラクティブなクイズアプリケーションを実装した.Snowflakeの最新機能を実際に体験しながら学べる仕組みを提供し,技術とエンターテインメントを融合させた新しい形式のイベントアプリケーションとなった.

 ソースコードは全て公開しているため,同様のイベントアプリケーション開発の参考にしていただければ幸いである.

謝辞

 本イベントの企画に関わったスタッフの皆様,Day2の遅い時間にSnowflake Community Meetupを楽しんでいただいた参加者の皆様,Snowflakeスタッフの皆様に心から感謝申し上げます.
 最後になりますが,SnowVillage最高!近々,SnowVillage DataScience&DataEngineering支部の第1回目のイベントも企画していくため,ご参加,ご登録のほどよろしくお願いします.

image.png

image.png

実装アプリケーション

リポジトリ

参考資料

3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?