3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1. はじめに

先日の ClickHouse のもくもく会で、ある参加者が作っていたアプリが面白そうでした。サンプルデータの世界中の携帯基地局を3Dの地球儀(グローブ)の上にプロットして、ぐりぐり回せるというものです。しかも「コードは1行も自分で書かず、全部 Claude Code に作らせた」とのことでした。元ネタは AWS と ClickHouse で実装したこちらの記事にまとめられてます1。素直に面白かったので、自分でも作ってみたいと思いました。

自分の環境では、WSL から Google Cloud も ClickHouse も触れます。そこで「AWS の部分を Google Cloud に置き換えたら、同じものが Claude Code でどこまで作れるか」を試してみました。アプリの基本的な作り ―― cell_towers を H3 で集約して deck.gl のグローブに描く、点群と六角形の 2 モード、画面にスキャン行数を出す、といった設計 ―― は元記事1を踏襲しています。そのうえで、AWS だった部分(Amplify / API Gateway / Lambda)を Google Cloud(Cloud Run)に置き換え、データストアは手元の ClickHouse(以下 CHTEST と表記)をそのまま使いました。コードは Claude Code に生成させています。どこを元記事から借り、どこを自分で変えたかは、各章でそのつど明記します。

先に結論です。

  • GCP の Cloud Run + ClickHouse で、約4,327万件の基地局データを3Dグローブに描画できた。radio(通信方式)と H3 解像度を切り替えられ、六角形に集約した表示と、個々の基地局を点で出す「点群」表示を行き来できる
  • ほぼ Claude Code 任せで完成した。アプリのコードは自分ではほとんど書いていない
  • 速い。H3 で事前集約しておいたので、デフォルトの解像度ではブラウザ操作から表示まで端から端で約 80 ミリ秒(ClickHouse 側のクエリは約 9 ミリ秒)
  • データを眺めると分かることがいくつかあった。たとえば 5G(NR)は世界で 867 件しか入っていない(このデータセット時点)
  • つまずいた点も書く。高解像度でレスポンスが Cloud Run の上限を超えた話、H3 関数の引数順、WSL から GCP を操作するときの注意など

完成した画面はこんな見た目です。個々の基地局が点となって、陸地の形が浮かび上がります(元記事と同じ「点群」表示)。

点群(raw)モードで世界全体を表示。個々の基地局を radio で色分けし、日本〜東南アジア〜インドの陸地が点の集まりで浮かぶ


2. 検証環境

項目 内容
フロントエンド React 19 / Vite 6 / TypeScript / Tailwind CSS 4 / deck.gl 9(GlobeView + H3HexagonLayer)
バックエンド Node.js 20 / Express / @clickhouse/client
実行基盤 Google Cloud Run(asia-northeast1)
データストア ClickHouse 26.2.1.474(CHTEST)、cell_towers 43,276,150 行
実装 Claude Code(コードは全て生成)

cell_towers は ClickHouse 公式のサンプルデータセットで2、世界中の基地局の位置(緯度・経度)と通信方式などが入っています。通信方式(radio 列)の内訳は次のとおりでした。

radio 件数
UMTS(3G) 20,686,487
LTE(4G) 12,101,148
GSM(2G) 9,931,304
CDMA 556,344
NR(5G) 867

3. 構成・実装

3.1. 全体構成

元記事は AWS Amplify(フロント配信)と API Gateway + Lambda(バックエンド)でしたが、今回は Cloud Run 1 つにまとめました。1 つのコンテナで、ビルド済みのフロント(静的ファイル)も /api も同じオリジンで配信します。

なぜ Cloud Run にしたか。コンテナをそのまま動かせて、リクエストが無い間はゼロまでスケールするので普段は課金されません。API Gateway + Lambda の役割を 1 サービスに集約でき、デプロイも gcloud run deploy --source の 1 コマンドで済みます3。フロントを別サービス(Firebase Hosting など)に分ける手もありましたが、今回は構成要素を最小にしたかったので、同じコンテナから配信する形にしました。

データストアは AWS 上の ClickHouse のままにしました。つまり Cloud Run(GCP 東京)から ClickHouse(AWS 東京)へ問い合わせるクロスクラウド構成です。同一クラウドよりネットワーク的には不利ですが、後述のとおりデフォルトの解像度では体感に影響しませんでした。

3.2. H3 で事前集約しておく

4,327万件をそのまま毎回ブラウザに送るのは現実的ではありません。そこで H3 で事前にまとめました。

H3 は Uber が公開している「六角形による地理インデックス」です4。地球の表面を大きさのそろった六角形のセルで敷き詰め、緯度・経度を渡すと、その点がどのセルに入るかを表す ID(64 ビットの整数)が返ります。四角形のグリッドではなく六角形なのは、どのセルも隣り合う 6 つのセルと中心どうしの距離がほぼ等しく、密度や近さを素直に扱えるためです。

セルの細かさは 解像度(resolution, 0〜15) で決まります。数字が大きいほどセルは小さく、1 段上がるごとにおよそ 7 倍細かくなります。ざっくり言うと、解像度 3 で 1 セルが県〜地方くらい、解像度 6 で市区町村くらいの広さのイメージです。

近くにある基地局は同じセル ID になるので、セル ID ごとに件数を数えるだけで地図用の集計ができます

セルごとに件数をあらかじめ数えた集約テーブル cell_towers_h3 を作っておき、ブラウザにはセルの中心座標と件数だけを返します。解像度 2〜6 を用意しました。行数は次のとおりです。

H3 解像度 六角形セル数 元の基地局数
2 5,477 43,276,150
3 23,834 43,276,150
4 100,624 43,276,150
5 389,259 43,276,150
6 1,332,941 43,276,150

集約は ClickHouse の H3 関数で行います5geoToH3 で緯度経度をセル ID に変換し、h3ToString で deck.gl が扱える文字列形式(例: 832f5afffffffff)にして返しています。

3.3. バックエンドのクエリ

/api/towers は解像度と radio をクエリ文字列で受け取り、型付きプレースホルダでバインドして ClickHouse に発行します。型を指定するのでインジェクションの心配がありません。

SELECT h3ToString(h3) AS h3, sum(cnt) AS cnt,
       any(center_lon) AS lon, any(center_lat) AS lat
FROM default.cell_towers_h3
WHERE h3_res = {res:UInt8} AND radio IN {radios:Array(String)}
GROUP BY h3 ORDER BY cnt DESC

3.4. 2 つの表示モード(六角形 / 点群)

表示は 2 モードを用意しました。六角形(集約)モードは、ここまでの cell_towers_h3 を引いて H3 セルごとの件数を出します。広い範囲を俯瞰するとき向きで、軽いのが利点です。もう一つの 点群(raw)モードは、集約せずに個々の基地局を 1 点ずつ描きます。生データ cell_towers を直接読むため重くなりますが、点の散らばりがそのまま見えます。元記事がズームインで生の点に切り替えていたのと同じ考え方で、左上のボタンで切り替えられます。


4. 手順・実行

Claude Code に投げて、おおむね次の順で進みました。

  1. データ準備: CHTEST に cell_towers_h3(解像度 2〜6)を作る。43万件×複数解像度の集約は時間がかかるので、ここだけは別途まとめて実行した
  2. バックエンド: Express で /api/towers/api/health、それに静的 SPA 配信を書く
  3. フロント: React + deck.gl の GlobeView に H3HexagonLayer を載せ、/api/towers を取得して描画。右上に「⚡ N 行スキャン / X ms」を出す
  4. デプロイ: gcloud run deploy --source でコンテナをビルドして Cloud Run へ
  5. 確認: ブラウザで開いて、解像度・radio を切り替えながら見る

左上のパネルで解像度(スライダー)と radio(チェックボックス)を切り替えられます。

左上のコントロールパネル。H3解像度スライダーと radio(LTE/UMTS/GSM/CDMA/NR)のチェックボックス


5. 動かした結果

5.1. 点群(raw)モードで全体を見る

冒頭の画像が、この「点群(raw)」モードで世界全体を出したものです。集約をやめて個々の基地局を点で描き、radio で色分けしています。右上には「⚡ 37,478,400 rows scanned / 1042 ms ・ 250,000 pts」と出ています。

このモードは集約テーブルを使わず、生の cell_towers(約4,327万行)を直接読んで点を間引いて返します。そのため「rows scanned」が数千万件になり、後述の六角形モード(事前集約で 1 桁ミリ秒)より時間がかかります。点が多すぎて重くならないよう、ここでは全体から 25 万点をサンプリングして返しています。元記事がズームインで生の点に切り替えていたのと同じ振る舞いです。

5.2. 六角形(集約)モードと3D表示

広い範囲を軽く俯瞰したいときは「六角形(集約)」モードです。H3 セルごとの件数を六角形にして表示します。

六角形(集約)モード。H3解像度3でアジア〜ヨーロッパの基地局密度を六角形で表示

拡大して傾けると、六角形が件数に応じて高さを持つ柱になります。マウスを当てると件数が出ます。下は東京付近を拡大したところで、ツールチップに「32,230 towers」と出ています。

東京付近を拡大した3D表示。六角形が件数に応じた高さの柱になり、ツールチップに件数が出る

5.3. 解像度を変える

解像度 2 は大きな六角形でざっくり、解像度 5 まで上げると海岸線や都市の形がくっきり出ます。

