システムデザイン完全ガイド ④|システムの部品箱

システムデザイン

SYSTEM DESIGN GUIDE — PART 4 / 7

  1. システムの部品箱
  2. CHAPTER 20キャッシュと CDN
    1. 20.1キャッシュを置ける場所(全部言えるように)
    2. 20.2キャッシュ戦略
    3. 20.3キャッシュの 3 大障害(面接必修)
      1. ① キャッシュスタンピード / サンダリングハード
      2. ② キャッシュペネトレーション(貫通)
      3. ③ キャッシュアバランチ(雪崩)
    4. 20.4追い出しポリシー
    5. 20.5Redis と Memcached
    6. 20.6CDN
  3. CHAPTER 21メッセージキューとストリーム処理基盤
    1. 21.1なぜ非同期にするのか
    2. 21.22 つのモデル
    3. 21.3Kafka の内部構造(面接必修)
    4. 21.4配信保証とコンシューマ設計
    5. 21.5製品の使い分け
    6. 21.6キューの落とし穴
  4. CHAPTER 22全文検索とベクター検索(セマンティック検索・ANN)
    1. 22.1なぜ LIKE '%keyword%' ではダメか
    2. 22.2転置インデックス(倒置索引)
    3. 22.3スコアリング(TF-IDF と BM25)
    4. 22.4Elasticsearch / OpenSearch の内部設計
    5. 22.5ベクター検索(Dense Retrieval)の基礎
    6. 22.6近似最近傍探索(ANN: Approximate Nearest Neighbor)
      1. ① HNSW(Hierarchical Navigable Small World)
      2. ② IVF-PQ(Inverted File + Product Quantization)
      3. ③ ScaNN(Scalable Nearest Neighbors — Google 開発)
    7. 22.7ハイブリッド検索と RRF(Reciprocal Rank Fusion)
    8. 22.8メタデータフィルタリング(Filtered Vector Search)
  5. CHAPTER 23オブジェクトストレージとファイル配信
    1. 23.1なぜ DB にファイルを入れないのか
    2. 23.2オブジェクトストレージの性質
    3. 23.3アップロードの設計(面接頻出)
    4. 23.4配信の設計
    5. 23.5重複排除とストレージ効率
  6. CHAPTER 24レート制限
    1. 24.1なぜ必要か
    2. 24.25 つのアルゴリズム
      1. ① 固定ウィンドウカウンタ
      2. ② スライディングウィンドウログ
      3. ③ スライディングウィンドウカウンタ(実務の定番)
      4. ④ トークンバケット(最も広く使われる)
      5. ⑤ リーキーバケット
    3. 24.3分散環境での実装
    4. 24.4レート制限の設計上の注意
  7. CHAPTER 25分散 ID 生成
    1. 25.1要件
    2. 25.2手法の比較
    3. 25.3Snowflake ID(面接必修)
    4. 25.4用途別の選び方
    5. 25.5セグメント(バッチ)方式 — 実用的で堅牢
  8. CHAPTER 26確率的データ構造
    1. 26.1Bloom フィルタ
    2. 26.2HyperLogLog — ユニーク数の推定
    3. 26.3Count-Min Sketch — 頻度の推定
    4. 26.4その他
  9. CHAPTER 27地理空間インデックス
    1. 27.1素朴な方法の限界
    2. 27.2主要な手法
      1. ① Geohash(最も使いやすい)
      2. ② QuadTree(四分木)
      3. ③ S2 (Google) / H3 (Uber)
      4. ④ PostGIS / R-Tree
      5. ⑤ Redis GEO
    3. 27.3設計パターン:Uber 型の配車

システムの部品箱

キャッシュ、CDN、メッセージキュー、全文検索とベクター検索、レート制限、分散 ID、確率的データ構造、地理空間インデックス。設計図に置く「箱」の中身を一つずつ開けます。

収録:第 20 章 〜 第 27 章

CHAPTER 20キャッシュと CDN

🎯 GOAL

キャッシュを「どこに・どう置き・どう壊れるか」まで説明できる。

20.1キャッシュを置ける場所(全部言えるように)

CACHE LAYERS
ブラウザキャッシュ
    ↓
CDN / エッジキャッシュ            ← 静的コンテンツ、地理的に近い
    ↓
ロードバランサ / リバースプロキシキャッシュ
    ↓
アプリのローカルメモリ(プロセス内)  ← 最速だがノードごとに別物
    ↓
分散キャッシュ(Redis / Memcached) ← 共有できる。ネットワーク 1 往復
    ↓
データベースのバッファプール        ← DB が自前で持つページキャッシュ
    ↓
OS のページキャッシュ
    ↓
ディスク
📐 原則

ユーザーに近いほど速く安いが、無効化が難しい。

20.2キャッシュ戦略

戦略 読み取り 書き込み 特徴
Cache-Aside(Lazy Loading) キャッシュを見る→無ければ DB→書き戻す DB に書いてキャッシュを削除 最も一般的。実装が単純。初回は必ずミス
Read-Through キャッシュライブラリが自動で DB を読む アプリが単純になる
Write-Through キャッシュと DB を連携して書く 原子的な連携がなければ常に一貫とは限らない。書き込みが遅く、書いたが読まれないデータで無駄が出る
Write-Behind(Write-Back) キャッシュに書いて、後でまとめて DB へ 超高速だがデータ損失リスク。カウンタ集計向き
Refresh-Ahead 期限切れ前に先回りで更新 予測が当たれば常にヒット
PYTHON — CACHE-ASIDE
def get_user(user_id):
    key = f"user:{user_id}"
    v = cache.get(key)
    if v is not None:
        return v                      # ヒット
    v = db.query("SELECT ... WHERE id=?", user_id)   # ミス
    cache.set(key, v, ttl=300 + random.randint(0, 60))  # ← ジッタを入れる
    return v

def update_user(user_id, data):
    db.update(...)
    cache.delete(f"user:{user_id}")   # ← 更新ではなく「削除」する
⚠️ なぜ更新ではなく削除か

2 つの更新が並行したとき、「DB 更新 → キャッシュ更新」の順序が入れ替わるとキャッシュに古い値が残り続けます。ただし削除にも race があります。リーダーが更新前の DB 値を読んでから、更新後の削除をまたいで古い値を書き戻す可能性があります。厳密さが必要なら、値にバージョンを付ける、更新と無効化の順序を管理する、CDC/Write-Through を使う、またはキャッシュを認可・整合性境界の外に置くなどの設計を選びます。

20.3キャッシュの 3 大障害(面接必修)

① キャッシュスタンピード / サンダリングハード

人気キーの TTL が切れた瞬間、1 万個の同時リクエストが全部 DB に殺到 → DB が死ぬ

対策

  • ミューテックス / シングルフライト:最初の 1 リクエストだけ DB を読み、他は待つ
  • 確率的早期期限切れ (XFetch):期限が近づくほど確率的に先回りで更新
  • TTL にジッタを加える(同時失効を防ぐ、最重要かつ最も簡単)
  • 論理的期限切れ:値と一緒に「論理期限」を持ち、期限切れでも古い値を返しつつ裏で更新
PYTHON — XFETCH
if now - delta * beta * log(random()) >= expiry:  # 期限前に確率的に再計算
    refresh()

② キャッシュペネトレーション(貫通)

存在しない ID を大量に問い合わせる → 毎回キャッシュミス → 毎回 DB に行く(攻撃にも使われる)

対策ネガティブキャッシュ(「存在しない」を短い TTL でキャッシュ)、Bloom フィルタで存在チェック(第 26 章)

③ キャッシュアバランチ(雪崩)

Redis クラスタが落ちる / 全キーの TTL が同時に切れる → 全リクエストが DB へ

対策:TTL のジッタ、多層キャッシュ(ローカル + 分散)、サーキットブレーカとレート制限で DB を守る、キャッシュ復旧時のウォームアップ

🎤 面接

加点フレーズ

SCRIPT
「キャッシュは可用性の依存先になるので、キャッシュが落ちてもサービスが死なない
 設計にします。具体的には、キャッシュミス時に DB へ流れる量をレート制限し、
 それを超えたリクエストは degraded なレスポンス
 (古いデータ or 一部機能停止)を返します。」

20.4追い出しポリシー

ポリシー 説明 向き
LRU 最も長く使われていないものを追い出す 汎用。最も一般的
LFU 使用頻度が低いものを追い出す 人気が安定している場合
FIFO 入った順 単純だが性能は劣る
TTL 時間で失効 鮮度が重要な場合
W-TinyLFU LFU の推定(Count-Min Sketch)+ LRU ウィンドウ ⭐ 現代のベストプラクティス(Caffeine が採用)
⚠️ LRU の弱点

一度きりの大量スキャン(バッチ処理)でキャッシュが全部入れ替わってしまう(キャッシュ汚染)。→ セグメント LRU や W-TinyLFU で緩和。

20.5Redis と Memcached

Redis Memcached
データ型 String, List, Hash, Set, Sorted Set, Stream, HLL, Bitmap, Geo 文字列のみ
永続化 RDB(スナップショット)/ AOF(追記ログ) なし
レプリケーション あり(非同期) なし
スレッド 単一スレッド(I/O は複数化可) マルチスレッド
用途 汎用データ構造サーバー、ランキング、キュー、分散ロック 純粋なキャッシュ、大きな値、マルチコアを使い切る
📐 数字・公式

Redis の Sorted Set はランキング設計で必須

REDIS
ZADD leaderboard 1500 "user:42"
ZREVRANGE leaderboard 0 9 WITHSCORES   -- 上位 10 人を O(log N + 10) で取得
ZREVRANK leaderboard "user:42"          -- 自分の順位
⚠️ Redis の注意点

  • 単一スレッドなので KEYS * や大きな LRANGE は全体をブロックするSCAN を使う)
  • レプリケーションは非同期なので、フェイルオーバー時に書き込みが失われうる
  • クラスタモードでは複数キーの操作が同一スロットに限定される(ハッシュタグ {user1}:profile で制御)

20.6CDN

仕組み:世界中の PoP(Point of Presence)にコンテンツを複製し、Anycast / GeoDNS で最寄りへ誘導する。

CDN FLOW
ユーザー(東京)→ 東京 PoP(キャッシュヒット → 5 ms で返却)
                    │ ミス
                    ▼
                 シールド/リージョナルキャッシュ
                    │ ミス
                    ▼
                 オリジンサーバー(米国, 120 ms)

CDN が扱えるもの

  • 静的:画像、CSS/JS、動画、フォント、ダウンロードファイル
  • 動的:API レスポンス(短い TTL + stale-while-revalidate)、エッジコンピューティング(Cloudflare Workers 等)で認証・A/B・パーソナライズも可能
重要な設計テクニック 説明
キャッシュキーの正規化 不要なクエリパラメータ(utm_source 等)を除外してヒット率を上げる
オリジンシールド 中間層を置き、オリジンへのリクエストを集約する
リクエストコアレッシング 同じキーへの同時ミスを 1 本にまとめてオリジンへ
署名付き URL 有効期限付きのアクセス許可(有料コンテンツ)
キャッシュパージ バージョン付き URL が基本。パージは全 PoP への伝播に時間がかかる
Vary ヘッダ Vary: Accept-Encoding は良い。Vary: User-Agent はヒット率が壊滅する
🎤 面接

SCRIPT
「画像は 200 KB × 2 万 QPS = 4 GB/s になるので、オリジンでは到底捌けません。
 CDN に載せ、ファイル名にコンテンツハッシュを含めて immutable を付けます。
 オリジンへのリクエストは全体の 1% 以下に抑えられる想定です。」

第 20 章 一問一答

Cache-Aside で更新時にキャッシュを「更新」ではなく「削除」する理由は。
並行更新で順序が逆転し古い値が残るのを防ぐため。
キャッシュスタンピードの対策を 3 つ。
シングルフライト(ミューテックス)、TTL ジッタ、確率的早期更新。
存在しないキーへの大量アクセス対策は。
ネガティブキャッシュと Bloom フィルタ。
Redis で KEYS * を避ける理由は。
単一スレッドのため全リクエストがブロックされる。

CHAPTER 21メッセージキューとストリーム処理基盤

🎯 GOAL

非同期化の設計ができ、Kafka の内部を説明できる。

21.1なぜ非同期にするのか

SYNC vs ASYNC
【同期】ユーザー登録 API
  DB 保存(20ms) → 確認メール送信(800ms) → 画像生成(1500ms) → レスポンス
  = 2.3 秒。しかもメール送信が失敗したら登録も失敗する

【非同期】
  DB 保存(20ms) → キューに積む(2ms) → レスポンス(22ms!)
  ワーカーが後からメール送信・画像生成

非同期化の 5 つの効果

  1. レイテンシ短縮:重い処理をリクエストから外す
  2. 疎結合:送信側は受信側を知らなくてよい
  3. バッファリング(平滑化):突発的な負荷をキューが吸収する
  4. リトライの一元化:失敗したら再処理できる
  5. ファンアウト:1 つのイベントを複数の消費者へ
⚠️ 代償

