検索オートコンプリートを10msで実現:Redis Sorted Sets活用の極意

Database tutorial - IT technology blog
Database tutorial - IT technology blog

なぜ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)テクニック

例えば、appleapplyballbattery というキーワードがあるとします。ユーザーが 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コマースシステムやニュースアプリをスケールさせているなら、これは見逃せないテクニックです。皆さんのバグ修正が早く終わり、少ないコードで高い成果が得られることを願っています!

Share: