Ràng buộc định hình thiết kế: gợi ý phải trả về trong khoảng 50-100ms vì nó chạy sau mỗi phím gõ, và người Việt gõ không dấu ("dien thoai") nhưng dữ liệu lưu có dấu ("điện thoại").
Chuẩn hoá là phần lõi. Mỗi truy vấn và mỗi cụm từ trong từ điển đều được lược về dạng chuẩn trước khi so khớp:
const fold = (s) =>
s.normalize('NFD') // split base char + diacritic
.replace(/[\u0300-\u036f]/g, '') // drop combining marks
.replace(/đ/g, 'd').replace(/Đ/g, 'D') // NFD does not decompose d-stroke
.toLowerCase()
.trim()Lưu ý bẫy kinh điển: đ/Đ không phải chữ có dấu phụ ghép nên NFD không tách được — phải thay thủ công. Lưu song song term (hiển thị có dấu) và term_folded (dùng để khớp).
Cấu trúc dữ liệu: với vài triệu cụm từ, một chỉ mục edge n-gram (Elasticsearch completion suggester hoặc edge_ngram analyzer) là lựa chọn vận hành tốt nhất. Tự dựng trie trong bộ nhớ chỉ hợp khi từ điển nhỏ và cố định — được độ trễ thấp nhưng phải tự lo cập nhật, phân phối và khởi động lại.
Xếp hạng: không chỉ theo tiền tố. Điểm = tần suất truy vấn trong 7 ngày + tỉ lệ click + tăng trưởng gần đây. Truy vấn hot lưu sẵn top-k trong Redis theo tiền tố → hầu hết phím gõ được phục vụ từ cache.
Điểm nghẽn:
- Số request bằng số phím gõ → debounce 100-150ms ở client, huỷ request cũ; giảm tải hơn mọi tối ưu phía server.
- Tiền tố 1-2 ký tự nóng nhất và ít thay đổi → cache TTL dài; tiền tố dài thì cache ngắn.
- Dữ liệu truy vấn phải lọc: bỏ cụm nhạy cảm, bỏ cụm tần suất thấp để tránh lộ truy vấn cá nhân.
Đánh đổi: cập nhật từ điển theo lô (hằng giờ) đủ tốt cho gợi ý và rẻ hơn nhiều so với cập nhật realtime; ngoại lệ là từ khoá đang bùng nổ theo sự kiện — xử lý bằng một danh sách trending nhỏ trộn vào kết quả, không phải bằng cách rebuild toàn bộ chỉ mục.