「探す」は、コンピュータの仕事の半分を占めると言ってもいいくらい基本的な処理。検索エンジン、、、ナビゲーション、 の中身まで、全部「探索」が中心にあります。今回はその発展史を、線形検索から最新のベクトル検索まで、自分で動かしながら追います。
① 線形 O(n) と二分 の絶望的な差を肌で感じる ② グラフ探索( / )と A* の違い ③ なぜ ChatGPT が「ベクトル検索」を内部で使っているか ④ 量子コンピュータの Grover アルゴリズムは何が嬉しいか
Step 0: なぜ「探す」が問題になるのか
100億ドキュメントから1秒以内に答えを返す Google、200万曲から好みの曲を見つける Spotify、20億トークンから次の単語を予測する ── 全部「探索の効率化」がコアの工夫です。 1946 ─ 線形検索(最初のコンピュータと同時):先頭から順に見るだけ。 1962 ─ 二分検索の体系化(実は古代から):ソート済みなら半分ずつ絞れる。 1953 ─ :H.P. Luhn が IBM で発明。O(1) 探索。 1959 ─ ダイクストラ法:最短経路。1968 ─ A*:ヒューリスティック付き最短経路。 2016 ─ (Hierarchical Navigable Small World):高次元ベクトルの近似最近傍探索。 2017〜 ─ ベクトル検索の実用化:(Facebook)、ScaNN(Google)、ChatGPT の など。 1996 ─ Grover アルゴリズム:量子コンピュータで O(√n)。
Step 1: 線形 vs 二分 ─ 計測してみる
import time, random, bisect
n = 10_000_000data = sorted(random.random() for _ in range(n))target = data[n // 2] # 中央の値を探す
# 線形(リストの index = O(n))t0 = time.perf_counter()data.index(target)print(f"線形: {time.perf_counter() - t0:.4f} s")
# 二分(O(log n))t0 = time.perf_counter()bisect.bisect_left(data, target)print(f"二分: {time.perf_counter() - t0:.4f} s")1000万要素では、線形は数百ms、二分は数µs。これが「アルゴリズム選択」の差。データ構造(ソート済みリスト)と探索方法(二分)はセットで設計する。
Step 2: ハッシュテーブル ─ O(1) で見つける
Python の dict や set は内部でハッシュテーブルを使ってます。「キーをハッシュ関数で配列インデックスに変換 → 直接アクセス」で、サイズに関わらず O(1)(衝突無視)。
import time, random
n = 10_000_000keys = list(range(n))random.shuffle(keys)d = {k: i for i, k in enumerate(keys)} # ハッシュテーブル構築
t0 = time.perf_counter()for _ in range(100_000): _ = d[random.randint(0, n-1)] # O(1) アクセスprint(f"100,000 回のハッシュ検索: {time.perf_counter()-t0:.3f} s")Step 3: グラフ探索 ─ BFS / DFS / A*
迷路を解く、地図上で目的地へのルートを探す、 で「友達の友達」を探す ── これら全部 グラフ探索。
from collections import deque
# 0=通路, 1=壁。S=start, G=goal は座標でmaze = [ [0,0,1,0,0], [1,0,1,0,1], [0,0,0,0,0], [0,1,1,1,0], [0,0,0,0,0],]start, goal = (0, 0), (4, 4)
def bfs(maze, start, goal): H, W = len(maze), len(maze[0]) q = deque([(start, [start])]) seen = {start} while q: (r, c), path = q.popleft() if (r, c) == goal: return path for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: nr, nc = r+dr, c+dc if 0 <= nr < H and 0 <= nc < W and maze[nr][nc] == 0 and (nr,nc) not in seen: seen.add((nr,nc)) q.append(((nr,nc), path + [(nr,nc)])) return None
path = bfs(maze, start, goal)print("最短経路:", path)
BFS: 最短経路(重みが等しいとき)。DFS: 深く掘る、解の存在判定。ダイクストラ: 重み付き最短経路。A*: ダイクストラ+ゴール方向のヒューリスティック → ゲームAIや経路探索の標準。
Step 4: ベクトル検索 ─ ChatGPT の中身
「意味が近い」を探したい場合、文字列の完全一致では無理。文を ベクトル(数百〜数千次元の数列) に変換し、ベクトル空間で近いものを探す=ベクトル検索。これが LLM の RAG(検索拡張生成)の中核。
import numpy as np
# 仮の文書ベクトル(本来は埋め込みモデルで作る)docs = { "犬は哺乳類": np.array([0.9, 0.1, 0.0, 0.2]), "猫は哺乳類": np.array([0.85, 0.15, 0.0, 0.25]), "魚は脊椎動物": np.array([0.1, 0.0, 0.9, 0.05]), "鳥は脊椎動物": np.array([0.2, 0.0, 0.85, 0.1]), "石は鉱物": np.array([0.0, 0.9, 0.0, 0.0]),}
def cosine(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 「ペット」っぽいクエリベクトルquery = np.array([0.88, 0.12, 0.0, 0.22])
scored = sorted(docs.items(), key=lambda kv: cosine(query, kv[1]), reverse=True)for name, v in scored: print(f"{cosine(query, v):.3f} {name}")全ベクトルとの類似度を計算すると O(n) で遅い。HNSW(2016) は「近所の近所を辿るネットワーク」で O(log n) に。FAISS / ScaNN / pgvector / Pinecone など、現代のは全部このアイデアを使っています。
Step 5: 量子探索 ─ Grover アルゴリズム
1996 年、Lov Grover が示した量子探索:ソートされていない n 要素から「条件を満たすもの」を O(√n) で見つけられる。古典コンピュータの O(n) より速い。10億要素なら、古典 10億回 vs 量子 約3万回。
ただし「データを 量子状態 に乗せる」前処理が必要で、現実的な大規模データではほとんど活きません。今のところ研究フェーズ。が、暗号攻撃などで重要視されています(AES の鍵長を実質半分にしてしまう)。
自由研究の提案
① 自分の写真でベクトル検索: 画像を CLIP 等のエンコーダで、好きな写真と「似てる」ものを探す。 ② Wikipedia コーパスで RAG 風: 100記事をベクトル化、自然文の質問に最も関連する記事を返すだけのミニ検索エンジン。 ③ A* で迷路ソルバー: 大きな迷路を生成し BFS と A* で比較。ヒューリスティック関数の作り方で速度が劇的に変わるのを確認。 ④ Bloom Filter / : 「確率的データ構造」── 100% 正確じゃないが、メモリと速度で圧倒的に有利。データ工学の現代標準ツール。
次回予告
EP.16 は計算理論の最先端。 問題、近似アルゴリズム、確率的データ構造、そして 2024 年に提示された「Powersort 級の最近の発見」と、量子計算が変えつつある計算可能性の地平。「何が原理的に解けないか」を知ることは、エンジニアの基礎体力です。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。