結果整合になる、デバッグが難しくなる、「キューが詰まっている」という新しい障害モードが生まれる、順序保証の設計が必要。

21.22 つのモデル

キュー(ポイントツーポイント) Pub/Sub(トピック)
配信先 1 メッセージを 1 コンシューマが処理 全購読者に配信
用途 ジョブ処理、タスク分散 イベント通知、複数システムへの伝播
SQS, RabbitMQ(queue), Celery Kafka, Pub/Sub, SNS, Redis Pub/Sub

21.3Kafka の内部構造(面接必修)

KAFKA TOPIC
Topic: "orders"
 ├ Partition 0: [msg0][msg1][msg2][msg3] →  ← 追記のみのログ
 ├ Partition 1: [msg0][msg1][msg2] →
 └ Partition 2: [msg0][msg1][msg2][msg3][msg4] →

各パーティションは複数ブローカーにレプリケート(leader + follower)
コンシューマグループ内では、1 パーティション = 1 コンシューマ

押さえるべき性質

  1. 順序保証はパーティション内のみ
    → 順序が必要なら同じキーを同じパーティションへhash(key) % partitions)。例:同じ user_id のイベントは必ず同じ順で処理される
  2. オフセット管理:コンシューマは「どこまで読んだか」を自分で管理する。→ メッセージは読んでも消えない(保持期間 or サイズで削除)→ 再処理(リプレイ)が可能 ← これが Kafka の最大の価値
  3. 並列度の上限 = パーティション数
    → コンシューマを増やしてもパーティション数を超えると遊ぶ。パーティション数は後から増やせるが、減らせないし、増やすとキーの割当が変わる
  4. 速さの理由:シーケンシャル I/O(追記のみ)、ゼロコピーsendfile でカーネル空間からそのまま NIC へ)、バッチング + 圧縮、ページキャッシュの活用

ISR (In-Sync Replicas) と acks

ACKS
acks=0   : 送りっぱなし。最速・最も危険
acks=1   : リーダーが書けば OK。リーダー障害で失う可能性
acks=all : ISR 全員が書いたら OK。min.insync.replicas=2 と併用が定番

21.4配信保証とコンシューマ設計

DELIVERY
at-most-once : 処理前にオフセットをコミット(失敗すると失われる)
at-least-once: 処理後にオフセットをコミット(重複しうる)← 基本
📐 鉄則

at-least-once + 冪等な処理(第 18 章)。

論点 設計
バックプレッシャ コンシューマが遅いとラグが増える。ラグを監視し、オートスケールする
ポイズンメッセージ 何度やっても失敗するメッセージが先頭で詰まる → DLQ(Dead Letter Queue)へ退避
リトライ 指数バックオフ。即時リトライは負荷を増やすだけ。リトライ用トピックを分ける
リバランス コンシューマ増減時にパーティションが再割当される。処理中断が起きる(協調的リバランスで緩和)
順序と並列の両立 キー単位で順序、キー間は並列。単一パーティション内でもキーごとにワーカーを分ける手法がある

21.5製品の使い分け

製品 特徴 向いている用途
Kafka 高スループット、永続ログ、リプレイ可 イベントストリーム、ログ収集、CDC、分析基盤
RabbitMQ 柔軟なルーティング、優先度、遅延配信 タスクキュー、複雑なルーティング
SQS フルマネージド、スケール上限とクォータを持つ 一般的なジョブキュー(順序が要るなら FIFO キュー)。上限、可視性、重複配信を確認
Redis Streams 軽量・低レイテンシ 小〜中規模、既に Redis がある場合
Pulsar ストレージと計算の分離、マルチテナント 大規模マルチテナント
🎤 面接

SCRIPT
「イベントを複数のコンシューマ(検索インデックス更新・通知・分析)が
 別々のペースで消費し、障害時に過去に遡って再処理したいので Kafka を選びます。
 単純なジョブキューであれば運用が楽な SQS で十分です。」

21.6キューの落とし穴

⚠️ キューは魔法ではありません

  • キューが伸び続けるなら、コンシューマの処理能力が足りていないだけ。キューは問題を「見えなくする」ことがある
  • キュー滞留時間 = ラグ。ユーザーが待つ処理をキューに入れると、「送信しました」と言いつつ 10 分後に届く、という体験になる
  • リトライストーム:全メッセージが同時に失敗して同時にリトライし、下流を殺す → ジッタ + サーキットブレーカ

第 21 章 一問一答

Kafka で順序が保証される単位は。
パーティション内のみ。同じキーは同じパーティションへ送ることで順序を担保する。
Kafka の並列度の上限は何で決まるか。
パーティション数(1 パーティション = 1 コンシューマ)。
DLQ の目的は。
何度も失敗するメッセージを退避し、後続の処理をブロックしないため。
Kafka が速い理由を 3 つ。
シーケンシャル I/O、ゼロコピー、バッチング+圧縮(+ページキャッシュ活用)。

CHAPTER 22全文検索とベクター検索(セマンティック検索・ANN)

🎯 GOAL

キーワード検索(転置インデックス)と意味検索(ベクター検索・ANN)の仕組みを理解し、現代のハイブリッド検索システムをゼロから設計できるようになる。

22.1なぜ LIKE '%keyword%' ではダメか

SQL
SELECT * FROM posts WHERE content LIKE '%システム設計%';
  • インデックスが使えない → 全行スキャン(1 億行なら数十秒)
  • 語彙の不一致問題 (Vocabulary Mismatch Problem):「おいしい林檎の切り方」で検索したとき、「アップルの皮むき手順」という文書はヒットしない
  • 表記ゆれ・語形変化・同義語に対応できない
  • スコアリング(どの文書がより関連しているかの順位付け)ができない

22.2転置インデックス(倒置索引)

INVERTED INDEX
【元データ】
doc1: "分散システムの設計"
doc2: "システム設計の面接"
doc3: "分散データベース"

【転置インデックス】(単語 → その語を含む文書リスト)
"分散"     → [doc1, doc3]
"システム" → [doc1, doc2]
"設計"     → [doc1, doc2]
"面接"     → [doc2]
"データベース" → [doc3]

検索「分散 AND 設計」→ [doc1,doc3] ∩ [doc1,doc2] = [doc1]
ANALYZER PIPELINE
原文 → ① 文字正規化(全角半角、大文字小文字、Unicode 正規化)
     → ② トークン化(英語は空白、日本語は形態素解析 or N-gram)
     → ③ ストップワード除去("the", "の")
     → ④ ステミング/正規化(running → run、送り仮名の統一)
     → ⑤ 同義語展開(PC = パソコン)
     → 転置インデックスへ