解像度2。大きな六角形で全体をざっくり表示

解像度5。細かい六角形で海岸線や都市の形がくっきり出る

5.4. 通信方式で切り替える

radio を切り替えると、通信方式ごとの分布の違いが見えます。GSM(2G)と LTE(4G)でも広がり方が違います。

GSM(2G)だけを表示した分布

LTE(4G)だけを表示した分布

そして NR(5G)だけにすると、画面はほぼ空になります。このデータセットには 5G が 867 件しか入っていないためで、世界に数クラスタしか出ません。

NR(5G)だけを表示。世界に数クラスタしか出ない

5.5. 速度

各解像度での所要時間です。elapsedMs は ClickHouse 側のクエリ実行時間、client_total はブラウザ操作から結果が返るまで(クロスクラウドのネットワーク往復を含む)です。

H3 解像度 ClickHouse 側 端から端 返した六角形の数
2 6 ms 0.05 s 1,922
3(デフォルト) 9 ms 0.08 s 9,062
4 19 ms 0.23 s 40,811
5 73 ms 1.09 s 171,604
6 約 150 ms 3.8 s 約 130 万

デフォルトの解像度 3 なら端から端で約 80 ミリ秒で、回しても引っかかりません。解像度を上げると遅くなりますが、その大半は ClickHouse のクエリ時間ではなく、返す六角形の数(JSON)が増えて転送に時間がかかるためです。


6. 考察・つまずいた点

6.1. H3 の事前集約で軽くなった

解像度 3 でブラウザに返す行は 9,062 件です。元の 4,327 万件を毎回集計して全部返すのに比べれば桁違いに小さく、これが対話的に回せる理由です。元記事が「4,327万行を 0.18 秒で集計」と紹介していたのは事前集約なしの全件集計の話で、こちらは集約テーブルを引くので ClickHouse 側は 1 桁ミリ秒で返ります。

6.2. H3 関数の引数順

最初に geoToH3(経度, 緯度, 解像度) の順で渡したら、東京を入れたのに中心が太平洋の上に出ました。このバージョン(26.2)では geoToH3(緯度, 経度, 解像度) の順(緯度が先)で渡すと正しい位置になりました。cell_towers の列は経度・緯度の順なので、呼び出しでは入れ替えて渡しています。h3ToGeo の戻り値も (latitude, longitude) の順でした。

6.3. 高解像度でレスポンスが上限超え

解像度 6 を選ぶと、最初はブラウザで「Unexpected end of JSON input」になりました。解像度 6 は世界で約 130 万の六角形になり、JSON にすると 100 MB を超えます。これが Cloud Run の HTTP レスポンス上限(32 MB)を超えて途中で切れていました。対処として、レスポンスを gzip / br で圧縮し、返す六角形を件数の多い上位 30 万に絞りました。圧縮後は数 MB に収まり、解像度 6 も表示できるようになりました。

解像度 6 で返すのは「全 130 万のうち件数上位 30 万」です。1 本だけ立っているような疎な地域は間引かれます。全世界を最細密で一度に出すのは用途的にも重いので、ここは割り切っています。


7. まとめ

AWS で公開されていた「全部 Claude Code で作る3Dグローブ」を、Google Cloud(Cloud Run)と ClickHouse で再現してみました。データストアはそのままに、フロントとバックエンドを GCP に載せ替える形で、ほぼコードを書かずに動くところまで到達できました。H3 で事前集約しておけば 4,327 万件でも対話的に回せること、高解像度ではレスポンスサイズが先に問題になること、H3 関数の引数順といった細かい引っかかりも分かりました。

きっかけをくれたもくもく会の参加者と、元記事1に感謝します。

なお「そもそも、なぜ ClickHouse だとこれが速いのか」を、同じ 4,327 万件を Postgres に積んで確かめた話は、別記事にまとめる予定です。


8. 参考

  1. 参考にした元記事(@Syoitu さん、AWS + ClickHouse で実装)。本記事はこのアプリ設計を踏襲し、AWS 部分を Google Cloud(Cloud Run)に置き換えて再現したものです。https://qiita.com/Syoitu/items/d463c58d15db90303392 2 3

  2. ClickHouse ドキュメント「Cell Towers」サンプルデータセット。https://clickhouse.com/docs/getting-started/example-datasets/cell-towers

  3. Google Cloud ドキュメント「Deploy from source code」(gcloud run deploy --source)。https://cloud.google.com/run/docs/deploying-source-code

  4. H3(Uber 開発の六角形による地理インデックス)公式サイト。https://h3geo.org/

  5. ClickHouse ドキュメント「H3 Indexes」関数(geoToH3 / h3ToGeo / h3ToString など)。https://clickhouse.com/docs/sql-reference/functions/geo/h3

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?