Elasticsearch Bài học 50: Xây dựng tính năng Autocomplete (Suggest) hoàn hảo với Completion Suggester.
Chúc mừng bạn đã đi đến bài học số 50, cột mốc cuối cùng khép lại chuỗi series System Design đồ sộ về Elasticsearch.
Trên bất kỳ sàn E-commerce nào, tính năng đầu tiên khách hàng tương tác không phải là trang chi tiết sản phẩm hay giỏ hàng, mà là thanh tìm kiếm. Khi họ vừa gõ chữ "sữa r", hệ thống ngay lập tức thả xuống một danh sách (dropdown) gợi ý "sữa rửa mặt", "sữa rửa mặt cetaphil".
Tính năng này gọi là Autocomplete (Tự động hoàn thành) hay Search as you type. Yêu cầu kỹ thuật của nó cực kỳ khắc nghiệt: Thời gian phản hồi phải dưới 10 mili-giây (thậm chí là 1ms) để kịp hiển thị ngay khi ngón tay người dùng vừa nhấc khỏi bàn phím.
Để đạt được tốc độ vi diệu này, chúng ta không thể dùng Inverted Index thông thường. Elasticsearch cung cấp một vũ khí tối thượng: Completion Suggester.
1. Cỗ máy FST (Finite State Transducer) trong RAM
Ở Bài 32, chúng ta đã thử dùng prefix query để tìm tiền tố. Nhưng prefix quét trên Inverted Index (lưu ở ổ cứng). Nếu dùng nó để làm Autocomplete, khi 1.000 khách hàng cùng gõ phím liên tục, Disk I/O của server sẽ quá tải.
Completion Suggester hoạt động trên một cấu trúc dữ liệu hoàn toàn khác biệt mang tên FST (Finite State Transducer). FST là một đồ thị dạng cây siêu nén, được load thẳng vào RAM (Heap Memory). Khi bạn gõ chữ "C", con trỏ trong cây FST đi xuống nhánh C; gõ tiếp chữ "E", nó đi xuống nhánh E. Nó lướt trên đồ thị trong RAM với tốc độ ánh sáng mà không cần chạm đến ổ đĩa, nhờ vậy thời gian truy vấn gần như bằng không.
Tuy nhiên, sự đánh đổi là cấu trúc này cực kỳ ngốn RAM và bắt buộc phải được khai báo bằng một kiểu dữ liệu riêng biệt.
2. Định nghĩa Mapping với kiểu completion
Để dùng tính năng này, bạn không thể tái sử dụng trường text hay keyword có sẵn. Bạn phải tạo thêm một trường mang kiểu dữ liệu completion.
JSON
PUT hasaki_products_suggest
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"name_suggest": {
"type": "completion",
"analyzer": "vi_analyzer"
}
}
}
}
(Bạn hoàn toàn có thể kết hợp Vietnamese Analyzer từ Bài 21 vào đây để bộ gõ hiểu được từ ghép tiếng Việt).
3. Nghệ thuật Index dữ liệu gợi ý (Kèm trọng số)
Khác với các trường thông thường, khi bạn truyền dữ liệu vào trường completion, bạn không chỉ truyền text, mà còn truyền thêm các biến thể từ khóa (input) và Trọng số (weight). Trọng số quyết định từ khóa nào được đẩy lên vị trí Top 1 trong danh sách gợi ý rơi xuống.
JSON
PUT hasaki_products_suggest/_doc/1
{
"name": "Sữa rửa mặt Cetaphil 500ml",
"name_suggest": {
"input": ["sữa rửa mặt cetaphil", "cetaphil sữa rửa mặt", "srm cetaphil"],
"weight": 100 // Tồn kho nhiều, đẩy trọng số lên 100
}
}
PUT hasaki_products_suggest/_doc/2
{
"name": "Sữa rửa mặt L'Oreal",
"name_suggest": {
"input": ["sữa rửa mặt loreal"],
"weight": 50
}
}
Bằng cách cung cấp nhiều input đa dạng, code Backend của bạn đã giúp Elasticsearch bao phủ mọi thói quen gõ phím của khách hàng.
4. Truy vấn siêu tốc với khối suggest
Khi khách hàng gõ từ "sữa", API Golang/PHP của bạn sẽ không gửi khối query quen thuộc, mà gọi vào khối suggest:
JSON
GET hasaki_products_suggest/_search
{
"size": 0,
"suggest": {
"goi_y_san_pham": {
"prefix": "sữa",
"completion": {
"field": "name_suggest",
"size": 5,
"fuzzy": {
"fuzziness": "AUTO"
}
}
}
}
}
Sức mạnh của cấu trúc này:
-
size: 0ở đầu request: Bỏ qua hoàn toàn quá trình lấy Document gốc để tiết kiệm mạng. -
fuzzy: Cho phép gợi ý mượt mà ngay cả khi khách gõ sai chính tả (Ví dụ: Gõ "sũa r" vẫn gợi ý "sữa rửa mặt"). Nó kết hợp hoàn hảo tính năng của Bài 31 với tốc độ của FST.
Kết quả trả về sẽ liệt kê ngay 5 options có điểm score (dựa theo weight) cao nhất, sẵn sàng để Frontend vẽ ra màn hình.
5. Cạm bẫy của Completion Suggester và Giải pháp Edge N-gram
Dù tốc độ là vô địch, Completion Suggester có một điểm yếu chí mạng về mặt thuật toán đồ thị FST: Nó chỉ có thể khớp từ đầu chuỗi (Prefix matching). Nếu bạn lưu input là "Sữa rửa mặt Cetaphil", khi khách gõ "Sữa", nó tìm thấy. Nhưng nếu khách gõ chữ "Ceta", nó sẽ không bao giờ tìm thấy, vì "Ceta" nằm ở giữa đồ thị, không phải điểm bắt đầu.
Để giải quyết bài toán "Gõ một từ ở giữa cũng ra gợi ý", Backend Engineer thường dùng một tuyệt chiêu khác thay thế: Edge N-gram Tokenizer.
Cách hoạt động của Edge N-gram: Thay vì dùng FST, bạn ép Analyzer chủ động băm từ "Cetaphil" thành các token tiền tố ngay lúc Index: ["c", "ce", "cet", "ceta", "cetap", ...]. Sau đó, bạn lưu vào Inverted Index chuẩn và dùng lệnh match thông thường để tìm kiếm. Cách này hy sinh ổ cứng (vì lượng token sinh ra cực lớn) và làm chậm quá trình ghi (Indexing), nhưng bù lại, nó linh hoạt vô cực lúc tìm kiếm. Khách gõ chữ gì ở đâu cũng có thể gợi ý được.
Tùy thuộc vào quy mô RAM của Server và trải nghiệm UI/UX mong muốn, bạn có thể linh hoạt chọn Completion Suggester (Tối ưu RAM, siêu nhanh) hoặc Edge N-gram (Tốn ổ cứng, linh hoạt hơn) để áp dụng vào hệ thống thực tế.
Với bài học số 50 này, bạn đã chính thức nắm trong tay toàn bộ kho vũ khí của một Data Engineer / Backend Architect thực thụ làm việc với Elasticsearch. Từ nền tảng phân tán lõi, thiết kế Mapping, bóc tách luồng ghi/đọc, tối ưu hóa hệ sinh thái CDC (Debezium, Kafka), quản trị ILM, cho đến những kỹ thuật Query và Aggregations đỉnh cao nhất để thao túng hàng chục triệu bản ghi. Chúc bạn có những chặng đường phát triển rực rỡ và vững chắc trên các hệ thống triệu đô tại Hasaki cũng như các dự án cơ sở hạ tầng lớn trong tương lai.
All rights reserved