⚠️ 日本語の難しさ

分かち書きがないので、

  • 形態素解析(Kuromoji, MeCab):辞書ベース。精度が高いが未知語に弱い
  • N-gram(bi-gram):「システ」「ステム」…と機械的に分割。取りこぼしがないがインデックスが巨大でノイズ(偽陽性)が多い

実務では両方のフィールドを作り、OR で検索するのが定石。

22.3スコアリング(TF-IDF と BM25)

📐 TF-IDF

スコア = 出現回数(TF) × 希少度(IDF)

  • TF:その文書に何回出てくるか(多いほど関連が強い)
  • IDF:全文書のうち何割に出てくるか(レアな語ほど重要)
📐 BM25

TF-IDF の改良版で現代のキーワード検索の標準。TF の効果を飽和させ(10 回出現しても 100 回出現してもスコアが無限に伸びないよう上限を設ける)、文書長で正規化する(長い文書ほど単語が多くなるペナルティを与える)。

22.4Elasticsearch / OpenSearch の内部設計

STRUCTURE
Index(テーブル相当)
 └ Shard(Lucene インデックス 1 つ = 独立した検索単位)
     ├ Primary shard
     └ Replica shard(読み取り分散 + 冗長化)
論点 指針
シャード数 変更には split/shrink/reindex など製品ごとの手順が必要。1 シャード 10〜50GB はワークロードと運用上限から決める目安
更新の反映 refresh_interval(既定 1 秒)。準リアルタイムであり即時ではない
更新コスト Lucene セグメントはイミュータブル。更新 = 削除マーク + 新規追加 → マージが走る
真実の情報源 しない。DB を真実の情報源とし、ES は再構築可能な派生データとする
時系列データ 日次インデックス + エイリアス、ILM(古いものを安いノードへ、最後は削除)
📐 DB → 検索インデックスの同期

① 二重書き込み(アプリが両方に書く)→ ❌ 不整合が必ず起きる
② Outbox / CDC(Debezium が MySQL binlog を読んで Kafka へ)→ ✅ 推奨
③ 定期バッチで全件再構築 → 補助として併用(ドリフト修正)

22.5ベクター検索(Dense Retrieval)の基礎

キーワード検索が「単語の一致」を見るのに対し、ベクター検索は「意味の近さ(意味空間上の距離)」を見ます。

EMBEDDING
テキスト "犬が公園を走る" ──[Embedding モデル]──► [0.12, -0.85, 0.44, ..., 0.03] (768〜1536次元)
テキスト "子犬が外で遊ぶ" ──[Embedding モデル]──► [0.15, -0.81, 0.40, ..., 0.05]
  → 高次元空間での距離(コサイン類似度・内積)が非常に近い!
類似度の尺度 計算式 特徴・使いどころ
コサイン類似度 cos(θ) = (A·B) / (‖A‖‖B‖) ベクトルの長さ(文章量)を無視し、方向(意味)の一致度だけを測る(-1〜1)
内積 (Dot Product) A·B = Σ AᵢBᵢ ベクトルが正規化(長さ=1)されていればコサイン類似度と等価。計算が最速
ユークリッド距離 (L2) √(Σ (Aᵢ − Bᵢ)²) 空間上の直線距離。距離が小さいほど類似
📐 Embedding のサイズ見積もり(面接必須)

1 ベクトル = 1536 次元 (OpenAI text-embedding-3 等) × 4 バイト (float32) ≒ 6 KB
・1,000 万文書 = 10^7 × 6 KB = 60 GB
・1 億文書     = 10^8 × 6 KB = 600 GB(純ベクトルデータのみ)
※ 後述の HNSW グラフインデックスのオーバーヘッドで約 1.5〜2 倍(1〜1.2 TB)になるため、
   メモリ見積もりと量子化(Quantization)の検討が不可欠。

22.6近似最近傍探索(ANN: Approximate Nearest Neighbor)

1 億件のベクトルに対して、クエリベクトルとの距離を全件計算(ブルートフォース:O(N·D))すると、1 回の検索に数秒かかり実用になりません。そこで「100% 正確な最寄りではなく、精度とレイテンシの目標をベンチマークで決め、近似候補を探す」アルゴリズム(ANN)を使います。

ANN COMPARISON
┌─────────────────────────────────────────────────────────────┐
│                    ANN アルゴリズムの比較                   │
├──────────────────┬──────────────────┬───────────────────────┤
│ HNSW             │ IVF-PQ           │ ScaNN (Google)        │
├──────────────────┼──────────────────┼───────────────────────┤
│ グラフベース     │ クラスタ + 積量子化│ クラスタ + 異方性量子化│
│ 高精度・超高速   │ メモリ 90% 削減  │ 高速・高再現率        │
│ メモリ消費 大    │ 精度はやや低下   │ Google 検索/YouTube   │
└──────────────────┴──────────────────┴───────────────────────┘

① HNSW(Hierarchical Navigable Small World)

  • 仕組み:スキップリストのグラフ版。多層の近傍グラフを構築する。最上位層はまばらなノード間を大股でジャンプし、クエリに近い領域を高速特定。最下位層は密なノード間を細かく探索し、高精度な近傍を見つける
  • 長所:適切なパラメータなら高速な近似検索を実現できる。p99、Recall、メモリは次元数、データ分布、ef/search 幅、ハードウェア、負荷でベンチマークする
  • 短所メモリ消費が巨大(生ベクトルに加え、各ノードのグラフエッジ情報で 1.5〜2 倍の RAM が必要)

② IVF-PQ(Inverted File + Product Quantization)

  • IVF(転置ファイル):空間を k-means で K 個(例:4096 個)のボロノイ領域(クラスタ)に分割。クエリが属する最寄り数クラスタのベクトルだけを探索対象にする(探索範囲の劇的削減)
  • PQ(積量子化):1536 次元のベクトルを 8 個のサブベクトル(各 192 次元)に分割し、各サブベクトルを 256 個の代表値(重心)の ID(1 バイト)に置き換える → 6,144 バイト (float32) が、わずか 8 バイト(8 個の 1Byte ID)に圧縮(99% 以上のメモリ削減!)
  • 長所メモリ効率が極めて高い。圧縮率、メタデータ、検索精度を測ったうえで、1 億件を数十 GB 規模の RAM に収められる場合がある
  • 短所:量子化による精度低下があり得る。Recall は圧縮方式、学習データ、検索パラメータで測定する

③ ScaNN(Scalable Nearest Neighbors — Google 開発)

  • Google のイノベーション異方性量子化 (Anisotropic Quantization)
  • 仕組み:量子化の誤差を計算する際、「内積(距離)の平行方向の誤差」を厳しく罰し、「直交方向の誤差」を許容する。これにより、量子化しながらも内積の順位逆転を最小化
  • 用途:Google 検索、YouTube 推薦、Google Cloud Vertex AI Search のバックエンド

22.7ハイブリッド検索と RRF(Reciprocal Rank Fusion)

実務の検索において、ベクター検索単体でキーワード検索を完全に置き換えることはできません

検索方式 得意なこと 苦手なこと
キーワード検索 (BM25) 商品型番(A-1234)、固有名詞、エラーコード、完全一致 抽象的な概念、言い換え、長文の質問
ベクター検索 (Dense) 意図の理解、意味の類似(「安い宿」=「格安ホテル」) 型番の完全一致、否定表現、専門用語

異なるスコア体系(BM25 の実数値スコアと、コサイン類似度の 0〜1 スコア)をそのまま足し算することはできません。「順位(Rank)」だけを使って安全に統合するアルゴリズムが RRF です。

📐 RRF スコアの計算式

RRF_Score(d) = Σ[m ∈ M]  1 / (k + r_m(d))

  M       : 検索エンジンの集合(BM25 と ベクター検索)
  r_m(d)  : エンジン m における文書 d の順位(1位なら 1, 2位なら 2...)
  k       : 定数(通常 k = 60 が標準)
EXAMPLE
【例: 文書 A】
・BM25 で 1 位  → 1 / (60 + 1) = 0.01639
・Vector で 3 位 → 1 / (60 + 3) = 0.01587
  → 合計 RRF スコア = 0.03226(最上位候補)

22.8メタデータフィルタリング(Filtered Vector Search)

「東京都内のホテルで、評価 4.0 以上(メタデータ条件)の中で、クエリに最も近いもの」を探す場合、ナイーブな実装では破綻します。

FILTERING STRATEGIES
❌ Pre-filtering(先に SQL 等で絞り込んでから ANN)
   → 候補が 1 万件に減った状態でグラフ探索すると、
     グラフが途切れて ANN が辿り着けない(孤立ノード問題)

❌ Post-filtering(ANN で上位 100 件取ってからメタデータで絞る)
   → 上位 100 件の中に条件合致が 1 件もなく、検索結果が 0 件になる

✅ Single-Stage / Iterative Filtered Search(グラフ探索中にメタデータを評価)
   → HNSW の探索ステップごとに「メタデータ条件を満たすか」を
     ビットマップ等で高速チェックしながら巡回
🎤 面接

SCRIPT
「商品検索では BM25 と HNSW ベクター検索のハイブリッド検索を採用し、
 結果を RRF(k=60)で統合します。
 型番やブランド名による完全一致は BM25 で確実に拾い、
 自然文での意図検索は Embedding でカバーします。
 価格やカテゴリの絞り込みには、グラフ探索中にビットセットで条件評価を行う
 Iterative Filtering を使い、結果の取りこぼしを防ぎます。」

第 22 章 一問一答

転置インデックスとベクター検索の決定的な違いは。
転置インデックスは単語の完全一致と出現頻度を見るが、ベクター検索は高次元空間での意味的な近さ(コサイン類似度/内積)を見る。
HNSW と IVF-PQ のトレードオフは。
HNSW はメモリを多く使う代わりに調整次第で高い Recall を得やすい。IVF-PQ は圧縮でメモリを減らせるが、削減率と Recall 低下はデータ・パラメータで実測する。
ハイブリッド検索で RRF を使う理由は。
BM25 とベクター類似度ではスコアの尺度や分布が異なるため、順位(Rank)のみを使って安全かつロバストに統合できるから。
Post-filtering(ベクター検索後のメタデータ絞り込み)の問題点は。
ANN で取得した上位候補の中に条件を満たすものが存在せず、結果が 0 件になるリスクがある。

CHAPTER 23オブジェクトストレージとファイル配信

🎯 GOAL

画像・動画のアップロードと配信を、DB に入れずに設計できる。

23.1なぜ DB にファイルを入れないのか

⚠️ 落とし穴

大きな BLOB を RDB に入れると、バックアップ、バッファプール、レプリケーション、配信が重くなることがあります。一方、サイズが小さく、トランザクションと一緒に保存する必要があるデータでは RDB が合理的な場合もあります。サイズ、更新頻度、アクセス経路、バックアップ、暗号化、CDN 要件で決めます。

✅ 大きな・頻繁に配信するファイルは、オブジェクトストレージ(S3 / GCS)に置き、DB にはメタデータとキーを置く構成が一般的です。

23.2オブジェクトストレージの性質

性質 内容
データモデル key → オブジェクト(+メタデータ)。ディレクトリは擬似的/ はキーの一部)
操作 PUT / GET / DELETE / LIST。部分更新はできない(全体を置き換える)
耐久性 事業者、リージョン、ストレージクラス、冗長化設定ごとの保証値を確認
一貫性 オブジェクトストレージ製品・操作・リージョン構成ごとの read-after-write、LIST、削除の保証を確認
コスト 保存が非常に安く、リクエスト数と外向き転送で課金される
ストレージクラス Standard / Infrequent Access / Glacier など。取り出し料金、最小保存期間、遅延を含めて料金表で評価

23.3アップロードの設計(面接頻出)

❌ 悪い設計:クライアント → アプリサーバー → S3(アプリサーバーの帯域とメモリを浪費し、スケールしない)

✅ PRE-SIGNED URL
① クライアント → アプリ:「upload.jpg を上げたい(10 MB, image/jpeg)」
② アプリ: 権限チェック → S3 の署名付き PUT URL を発行(有効期限 15 分)
③ クライアント → S3 に直接 PUT   ← アプリサーバーを経由しない
④ S3 のイベント通知 → Lambda/キュー → サムネイル生成・DB のステータス更新
📐 マルチパートアップロード

  • 5 MB〜100 MB のチャンクに分割して並列アップロード
  • 失敗したチャンクだけ再送 → レジューム可能
  • 完了時に S3 側で結合
RESUMABLE UPLOAD
アップロードセッションを DB に記録: upload_id, 総サイズ, 完了チャンクのビットマップ
→ 中断後は「どこから再開すればいいか」をクライアントに返せる

23.4配信の設計

非公開ファイル: 署名付き GET URL(有効期限付き)→ CDN 経由
公開ファイル:   CDN に長期キャッシュ + コンテンツハッシュ付きファイル名
VIDEO PIPELINE
アップロード → トランスコード(複数解像度・ビットレート)
   1080p/5Mbps, 720p/2.5Mbps, 480p/1Mbps, 360p/600kbps
