なぜSQLのLIKE句はパフォーマンスの「悪夢」なのか?
検索機能を実装する際、多くのエンジニアは LIKE '%keyword%' という最も手軽なクエリを使う方法を選びがちです。数千行程度のテーブルであれば問題ありません。しかし、データベースが100万レコードに達すると、ディスクのフルスキャン(Full Table Scan)が発生し、1つのSQLクエリのレスポンスに500msから2sもかかってしまうことがあります。
一方、現代のオートコンプリートの基準は10ms以下です。この数字を達成するには、データをRAM上に置く必要があります。Redisの Sorted Sets (ZSET) は、単なるキャッシュとしてだけでなく、辞書順(lexicographical)によるソート機能を利用して、瞬時に接頭辞(prefix)をフィルタリングできる強力な武器になります。
辞書順(Lexicographical Ordering)の威力
通常、Sorted Setsは score を使って順位を決定します。しかし、ここで秘訣があります。すべての score を0に設定すると、Redisは自動的に要素をアルファベット順(A-Z)に並べ替えます。このとき、Sorted Setは整理整頓された紙の辞書と全く同じように機能します。
コマンド ZRANGEBYLEX を使えば、ページを1枚ずつめくるのではなく、目的のキーワードがあるページへ直接ジャンプできます。これが、CPUに負荷をかけずに極めて高いスループットを実現するための鍵となります。
実践的な前方一致(Prefix Matching)テクニック
例えば、apple、apply、ball、battery というキーワードがあるとします。ユーザーが app と入力したとき、目的は「app」と辞書上の次の単語の間にあるものをすべて取得することです。
実行されるRedisコマンドは以下の通りです:
ZRANGEBYLEX search_zset "[app" "(app\xff"
[app: 開始地点(文字列 “app” を含む)。(app\xff: 終了地点。\xffという文字は文字コードにおける最大バイトであり、”apple”、”apply”、”application” などのすべてのバリエーションを網羅するのに役立ちます。
Pythonでの実装
実装を始めるには、データをRedisにロードする必要があります。手元に巨大な製品CSVファイルがある場合は、処理しやすいように先にJSONに変換することをお勧めします。私は、クライアントサイドで処理され内部データの漏洩の心配がない toolcraft.appのCSV to JSONツール をよく使っています。
ステップ1:データの投入(シーディング)
アルファベット順のソートモードを有効にするため、すべてのキーワードにスコア0を割り当てます。
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def seed_data(keywords):
with r.pipeline() as pipe:
for kw in keywords:
# キーワードを小文字化して前後の空白を削除し、スコア0で追加
pipe.zadd('search_suggest', {kw.lower().strip(): 0})
pipe.execute()
data = ["iPhone 15", "iPhone 15 Pro", "Samsung S23", "Macbook Air", "Macbook Pro", "iPad Mini"]
seed_data(data)
ステップ2:超高速な結果クエリ
以下の関数は、ユーザーがキーを入力した瞬間にサジェスト結果を即座に返します。
def get_suggestions(prefix, limit=5):
prefix = prefix.lower().strip()
if not prefix: return []
# [prefix] から [prefix + max_byte] までの検索範囲を設定
results = r.zrangebylex('search_suggest', f"[{prefix}", f"({prefix}\xff", start=0, num=limit)
return [res.decode('utf-8') for res in results]
# 実際のテスト
print(get_suggestions("ip"))
# 出力例: ['ipad mini', 'iphone 15', 'iphone 15 pro']
アップグレード:人気度を考慮する場合
優れた体験を提供するには、単なるA-Z順では不十分な場合があります。例えば、「i」と入力されたとき、「iPhone」がトレンドであれば「iPad」より前に表示されるべきです。しかし、ZRANGEBYLEX はスコアが0である必要があるため、検索回数を直接ここに保存することはできません。
最適な解決策: まず Redis から ZRANGEBYLEX を使って約50個の生の検索結果を取得します。次に、ZMSCORE コマンドを使用して、別のSorted Setからそれら50個の単語の人気スコアを取得し、バックエンド層で再ソートを行います。このハイブリッドなアプローチにより、レイテンシを極めて低く(通常15ms未満)保つことができます。
注意すべきメモリ使用量
Redisは非常に高速ですが、RAMを消費します。100万個のキーワード(1単語平均20文字)を含むSorted Setは、約150MB〜200MBのRAMを消費します。以下の対策を推奨します:
- Normalize: メモリの無駄を防ぐため、常に小文字化し、余分な空白を削除します。
- 長さの制限: データ構造を最適化するため、50文字以下のキーワードのみを保存します。
- TTL & Cleanup: 過去30日間検索されていないキーワードを定期的に削除します。
結論
Redis Sorted Setsの使用は、オートコンプリート機能においてSQLのLIKE句に代わる、最もコストパフォーマンスに優れた強力な方法です。Elasticsearchよりも軽量でありながら、従来のデータベースよりも何倍も高速です。Eコマースシステムやニュースアプリをスケールさせているなら、これは見逃せないテクニックです。皆さんのバグ修正が早く終わり、少ないコードで高い成果が得られることを願っています!

