Tại sao SQL LIKE là “cơn ác mộng” hiệu năng?
Khi làm tính năng tìm kiếm, nhiều dev thường chọn giải pháp nhanh nhất là dùng câu lệnh LIKE '%keyword%'. Cách này ổn với bảng vài nghìn dòng. Tuy nhiên, khi database chạm mốc 1 triệu bản ghi, một câu query SQL có thể mất từ 500ms đến 2s để phản hồi vì phải quét toàn bộ ổ đĩa (Full Table Scan).
Trong khi đó, tiêu chuẩn cho Autocomplete hiện nay là dưới 10ms. Để đạt con số này, chúng ta cần đưa dữ liệu lên RAM. Redis với cấu trúc Sorted Sets (ZSET) không chỉ là bộ nhớ đệm đơn thuần, mà còn là vũ khí hạng nặng giúp lọc tiền tố trong chớp mắt nhờ khả năng sắp xếp theo thứ tự từ điển (lexicographical).
Sức mạnh của Lexicographical Ordering
Bình thường, Sorted Sets dùng score để xếp hạng. Nhưng đây là bí mật: nếu bạn để tất cả score bằng 0, Redis sẽ tự động sắp xếp các phần tử theo bảng chữ cái A-Z. Lúc này, Sorted Set hoạt động y hệt một cuốn từ điển giấy được sắp xếp ngăn nắp.
Lệnh ZRANGEBYLEX cho phép chúng ta nhảy ngay đến trang có từ khóa cần tìm thay vì lật từng trang một. Đây chính là mấu chốt để đạt throughput cực cao mà không làm CPU quá tải.
Kỹ thuật Prefix Matching thực tế
Hãy tưởng tượng bạn có các từ khóa: apple, apply, ball, battery. Khi người dùng gõ app, mục tiêu là lấy ra mọi thứ nằm giữa “app” và từ tiếp theo trong từ điển.
Câu lệnh Redis thực thi sẽ như sau:
ZRANGEBYLEX search_zset "[app" "(app\xff"
[app: Điểm bắt đầu (bao gồm cả chuỗi “app”).(app\xff: Điểm kết thúc. Ký tự\xfflà byte cao nhất trong bảng mã, giúp bao phủ toàn bộ các biến thể như “apple”, “apply” hay “application”.
Triển khai với Python
Để bắt tay vào làm, bạn cần nạp dữ liệu vào Redis. Nếu đang có sẵn file CSV sản phẩm khổng lồ, bạn nên convert sang JSON trước để dễ xử lý. Mình thường dùng tool CSV to JSON tại toolcraft.app vì nó xử lý client-side, không lo lộ data nội bộ.
Bước 1: Nạp dữ liệu (Seeding)
Chúng ta sẽ gán điểm số bằng 0 cho tất cả keyword để kích hoạt chế độ sắp xếp theo chữ cái.
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def seed_data(keywords):
with r.pipeline() as pipe:
for kw in keywords:
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)
Bước 2: Query kết quả siêu tốc
Hàm dưới đây sẽ trả về kết quả gợi ý ngay lập tức khi người dùng gõ phím.
def get_suggestions(prefix, limit=5):
prefix = prefix.lower().strip()
if not prefix: return []
# Thiết lập vùng tìm kiếm từ [prefix] đến [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]
# Test thực tế
print(get_suggestions("ip"))
# Output: ['ipad mini', 'iphone 15', 'iphone 15 pro']
Nâng cấp: Khi độ phổ biến lên ngôi
Chỉ xếp theo A-Z là chưa đủ cho một trải nghiệm xịn. Ví dụ: khi gõ “i”, “iPhone” nên đứng trước “iPad” nếu nó đang là xu hướng. Tuy nhiên, ZRANGEBYLEX yêu cầu score phải bằng 0, nên chúng ta không thể lưu lượt search trực tiếp vào đây.
Giải pháp tối ưu: Hãy dùng Redis lấy ra khoảng 50 kết quả thô bằng ZRANGEBYLEX. Sau đó, dùng lệnh ZMSCORE để lấy điểm số phổ biến của 50 từ đó từ một Sorted Set khác và thực hiện sort lại ở tầng Backend. Cách tiếp cận Hybrid này giữ cho latency vẫn ở mức cực thấp (thường < 15ms).
Con số về bộ nhớ cần lưu ý
Redis rất nhanh nhưng ngốn RAM. Một Sorted Set chứa 1 triệu từ khóa (trung bình 20 ký tự mỗi từ) sẽ tiêu tốn khoảng 150MB – 200MB RAM. Bạn nên:
- Normalize: Luôn lowercase và xóa khoảng trắng thừa để tránh lãng phí bộ nhớ.
- Giới hạn độ dài: Chỉ lưu từ khóa dưới 50 ký tự để tối ưu cấu trúc dữ liệu.
- TTL & Cleanup: Xóa định kỳ các từ khóa không có lượt search trong 30 ngày qua.
Kết luận
Dùng Redis Sorted Sets là phương án “ngon – bổ – rẻ” nhất để thay thế SQL LIKE cho tính năng Autocomplete. Nó nhẹ hơn Elasticsearch nhưng nhanh hơn gấp nhiều lần database truyền thống. Nếu bạn đang scale hệ thống E-commerce hoặc App tin tức, đây là kỹ thuật không thể bỏ qua. Chúc anh em fix bug nhanh, code ít nhưng hiệu quả cao!