→ セグメント化(HLS/DASH: 2〜10 秒ごとの .ts / .m4s ファイル + マニフェスト)
→ CDN へ配置
→ プレイヤーが帯域を測定し、セグメント単位で品質を切替(ABR: Adaptive Bitrate)
📐 数字・公式

ABR が重要な理由:「再生開始の速さ」と「途中で止まらないこと」は画質より体験に効きます。低画質で即開始し、徐々に上げるのが定石です。

23.5重複排除とストレージ効率

  • コンテンツアドレス指定:ファイルの SHA-256 をキーにする → 同じファイルは 1 つしか保存されない(Dropbox 型の設計)
  • チャンクレベルの重複排除:可変長チャンク(Rabin fingerprint)に分割してハッシュ化 → ファイルの一部だけ変更されたとき、変更チャンクだけ転送すればよい
  • デルタ同期 (rsync):差分だけを送る
🎤 面接(Google Drive 設計)

SCRIPT
「ファイルを 4 MB の可変長チャンクに分割し、各チャンクの SHA-256 をキーにして
 保存します。クライアントは同期前にチャンクハッシュのリストをサーバーに送り、
 サーバーが持っていないチャンクだけをアップロードします。
 これによりストレージも帯域も大幅に節約でき、
 同じファイルを複数人が持つ場合の重複も自動的に排除されます。」

第 23 章 一問一答

署名付き URL の利点は。
アプリサーバーを経由せず直接ストレージと通信でき、帯域とスケールの問題を回避できる。
マルチパートアップロードの利点は。
並列化による高速化と、失敗チャンクだけの再送(レジューム)。
ABR ストリーミングとは。
複数ビットレートのセグメントを用意し、クライアントが帯域に応じて動的に品質を切り替える方式。

CHAPTER 24レート制限

🎯 GOAL

5 つのアルゴリズムを説明でき、分散環境での実装を設計できる。

24.1なぜ必要か

  1. サービス保護:過負荷・DDoS からの防御
  2. 公平性:1 ユーザーが全体を占有しない
  3. コスト管理:従量課金のバックエンド(LLM API など)の暴走防止
  4. 課金プラン:Free は 100 req/h、Pro は 10,000 req/h

24.25 つのアルゴリズム

① 固定ウィンドウカウンタ

key = "user:42:2026-08-22T10:00"  → INCR、TTL 60 秒。上限を超えたら拒否
  • ✅ 極めて単純、メモリ効率が良い
  • 境界問題:10:00:59 に 100 回、10:01:00 に 100 回 → 2 秒間で 200 回通ってしまう(上限の 2 倍)

② スライディングウィンドウログ

Redis の Sorted Set にタイムスタンプを全部記録
ZREMRANGEBYSCORE(古いものを削除)→ ZCARD(件数)→ 上限内なら ZADD
  • ✅ 完全に正確
  • メモリを大量消費(全リクエストのタイムスタンプを保持)

③ スライディングウィンドウカウンタ(実務の定番)

現在のウィンドウのカウント + 前のウィンドウのカウント × (はみ出し割合)
例: 10:01:15 時点 → count(10:01) + count(10:00) × (45/60)
  • ✅ メモリ効率が良く、境界問題もほぼ解消。近似だが実用上十分

④ トークンバケット(最も広く使われる)

TOKEN BUCKET
バケットに毎秒 r 個のトークンを補充(上限 b 個)
リクエスト時に 1 個消費。無ければ拒否(or 待つ)

┌─────────┐
│ ●●●●●   │ ← 毎秒 r 個補充、最大 b 個まで貯まる
└────┬────┘
     ▼ リクエストが 1 個消費
  • バースト(急な集中)を許容できる(貯めたトークン分)
  • ✅ 状態が 2 つ(トークン数・最終更新時刻)だけで済む → メモリ効率が最高
  • 実装:「経過時間 × r」を遅延計算で補充する(タイマー不要)

⑤ リーキーバケット

リクエストをキューに入れ、一定速度で処理する
  • 出力レートが完全に一定(下流を守るのに最適)
  • ❌ バーストを許容しない、キューによる遅延が発生
目的 アルゴリズム
API のレート制限(汎用) トークンバケット
下流を一定速度で守る リーキーバケット
メモリ最小で近似で良い スライディングウィンドウカウンタ
厳密な課金 スライディングウィンドウログ

24.3分散環境での実装

[LB] → [APIサーバー × 50 台] → 共有の Redis でカウント
⚠️ 問題

各サーバーがローカルにカウントすると、50 台 × 100 req = 5000 req が通ってしまう。

方式 説明 トレードオフ
中央 Redis 全サーバーが 1 つの Redis を見る。Lua スクリプトで原子的に 正確だがレイテンシ +1 RTT、Redis が SPOF
ローカル + 定期同期 ローカルで数え、100 ms ごとに集約 高速だが一時的に超過を許す
クォータ分配 各サーバーに上限/N を配分し、余ったら融通 Google の Doorman 方式。スケールする
ステートレス(トークン埋め込み) 事前に発行したトークンを消費 実装が複雑
📐 Redis での原子的な実装

LUA
-- トークンバケットの例。GET/SET を分けると競合するので Lua で原子的に実行する
local tokens = tonumber(redis.call('HGET', KEYS[1], 'tokens') or capacity)
local last   = tonumber(redis.call('HGET', KEYS[1], 'ts') or now)
tokens = math.min(capacity, tokens + (now - last) * rate)
if tokens >= 1 then
  redis.call('HMSET', KEYS[1], 'tokens', tokens - 1, 'ts', now)
  return 1   -- 許可
else
  return 0   -- 拒否
end

24.4レート制限の設計上の注意

制限のキー(何単位で制限するか)
API キー / ユーザー ID  ← 認証済みなら最良
IP アドレス            ← 未認証時。ただし NAT 配下の企業ユーザーを巻き込む
IP + エンドポイント     ← ログイン試行など、危険な操作は厳しく
テナント / 組織         ← B2B SaaS
返すべきヘッダ
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 1755855600
📐 数字・公式

Retry-After はクライアントに再試行時刻を伝える有用なヘッダです。規格上必須とは限りませんが、返す場合は値を正しく設定し、クライアント側にも指数バックオフ、ジッタ、リトライ予算を実装します。

⚠️ 多層防御

レート制限は 1 箇所ではなく、CDN/WAF(IP 単位・粗い)→ API ゲートウェイ(ユーザー単位)→ サービス内(機能単位)の多層で行います。

第 24 章 一問一答

固定ウィンドウの境界問題とは。
ウィンドウの境界をまたぐと短時間に上限の 2 倍まで通ること。
トークンバケットの利点は。
バーストを許容しつつ平均レートを守れ、状態が小さい。
分散レート制限で Redis を使う際の注意は。
読み書きを Lua で原子化する。レイテンシと SPOF 化にも備える。
429 で返すべきヘッダは。
Retry-After(および RateLimit-* 系)。

CHAPTER 25分散 ID 生成

🎯 GOAL

用途に応じた ID 方式を選べる。

25.1要件

  • 一意(全世界で衝突しない)
  • 高スループット(数万〜数百万/秒)
  • 単調増加が望ましい(DB のインデックス効率、時系列ソート)
  • 短い・推測されにくい(URL に使う場合)
⚠️ 落とし穴

この 4 つは互いに矛盾します。「推測されにくい」と「単調増加」は両立しません。

25.2手法の比較

手法 長さ ソート可能 分散 欠点
DB の AUTO_INCREMENT 8 B △ 単一 DB の採番境界に依存 単一 DB の書き込み・フェイルオーバー要件を確認。必ず SPOF とは限らない
UUID v4(ランダム) 16 B ✅ 完全に独立 インデックスが断片化、長い
UUID v7 / ULID 16 B ✅(時刻順) 時刻が漏れる
Snowflake(Twitter) 8 B 時計の巻き戻しに弱い、ノード ID 管理が必要
チケットサーバー / セグメント 8 B 中央サーバーが必要(ただしバッチ払い出しで軽い)
ハッシュ + 短縮 6〜8 文字 衝突チェックが必要

25.3Snowflake ID(面接必修)

SNOWFLAKE — 64 BIT
 1 bit    41 bits            10 bits       12 bits
┌──┬────────────────────┬──────────┬────────────┐
│0 │ タイムスタンプ(ms)  │ ノードID  │ シーケンス  │  = 64 bit
└──┴────────────────────┴──────────┴────────────┘
  ↑符号ビット(通常0)    ↑1024ノード ↑同一ms内で4096個
📐 性能

1 ノードあたり同一ミリ秒に 4096 個という設計上限です。実際の発行率は、時計待ち、永続化、ノード管理、競合、フェイルオーバーで決まります。41 bit の期間は基準時刻から約 69 年ですが、仕様として明記します。

実装上の問題 対策
時計の巻き戻し(NTP 補正) 巻き戻しを検知したら、追いつくまで待つ or エラーにする
ノード ID の割当 ZooKeeper/etcd で採番、または k8s の StatefulSet の序数、MAC アドレス
同一 ms 内で 4096 個を超えた 次の ms まで待つ(スピン)
ID から時刻・順序が推測できる 公開 ID には別途、認可、レート制限、署名付き参照、暗号化などを使う。Hashids は暗号化や認可の代わりではない

25.4用途別の選び方

USE CASES
DB の主キー(InnoDB)    → Snowflake / UUIDv7 / ULID(単調増加が必須)
公開 URL の ID          → 推測不可能な ID(ランダム or 暗号化した連番)
短縮 URL                → Base62 の連番 or ハッシュ先頭 7 文字 + 衝突時に再試行
分散トレース ID         → UUID v4 / 128 bit ランダムで十分
冪等性キー              → クライアント生成の UUID v4
📐 Base62 変換

62 文字 [0-9a-zA-Z]
6 文字 → 62^6 = 568 億通り
7 文字 → 62^7 = 3.5 兆通り   ← 実用上ここが定番

25.5セグメント(バッチ)方式 — 実用的で堅牢

SEGMENT
アプリノードが DB から「1000 個分の範囲」をまとめて確保する
  UPDATE id_segments SET max_id = max_id + 1000 WHERE biz='order' RETURNING max_id;
  → ノードはメモリ内で 1000 個を配る(DB アクセスは 1000 回に 1 回)
  → 半分使ったら非同期で次の範囲を先読み確保(バッファ枯渇を防ぐ)
  • ✅ DB 負荷が 1/1000、単調増加、実装が単純
  • ❌ ノード再起動で番号が飛ぶ(実害はない)
🔬 深掘り

これは美団の Leaf や多くの実サービスで使われている、非常に実用的な方式です。

第 25 章 一問一答

UUID v4 を InnoDB の主キーにすべきでない理由は。
ランダムなためクラスタ化インデックスへの挿入が分散し、ページ分割と断片化を招く。
Snowflake の 64 bit の内訳は。
符号 1 + タイムスタンプ 41 + ノード ID 10 + シーケンス 12。これは識別子の形式であり、全ノードの厳密な実時間順序を保証しない。
Snowflake の最大の運用リスクは。
時計の巻き戻しによる ID 重複。検知して待機する必要がある。

CHAPTER 26確率的データ構造

🎯 GOAL

「正確さを少し捨てて、メモリを 1/100 にする」道具を使い分けられる。

26.1Bloom フィルタ

「絶対に無い」を高速・省メモリで判定する。

BLOOM FILTER
m ビットの配列と k 個のハッシュ関数
追加: h1(x), h2(x), h3(x) のビットを 1 にする
検査: 全部 1 なら「たぶんある」、1 つでも 0 なら「絶対にない」

     0 1 0 1 1 0 0 1 0 1 0 0
        ↑   ↑     ↑
      h1(x) h2(x) h3(x)
📐 性質

  • 偽陽性 (false positive) はあるが、偽陰性 (false negative) はない
  • 誤検知率 1% なら1 要素あたり約 9.6 bit(10 億要素で 1.2 GB)
  • 誤検知率 0.1% なら 1 要素あたり約 14.4 bit
  • 削除できない(Counting Bloom Filter なら可能)
  • 最適なハッシュ数 k = (m/n) × ln2
使いどころ 説明
LSM-Tree の SSTable 「このファイルにこのキーは無い」を即判定し、無駄な I/O を防ぐ
キャッシュペネトレーション防止 存在しない ID への DB アクセスを遮断
Web クローラ 訪問済み URL の判定
ユーザー名の重複チェック 「たぶん使われている」なら DB で確認
🔬 深掘り

Cuckoo フィルタ:削除可能で、低誤検知率では Bloom より省メモリ。

26.2HyperLogLog — ユニーク数の推定

1.5 KB で数十億のユニーク数を、誤差 2% 以内で数える。

HYPERLOGLOG
仕組み: ハッシュ値の先頭の 0 の連続数を見る
  「先頭に 0 が 10 個続く値が観測された」→ 要素数は 2^10 ≒ 1024 くらいだろう
  これを多数のバケットで平均(調和平均)して精度を上げる
📐 性能

標準誤差 = 1.04 / √m。m=16384 バケットで誤差 0.81%、メモリ 12 KB

使いどころ:ユニークビジター数、ユニーク検索クエリ数、「この動画を見たユニークユーザー数」など。

⚠️ 落とし穴

和集合は取れるが、積集合は精度が落ちる

REDIS
PFADD visitors:2026-08-22 user1 user2 user3
PFCOUNT visitors:2026-08-22
PFMERGE visitors:week visitors:day1 visitors:day2 ...   -- 和集合が取れる

26.3Count-Min Sketch — 頻度の推定

「どの要素が何回出たか」を固定メモリで近似する。

COUNT-MIN SKETCH
d 個のハッシュ関数 × w 個のカウンタの 2 次元配列
追加: 各行の hash_i(x) 番目のカウンタを +1
問合: 各行の値の最小値を返す(衝突による過大評価を最小値で抑える)
  • 常に実際の値以上(過小評価しない)
  • ヘビーヒッター検出(アクセスが多い上位 K 個)に使う
  • 使いどころ:レート制限の粗いカウント、トレンド検出、キャッシュの人気度推定(W-TinyLFU)

26.4その他

構造 用途
t-digest / DDSketch 分散環境でのパーセンタイル(p99)の近似集計。マージ可能
Top-K / Space-Saving 上位 K 個の頻出要素
MinHash / SimHash 集合の類似度・near-duplicate 検出(重複記事の除去)
スキップリスト 確率的な平衡構造。Redis の Sorted Set、LSM の MemTable
🎤 面接

SCRIPT
「1 日のユニークビジター数を正確に出すには全ユーザー ID を保持する必要があり、
 1 億人なら数 GB になります。しかし表示は概算で十分なので、
 HyperLogLog を使えば 12 KB・誤差 1% 未満で済みます。
 課金に関わる正確な数値が必要な部分だけ、日次バッチで正確に集計します。」

これは「要件に応じて精度とコストを交換できる」という強いシグナルです。

第 26 章 一問一答

Bloom フィルタの偽陰性はあるか。
ない。偽陽性のみ発生する。
HyperLogLog のメモリと誤差は。
約 12 KB で誤差 1% 未満(数十億要素まで)。
Count-Min Sketch が過小評価しない理由は。
衝突は加算方向にのみ働き、問い合わせで最小値を採用するため。

CHAPTER 27地理空間インデックス

🎯 GOAL

「近くの店舗 / ドライバーを探す」を効率的に設計できる。

27.1素朴な方法の限界

SQL
SELECT * FROM drivers
WHERE SQRT(POW(lat-?,2) + POW(lng-?,2)) < 0.05;   -- 全行スキャン。100 万台で数秒

2 次元の距離は B-Tree インデックスで扱えません(1 次元しか順序付けできない)。→ 2 次元を 1 次元に落とすのが地理空間インデックスの本質です。

27.2主要な手法

① Geohash(最も使いやすい)

GEOHASH
緯度経度を交互にビット分割し、Base32 文字列にエンコードする
  "xn774c"  ← 東京駅付近

文字数と精度(緯度で幅が変わる概算):
  4 文字 → 約 20 km 四方
  5 文字 → 約 5 km
  6 文字 → 約 1.2 km
  7 文字 → 約 150 m
  8 文字 → 約 38 m

前方一致 = 近い!  → 通常の文字列インデックス(B-Tree)でそのまま検索できる
  SELECT * FROM shops WHERE geohash LIKE 'xn774%';
⚠️ 境界問題

セルの端にいると、隣接セルの近い店が見つからない。→ 自分のセル + 隣接 8 セルの計 9 個を検索するのが定石。

② QuadTree(四分木)

領域を再帰的に 4 分割し、1 ノードあたりの点が閾値を超えたら分割する
→ 密度に応じて適応的(都心は細かく、地方は粗く)
  • ✅ 密度の偏りに強い
  • ❌ 木構造をメモリに保持する必要があり、更新(移動体)に弱い

③ S2 (Google) / H3 (Uber)

  • S2:地球を立方体に投影し、ヒルベルト曲線で 1 次元化。球面上の面積の偏りを抑えるが、完全に同一ではない
  • H3:六角形を基本とする階層グリッド。セル形状には五角形などもあり、距離が全方向で完全に等しいわけではない。配車・配送では隣接セルと道路距離を評価する

④ PostGIS / R-Tree

SQL
CREATE INDEX ON shops USING GIST (location);
SELECT * FROM shops
WHERE ST_DWithin(location, ST_MakePoint(?,?)::geography, 1000)  -- 1km 以内
ORDER BY location <-> ST_MakePoint(?,?) LIMIT 10;               -- KNN 検索
  • ✅ 多角形・経路など複雑な形状も扱える。SQL で完結
  • ❌ 超高頻度更新(数百万台の位置更新)には向かない

⑤ Redis GEO

REDIS
GEOADD drivers 139.767 35.681 "driver:42"
GEOSEARCH drivers FROMLONLAT 139.77 35.68 BYRADIUS 3 km ASC COUNT 10

内部は Sorted Set + Geohash スコア。移動体のリアルタイム検索に最適

27.3設計パターン:Uber 型の配車

RIDE-HAILING
【ドライバーの位置更新】4 秒ごと × 100 万台 = 25 万 QPS
  → 永続 DB には書かない。Redis (GEO) or メモリ上のグリッドに保持
  → 履歴が必要なら Kafka → オブジェクトストレージへ非同期に流す

【近傍検索】
  ① 乗客の位置から H3/Geohash セルを算出
  ② 自セル + 隣接セルのドライバーを取得(数十〜数百人)
  ③ 直線距離でフィルタ → 実際の道路距離/ETA を計算(ここが重い)
  ④ スコアリング(距離・評価・車種)してマッチング

【スケール】
  地理でシャーディング(都市単位)→ 都市間のトラフィックは独立
  ホットスポット(渋谷駅)はセルをさらに細分化
🎤 面接

SCRIPT
「位置情報は書き込みが 25 万 QPS と多く、かつ数秒で陳腐化するので、
 永続化せずインメモリのグリッド(Redis GEO)に保持します。
 検索はセル単位で行い、境界問題に対応するため隣接セルも含めて 9 セルを検索します。
 都市ごとにシャーディングすれば、地理的にトラフィックが自然に分離されます。」

第 27 章 一問一答

Geohash の前方一致が使える理由は。
空間充填曲線により近い地点が似た接頭辞を持つため。
Geohash の境界問題への対処は。
隣接 8 セルを含む計 9 セルを検索する。
QuadTree の利点は。
密度に応じて適応的に分割するため、偏った分布でも効率が落ちにくい。