Người dùng gõ sai, nhưng vẫn muốn máy hiểu đúng
Có một cảnh rất quen thuộc.
Mười một giờ bốn mươi lăm phút trưa.
Bạn đang đói.
Đồng nghiệp bắt đầu đứng lên hỏi:
Ăn gì?
Cả phòng im lặng.
Một người nói cơm.
Một người nói bún.
Một người vừa giảm cân được hai tiếng nên đề nghị salad.
Cuối cùng, bạn mở ứng dụng giao đồ ăn và gõ:
1ga ran pho mai
Không dấu.
Không viết hoa.
Thiếu chữ.
Có thể còn sai chính tả.
Nhưng trong đầu bạn, yêu cầu rất rõ:
Tôi muốn tìm gà rán có phô mai, giao được tới chỗ tôi, quán đang mở, giá đừng quá vô lý và tốt nhất là tới trước khi cuộc họp đầu giờ chiều bắt đầu.
Ô tìm kiếm chỉ nhận được bốn từ.
Phía sau bốn từ đó là cả một cục nhu cầu rất lớn.
Nếu hệ thống chỉ tìm những nhà hàng có đúng chuỗi ký tự ga ran pho mai, nó có thể bỏ qua:
- Gà sốt phô mai
- Chicken cheese
- Gà cay phủ cheese
- Korean fried chicken
- Gà rán mozzarella
- Combo gà lắc phô mai
Người dùng viết chưa đúng.
Tên món cũng không giống hoàn toàn.
Nhưng ý nghĩa lại đứng rất gần nhau.
Đó là lúc bài toán search không còn là:
Hai chuỗi chữ có giống nhau không?
Nó trở thành:
Hai ý định có đang nói về cùng một thứ không?
Đây cũng là điều khiến mình tâm đắc khi đọc bài “Evolution and Scale of Uber’s Delivery Search Platform” của Uber Engineering.
Uber không chỉ kể chuyện đưa thêm một mô hình embedding vào ô tìm kiếm.
Họ kể một câu chuyện khó hơn nhiều:
Làm sao để một hệ thống hiểu được ý người dùng bằng semantic search, tìm trong kho dữ liệu hàng tỷ ứng viên, trả lời đủ nhanh, chi phí không nổ tung và vẫn có thể rollback nếu model mới đột nhiên nổi hứng hiểu “apple” thành điện thoại thay vì trái cây?
Search truyền thống giống bác thủ kho chỉ biết nhìn nhãn
Cách search quen thuộc nhất là lexical search, tức tìm kiếm dựa trên từ ngữ.
Người dùng gõ:
1bún bò
Hệ thống tìm những tài liệu có chứa:
1bún bò
Rất hợp lý.
Nhanh.
Dễ giải thích.
Nếu chữ trên tờ giấy giống chữ người dùng gõ vào, trả kết quả về.
Có thể hình dung lexical search như một bác thủ kho.
Bạn hỏi:
Chú tìm giúp con thùng ghi “nước ngọt”.
Bác đi dọc kho.
Nhìn từng nhãn.
Thấy thùng nào có chữ “nước ngọt” thì lấy ra.
Vấn đề bắt đầu xuất hiện khi thùng hàng ghi:
1soft drink
Hoặc:
1soda
Hoặc người dùng gõ:
1nuoc ngot ko duong
Bác thủ kho có thể đứng nhìn hai thứ mang cùng một ý nghĩa nhưng không nhận ra chúng liên quan.
Search bằng từ khóa làm rất tốt khi người dùng và dữ liệu dùng cùng một cách viết.
Ngoài đời, điều đó không xảy ra thường xuyên như chúng ta mong muốn.
Uber chỉ ra rằng truy vấn thực tế chứa đủ loại rắc rối:
- Từ đồng nghĩa như
sodavàsoft drink - Lỗi chính tả như
mozzarela - Viết tắt như
gf pizza - Trộn nhiều ngôn ngữ
- Từ đa nghĩa như
apple - Cùng một nhu cầu nhưng mỗi khu vực gọi bằng một tên khác nhau
Lexical search nhìn thấy chữ.
Nó không tự nhiên nhìn thấy ý nghĩa. (Uber)
Một từ có thể đội nhiều cái mũ
Ngôn ngữ không phải bảng mã vạch.
Một từ không phải lúc nào cũng chỉ tới đúng một vật.
Ví dụ người dùng gõ:
1pan
Trong tiếng Anh, pan có thể là cái chảo.
Trong tiếng Tây Ban Nha, pan là bánh mì.
Hệ thống search toàn cầu không thể nhìn thấy ba ký tự rồi vui vẻ kết luận:
Đây chắc chắn là dụng cụ nhà bếp.
Nó phải nhìn thêm:
- Người dùng đang ở quốc gia nào?
- Giao diện đang dùng ngôn ngữ gì?
- Người đó đang tìm trong mục nhà hàng hay bán lẻ?
- Những sản phẩm nào đang có thể giao tại khu vực đó?
- Hành vi trước đó là xem bánh mì hay xem đồ gia dụng?
Search không còn là công việc dò từ trong từ điển.
Nó giống một người phiên dịch đứng giữa một khu chợ đông người.
Mỗi người nói một giọng.
Một từ mang nhiều nghĩa.
Âm thanh chen vào nhau.
Người phiên dịch không chỉ cần nghe đúng chữ.
Họ phải hiểu người nói đang đứng ở đâu và đang muốn mua cái gì.
Semantic search: Biến câu chữ thành tọa độ
Semantic search giải bài toán theo một hướng khác.
Thay vì giữ câu truy vấn và sản phẩm dưới dạng chữ, hệ thống biến chúng thành các vector.
Ví dụ:
1"gà rán phô mai"
được biến thành:
1[0.12, -0.43, 0.76, ...]
Một món có tên:
1"Korean cheese fried chicken"
cũng được biến thành một vector khác:
1[0.15, -0.39, 0.72, ...]
Các con số cụ thể không quan trọng với con người.
Điều quan trọng là trong không gian vector, hai thứ có ý nghĩa gần nhau sẽ được đặt gần nhau.
Ta có thể dùng cosine similarity để đo mức độ gần:
$$ similarity(q, d) = \frac{q \cdot d}{\lVert q \rVert \lVert d \rVert} $$
Trong đó:
- $q$ là vector của câu người dùng tìm kiếm.
- $d$ là vector của nhà hàng, món ăn hoặc sản phẩm.
- Giá trị càng cao thì hai vector càng có xu hướng gần nhau.
Hãy tưởng tượng một thành phố rất lạ.
Khu bán phở nằm gần khu bán bún bò.
Pizza nằm gần pasta.
Đồ ăn chay nằm gần salad.
Điện thoại nằm xa bánh mì.
Một câu truy vấn sau khi được biến thành tọa độ sẽ được thả vào thành phố đó.
Hệ thống chỉ cần tìm những ngôi nhà gần nó nhất.
Đó là trực giác của vector search.
Nghe thì có vẻ đơn giản.
Biến chữ thành vector.
Tìm vector gần nhất.
Xong thuật toán, quá dễ.
Nhưng khi thành phố có hàng tỷ căn nhà, câu chuyện bắt đầu có mùi khói từ phòng máy chủ.
Search giao đồ ăn không sống trong thế giới của riêng ngôn ngữ
Giả sử semantic model hiểu rất chính xác rằng người dùng muốn sushi.
Nó tìm được một nhà hàng sushi tuyệt vời.
Điểm embedding cao.
Đánh giá tốt.
Menu đẹp.
Chỉ có một vấn đề nhỏ.
Quán nằm cách người dùng 1.200 kilomet.
Mô hình không sai về ngôn ngữ.
Nhưng bữa trưa vẫn không tới.
Search giao đồ ăn khác với search tài liệu trên Internet ở chỗ nó bị trói chặt vào vật lý.
Một kết quả tốt còn phải thỏa mãn:
- Quán nằm trong khu vực có thể giao
- Quán đang mở cửa
- Sản phẩm còn bán
- Hình thức giao hàng phù hợp
- Khu vực của người dùng được phục vụ
- Loại tài liệu đúng với truy vấn
- Thời gian và chi phí giao hàng chấp nhận được
Một vector có thể nói hai vật rất giống nhau.
Nhưng nó không thể làm đường phố ngắn lại.
Nó không thể attention cho tài xế bay xuyên qua kẹt xe.
Đây là điểm mình thấy search trong delivery thú vị hơn nhiều bài demo semantic search.
Ý nghĩa chỉ là vòng đầu tiên.
Sau đó là địa lý, tồn kho, vận hành và giới hạn của thế giới thật.
Two-tower: Hai người phiên dịch gặp nhau giữa cây cầu
Uber sử dụng kiến trúc two-tower cho mô hình semantic search.
Một tower xử lý truy vấn.
Tower còn lại xử lý tài liệu.
Có thể hình dung như sau:
1Người dùng gõ câu tìm kiếm
2 │
3 ▼
4 Query Tower
5 │
6 ▼
7 Query Embedding
8 │
9 │ tìm vector gần nhất
10 │
11 Document Embeddings
12 ▲
13 │
14 Document Tower
15 ▲
16 │
17Nhà hàng / món ăn / sản phẩm
Query Tower học cách hiểu câu người dùng gõ.
Document Tower học cách hiểu nội dung phía bên kia:
- Nhà hàng
- Món ăn
- Sản phẩm tạp hóa
- Sản phẩm bán lẻ
Hai tower giống hai người phiên dịch đứng ở hai đầu một cây cầu.
Một người dịch ngôn ngữ của khách hàng.
Người còn lại dịch ngôn ngữ của catalog.
Mục tiêu huấn luyện là khiến hai bản dịch gặp nhau ở giữa.
Nếu người dùng tìm nước ngọt không đường, vector truy vấn cần đứng gần:
- Coke Zero
- Pepsi không calo
- Soda zero sugar
Và đứng xa:
- Nước rửa chén
- Bột giặt
- Cơm gà
Uber dùng Qwen làm backbone cho cả hai tower, sau đó fine-tune bằng dữ liệu nội bộ của Uber Eats để không gian embedding thích nghi với bài toán giao đồ ăn, tạp hóa và bán lẻ. Một mô hình embedding chung hiện được dùng cho nhiều vertical và nhiều thị trường, tận dụng khả năng đa ngôn ngữ và kiến thức nền của mô hình ngôn ngữ lớn. (Uber)
Vì sao không tính mọi thứ ngay khi người dùng tìm kiếm?
Giả sử hệ thống có hàng tỷ tài liệu.
Mỗi lần người dùng gõ một câu, ta làm như sau:
- Chạy Query Tower để tạo vector truy vấn.
- Chạy Document Tower cho hàng tỷ sản phẩm.
- So sánh toàn bộ.
- Trả kết quả.
Nghe rất trong sáng.
Máy chủ cũng sẽ sáng.
Sáng rực lên vì nóng.
Tài liệu thay đổi chậm hơn truy vấn.
Tên một món ăn có thể giữ nguyên trong nhiều giờ hoặc nhiều ngày.
Trong khi truy vấn mới xuất hiện liên tục.
Vì vậy Uber tách hai công việc:
- Query embedding được tính online theo thời gian thực.
- Document embedding được tính trước bằng batch job offline.
Khi người dùng search, hệ thống không cần đọc lại toàn bộ catalog bằng model.
Nó chỉ tính vector của câu truy vấn rồi tìm trong kho vector đã được chuẩn bị sẵn.
Đây là một trong những lợi ích lớn của two-tower.
Hai phía được tách rời.
Một bên chạy nhanh theo request.
Một bên âm thầm chuẩn bị từ trước.
Giống một nhà hàng không đợi khách gọi món rồi mới đi trồng rau.
Model lớn chỉ là phần đầu của dây chuyền
Uber phát triển mô hình bằng PyTorch và Hugging Face Transformers, dùng Ray để điều phối training phân tán, kết hợp Distributed Data Parallel với DeepSpeed ZeRO-3 để chia optimizer state, gradient và model parameter giữa nhiều thiết bị.
Mixed precision và gradient accumulation giúp họ huấn luyện trên hàng trăm triệu điểm dữ liệu. Sau mỗi lần train thành công, hệ thống tạo các mã phiên bản riêng cho Query Model, Document Model và một ID chung đại diện cho cặp two-tower nhằm bảo đảm khả năng truy vết và tái tạo. (Uber)
Phần này nghe giống danh sách công nghệ.
Nhưng ý nghĩa phía sau rất đời thường.
Một model trong production phải có giấy khai sinh.
Phải biết:
- Nó được train từ dữ liệu nào.
- Query Tower phiên bản bao nhiêu.
- Document Tower phiên bản bao nhiêu.
- Hai tower có phải một cặp không.
- Index đang chứa embedding do model nào tạo ra.
- Khi có lỗi thì quay lại phiên bản nào.
Nếu không có versioning, việc deploy model giống thay một nửa ổ khóa nhưng quên thay chìa khóa.
Đến tối, cả đội đứng ngoài cửa và tranh luận xem ai đã làm chuyện đó.
InfoNCE: Kéo món đúng lại gần, đẩy món sai ra xa
Để huấn luyện không gian embedding, Uber sử dụng loss dựa trên InfoNCE kết hợp Matryoshka Representation Learning.
Ở mức trực giác, InfoNCE làm một việc khá dễ hiểu.
Với một truy vấn:
1pizza hải sản
Mô hình cần:
- Kéo kết quả đúng như pizza hải sản lại gần.
- Đẩy những kết quả không liên quan ra xa.
Một dạng công thức đơn giản có thể viết như sau:
$$ \mathcal{L}
-\log \frac{ \exp(sim(q,d^+)/\tau) }{ \exp(sim(q,d^+)/\tau) + \sum_{i=1}^{N}\exp(sim(q,d_i^-)/\tau) } $$
Trong đó:
- $d^+$ là tài liệu phù hợp.
- $d_i^-$ là những tài liệu không phù hợp.
- $\tau$ là tham số temperature.
- $sim$ là hàm đo độ tương đồng.
Nói theo cách lúa nhất có thể:
Ta đưa cho model một tô phở thật và mười món không phải phở.
Mỗi lần model chỉ nhầm ly trà sữa là phở, ta gõ nhẹ vào đầu nó bằng gradient.
Lặp lại hàng trăm triệu lần.
Cuối cùng, model bắt đầu biết đặt đúng thứ gần nhau hơn.
InfoNCE là một mục tiêu phổ biến trong contrastive learning, nơi biểu diễn được học bằng cách kéo các cặp liên quan lại gần và tách các cặp không liên quan. (arXiv)
Matryoshka embedding: Một con búp bê chứa nhiều con búp bê nhỏ hơn
Embedding thường có kích thước cố định.
Ví dụ:
11536 chiều
Vector càng dài thì có thể chứa càng nhiều chi tiết.
Nhưng nó cũng kéo theo:
- Tốn RAM hơn
- Tốn ổ đĩa hơn
- Tốn băng thông hơn
- Tốn CPU hơn khi tính khoảng cách
- Index lớn hơn
- Truy vấn chậm hơn
Nếu dùng vector ngắn, hệ thống rẻ và nhanh hơn.
Nhưng có thể mất chất lượng.
Thông thường, muốn có model 256 chiều và model 1.536 chiều, ta có thể phải train hai model khác nhau.
Matryoshka Representation Learning đưa ra một ý tưởng rất đẹp.
Tên Matryoshka đến từ loại búp bê Nga.
Mở một con lớn ra.
Bên trong có một con nhỏ.
Mở tiếp.
Lại có một con nhỏ hơn nữa.
Embedding Matryoshka cũng vậy.
Một vector lớn được sắp xếp để phần đầu của nó vẫn có thể hoạt động như một embedding ngắn có ý nghĩa:
1[128 chiều]
2 nằm trong
3[256 chiều]
4 nằm trong
5[512 chiều]
6 nằm trong
7[1536 chiều]
Phần ngắn giữ thông tin thô và quan trọng.
Phần dài bổ sung thêm chi tiết.
Một model có thể phục vụ nhiều mức ngân sách khác nhau mà không cần train riêng từng embedding size. Ý tưởng MRL ban đầu được thiết kế để học biểu diễn từ thô đến tinh, cho phép cắt vector theo nhiều kích thước tùy yêu cầu downstream. (arXiv)
Uber huấn luyện các kích thước:
1128, 256, 512, 768, 1024, 1280, 1536
với trọng số ngang nhau. Trong đánh giá offline của họ, vector 256 chiều chỉ giảm chất lượng dưới 0,3% ở tiếng Anh và tiếng Tây Ban Nha so với embedding đầy đủ, đồng thời giúp giảm chi phí lưu trữ gần 50%. (Uber)
Đây là đoạn mình rất thích.
Không phải request nào cũng cần mặc áo giáp 1.536 chiều.
Có lúc một chiếc áo mỏng 256 chiều đã đủ đi chợ.
Hàng tỷ ứng viên: Không thể đi gõ cửa từng nhà
Sau khi có Query Embedding và hàng tỷ Document Embedding, ta vẫn còn một câu hỏi:
Làm sao tìm những vector gần nhất đủ nhanh?
Cách chính xác nhất là so sánh truy vấn với từng tài liệu.
Giả sử có một tỷ vector.
Mỗi request phải tính một tỷ khoảng cách.
Một người dùng gõ thêm chữ.
Lại một tỷ lần.
Mười nghìn người cùng search.
Phòng máy chủ sẽ bắt đầu cầu nguyện.
Vì vậy các hệ thống lớn thường dùng Approximate Nearest Neighbor, viết tắt là ANN.
Từ khóa quan trọng là approximate: xấp xỉ.
Hệ thống không cam kết lật từng viên đá trong thành phố để tìm ngôi nhà gần nhất tuyệt đối.
Nó tìm một nhóm kết quả rất gần, nhưng nhanh hơn nhiều.
Uber dùng HNSW graph trong hệ thống index.
HNSW tổ chức các vector thành một mạng lưới nhiều tầng.
Có thể hình dung như tìm đường trong thành phố:
- Tầng trên là đường cao tốc, ít điểm nhưng đi rất xa.
- Tầng giữa là đường lớn.
- Tầng dưới là đường nhỏ dẫn tới từng khu vực.
Ta không đi qua từng căn nhà.
Ta chạy trên cao tốc tới khu vực phù hợp, rẽ xuống đường lớn, rồi mới tìm trong những ngõ gần nhất.
HNSW là một thuật toán ANN dựa trên đồ thị phân tầng, được thiết kế để tìm láng giềng gần trong không gian nhiều chiều với chi phí thấp hơn việc quét toàn bộ tập dữ liệu. (arXiv)
Đừng để vector search tìm trong cả Trái Đất
Dù HNSW nhanh, không có nghĩa ta nên ném toàn bộ dữ liệu vào rồi bảo nó tự lo.
Nếu người dùng đang ở TP.HCM, không cần search món ăn tại Paris.
Nếu người dùng đang tìm nhà hàng, không cần kéo cả catalog bột giặt vào cuộc thi.
Uber áp dụng các bộ lọc trước khi chạy ANN, gồm:
- Hexagon địa lý
city_iddoc_typefulfillment_types
Những điều kiện này thu nhỏ tập ứng viên trước khi vector search bắt đầu.
Sau khi ANN lấy top-k của từng vertical, hệ thống có thể chạy thêm một bước micro re-ranking bằng mạng neural nhỏ trước khi đưa kết quả sang các ranker phía sau. (Uber)
Nói theo cách nhà quê nhất:
Trước khi tìm con gà giống ảnh, hãy vào đúng chuồng gà.
Đừng chạy sang chuồng vịt, ao cá và nhà hàng xóm rồi than rằng vector search tốn CPU.
Đây là một bài học rất quan trọng.
Semantic search không thay thế filter.
Embedding không thay thế business rule.
Vector rất giỏi đo ý nghĩa.
Nó không nên bị bắt làm công việc của câu WHERE.
K không phải càng lớn càng tốt
Trong ANN search, tham số k ở mức shard ảnh hưởng tới số ứng viên được lấy ra.
K lớn hơn thường tăng khả năng không bỏ sót kết quả tốt.
Nhưng đổi lại:
- Tốn CPU hơn
- Tăng latency
- Tăng lượng dữ liệu cần merge
- Tăng chi phí downstream
Nhiều người có xu hướng chọn số lớn để yên tâm.
Giống đi chợ mua một bó hành nhưng thuê nguyên chiếc xe tải vì sợ thiếu chỗ.
Uber thử nghiệm việc giảm shard-level k từ 1.200 xuống khoảng 200. Kết quả được công bố là latency giảm 34%, CPU giảm 17%, trong khi tác động lên recall không đáng kể. (Uber)
Điều thú vị không phải là con số 200.
Không nên sao chép nó về hệ thống của mình rồi coi như chân lý.
Điều đáng học là Uber nhìn vào phân phối dữ liệu và đo trade-off.
Hệ thống search tốt không phải hệ thống lấy nhiều nhất.
Nó là hệ thống biết lấy vừa đủ.
Quantization: Không cần mang theo cả tủ quần áo để đi ăn trưa
Vector float32 lưu mỗi chiều bằng 32 bit.
Khi có hàng tỷ vector và hàng trăm hoặc hàng nghìn chiều, số byte bắt đầu biến thành một con quái vật.
Quantization nén vector xuống dạng nhỏ hơn.
Thay vì giữ mọi con số với độ chi tiết rất cao, ta chấp nhận một mức xấp xỉ để tiết kiệm:
- Bộ nhớ
- Băng thông
- Cache
- Chi phí tính toán
- Thời gian truy vấn
Uber thử scalar quantization dạng int7. Theo kết quả họ công bố, cách này làm latency giảm hơn một nửa so với float32, trong khi recall vẫn trên 0,95. (Uber)
Nghe giống việc chụp ảnh.
Ảnh RAW giữ rất nhiều chi tiết.
Tệp lớn.
Xử lý nặng.
Ảnh nén mất một ít thông tin.
Nhưng nếu người dùng chỉ xem trên màn hình điện thoại nhỏ, phần mất đi có thể không đáng kể.
Không phải dữ liệu càng chính xác tuyệt đối thì hệ thống càng tốt.
Trong production, độ chính xác luôn ngồi cùng bàn với chi phí và thời gian.
Ba người này thường cãi nhau.
Tam giác không bao giờ ngủ: Chất lượng, tốc độ và tiền
Khi xây semantic search ở quy mô lớn, ta thường đứng giữa ba thứ:
1Chất lượng
2 /\
3 / \
4 /____\
5Tốc độ — Chi phí
Muốn tăng chất lượng:
- Vector dài hơn
- K lớn hơn
- Model lớn hơn
- Nhiều ứng viên hơn
- Re-ranking sâu hơn
Nhưng tất cả đều có giá.
Muốn giảm latency:
- Giảm chiều embedding
- Giảm k
- Quantize
- Filter mạnh hơn
- Dùng model nhẹ hơn
Nhưng đi quá xa có thể mất recall.
Điều Uber làm không phải tìm một cấu hình hoàn hảo cho mọi nơi.
Họ dùng MRL để có nhiều kích thước embedding từ cùng một model, điều chỉnh k, thử quantization và duy trì index riêng cho mảng nhà hàng so với grocery/retail. (Uber)
Đây mới là engineering.
Không phải hỏi:
Công nghệ nào mạnh nhất?
Mà hỏi:
Trong ngân sách, latency và chất lượng hiện tại, cấu hình nào đủ tốt để hàng triệu người dùng không phải chờ tô phở quay vòng trên màn hình?
Document embedding cũng cần một cái kho tử tế
Với catalog rất lớn, Uber không tính embedding cho mọi bản sao dữ liệu một cách mù quáng.
Họ tạo embedding ở cấp feature, chẳng hạn store hoặc item, rồi join các embedding đó trở lại catalog đầy đủ gồm hàng tỷ ứng viên trước khi index.
Embedding được lưu trong feature store ổn định và khóa bằng entity ID, để có thể tái sử dụng cho retrieval lẫn ranking. (Uber)
Đây là chi tiết dễ bị bỏ qua.
Model tạo ra vector.
Nhưng vector cần:
- Được lưu ở đâu
- Gắn với entity nào
- Phiên bản nào
- Sinh bởi model nào
- Có còn hợp lệ không
- Catalog đã thay đổi chưa
- Index đã nhận đủ chưa
Một dự án vector search không kết thúc sau lệnh:
1embedding = model.encode(text)
Nó chỉ vừa bắt đầu.
Từ đó trở đi là câu chuyện của data pipeline, versioning, storage, indexing và vận hành.
Vector không tự biết đường tới production.
Model và index là một cặp vợ chồng không được nhận nhầm nhau
Giả sử Query Model phiên bản mới tạo embedding theo một không gian khác.
Nhưng production index vẫn chứa Document Embedding của phiên bản cũ.
Cả hai đều là vector 256 chiều.
Không lỗi kiểu dữ liệu.
Không crash.
Request vẫn trả HTTP 200.
Chỉ có kết quả bắt đầu ngơ ngác.
Giống một người nói tiếng Việt rất chuẩn với một người nghe tiếng Thái rất chăm chú.
Hai bên đều hoạt động tốt.
Chỉ không hiểu nhau.
Đây là loại lỗi nguy hiểm.
Hệ thống không chết.
Nó chỉ âm thầm trở nên ngu hơn.
Uber làm mới model và index theo một workflow đồng bộ hai tuần một lần. Mỗi lần chạy sẽ chuẩn bị model mục tiêu, tạo lại document embedding và xây index Lucene Plus mới mà chưa thay đổi đường đọc đang phục vụ production. (Uber)
Từ khóa quan trọng là lockstep.
Model và index phải bước cùng nhịp.
Không ai được đi trước rồi quên người còn lại ở bến xe.
Blue-green deployment: Hai căn bếp, một căn đang nấu
Để cập nhật an toàn, Uber dùng mô hình blue-green ở cấp cột embedding.
Mỗi index có hai cột cố định:
1embedding_blue
2embedding_green
Một cột đang active.
Cột còn lại chờ cập nhật.
Ví dụ:
1Blue = model đang phục vụ
2Green = model mới
Hệ thống đưa embedding mới vào Green.
Kiểm tra.
Deploy.
Sau đó chỉ cần đổi cấu hình để chuyển traffic từ Blue sang Green.
Nếu có chuyện, quay lại Blue.
Ý tưởng giống nhà hàng có hai căn bếp.
Bếp Blue đang phục vụ khách.
Bếp Green được lắp thiết bị mới, thử món và kiểm tra gas.
Khi Green sẵn sàng, đổi bảng phân công.
Nếu Green cháy chảo, khách quay lại Blue.
Uber chọn hai cột trong cùng một index thay vì duy trì hai index hoàn toàn tách biệt để giảm chi phí lưu trữ và vận hành. Đổi lại, cách làm này tạo thêm rủi ro: quá trình xây snapshot mới vẫn materialize lại cả cột đang active, nên một bug có thể làm hỏng cả dữ liệu tưởng như không thay đổi. (Uber)
Vì vậy blue-green không phải cây đũa thần.
Nó chỉ cho ta một con đường chuyển đổi.
Hai bên đường vẫn cần lan can.
Ba cánh cửa trước khi model được bước vào production
Uber đặt ba nhóm kiểm tra tự động trước khi deploy index mới.
1. Completeness
Hệ thống kiểm tra số lượng tài liệu trong index có khớp với dữ liệu nguồn hay không.
Không chỉ tổng số record.
Còn kiểm tra các tập con quan trọng sau filter.
Điều này giúp phát hiện:
- Batch chưa chạy hết
- Một thị trường bị thiếu
- Một loại document biến mất
- Pipeline mới chỉ ingest được nửa catalog
Một index rất nhanh nhưng thiếu 30% món ăn vẫn là index hỏng.
2. Backward compatibility
Cột đang active được kỳ vọng không thay đổi phải giống byte-for-byte giữa index cũ và index mới.
Ví dụ Blue đang phục vụ.
Quá trình build Green không được làm Blue thay đổi.
Nếu một byte khác đi, pipeline dừng.
Đây là cách kiểm tra khá cứng.
Nhưng với phần dữ liệu “không được phép thay đổi”, sự cứng đầu là một đức tính tốt.
3. Correctness
Index mới được deploy vào môi trường không phải production.
Hệ thống replay các truy vấn thật với tốc độ kiểm soát, sau đó so sánh recall với index đang chạy.
Kiểm tra này giúp phát hiện:
- Dữ liệu bị hỏng trong lúc index
- Cấu hình khoảng cách sai
- Feature upstream bị regression
- Model mới tệ hơn model cũ
Ba kiểm tra này bảo vệ production trước khi chuyển traffic. (Uber)
Nói ngắn gọn:
- Đã mang đủ hàng chưa?
- Có làm hỏng hàng cũ không?
- Hàng mới có tốt hơn hoặc ít nhất không tệ hơn không?
Nghe giống câu hỏi của bác quản lý kho.
Nhưng nhiều hệ thống AI hỏng vì quên đúng ba câu này.
Kiểm tra lúc build chưa đủ, vì con người vẫn có thể bấm nhầm
Ngay cả khi pipeline hoàn hảo, production vẫn có thể gặp lỗi cấu hình.
Ví dụ:
- Query Model trỏ nhầm phiên bản
- Active column mapping bị đặt sai
- Index được build bằng model khác với dự kiến
- Cấu hình rollout cập nhật không đồng bộ
Uber đưa model ID vào metadata của từng cột index.
Trong một số request được lấy mẫu, service kiểm tra xem model tạo Query Embedding có khớp với model ID ghi trên cột index đang active hay không.
Nếu không khớp:
- Ghi metric compatibility error
- Log đầy đủ context
- Phát cảnh báo nếu lỗi không trở về 0 trong một khoảng ngắn
- Tự động rollback model về phiên bản trước
Index vẫn giữ nguyên. (Uber)
Đây là đoạn mình tâm đắc nhất về mặt production.
Họ không tin hoàn toàn vào deployment pipeline.
Họ cũng không tin hoàn toàn vào con người.
Hệ thống tự kiểm tra giấy đăng ký kết hôn của Query Model và Document Index ngay trong lúc phục vụ.
Nếu hai người không phải một cặp, cho quay xe.
Search platform không tiến hóa theo đường thẳng
Nhìn hệ thống semantic search hiện tại, ta dễ nghĩ Uber chỉ cần cài thêm vector search vào một nền tảng có sẵn.
Thực tế, search platform của Uber đã đi qua nhiều giai đoạn.
Thời kỳ đầu, họ dùng Elasticsearch và phải liên tục mở rộng cluster để theo kịp tăng trưởng.
Sau đó, các yêu cầu cập nhật thời gian thực như tìm tài xế đang di chuyển khiến kiến trúc near-real-time mặc định không còn đủ phù hợp cho mọi use case. Uber xây Sia, một search engine nội bộ với lớp Live Index trong bộ nhớ, Snapshot Index và Base Index, đồng thời dùng Kafka cho pull-based ingestion và active-active deployment.
Nhưng hệ thống tự xây cũng tạo ra một loại thuế khác.
Mỗi tính năng Lucene mới phải được tích hợp lại vào kiến trúc riêng.
Khi vector search và HNSW xuất hiện, Uber phải làm thêm rất nhiều công việc để đưa chúng vào mô hình Base/Snapshot/Live. Cuối cùng, họ nhận ra phần lớn workload không thực sự cần độ tươi tuyệt đối và bắt đầu chuyển hướng về hệ sinh thái mở, Lucene Plus và OpenSearch. (Uber)
Đây là bài học khá đẹp.
Một giải pháp tự xây có thể cứu doanh nghiệp ở giai đoạn này.
Rồi trở thành gánh nặng ở giai đoạn khác.
Kiến trúc không phải lời thề trăm năm.
Nó là một thỏa thuận tạm thời giữa nhu cầu hiện tại và cái giá đội kỹ thuật chấp nhận trả.
Một pipeline đơn giản để hình dung
Nếu bỏ qua nhiều lớp hạ tầng, hệ thống có thể được hình dung như sau:
1 OFFLINE
2
3Catalog nhà hàng / món ăn / sản phẩm
4 │
5 ▼
6 Document Tower
7 (Qwen)
8 │
9 ▼
10 Document Embeddings
11 │
12 ▼
13 Feature Store
14 │
15 ▼
16 Lucene Plus / HNSW Index
17
18
19 ONLINE
20
21Người dùng nhập truy vấn
22 │
23 ▼
24 Query Tower
25 (Qwen)
26 │
27 ▼
28 Query Embedding
29 │
30 ▼
31 Geo / city / type pre-filter
32 │
33 ▼
34 ANN Search bằng HNSW
35 │
36 ▼
37 Top-k candidates
38 │
39 ▼
40 Micro re-ranking
41 │
42 ▼
43 Downstream ranking
44 │
45 ▼
46 Kết quả search
Nhìn sơ đồ khá sạch.
Production thật thì còn thêm:
- Model registry
- Workflow orchestration
- Distributed training
- Batch inference
- Index builder
- Snapshot validation
- Version mapping
- Canary rollout
- Metrics
- Alert
- Rollback
- Capacity planning
- Cost monitoring
Sơ đồ kiến trúc luôn giống gian bếp trước giờ mở cửa.
Sạch sẽ.
Nồi nào nằm đúng chỗ nồi đó.
Production lúc cao điểm thì giống 12 giờ trưa.
Dầu nóng.
Order kêu liên tục.
CPU chạy gần đầy.
Một index build trễ mười phút cũng đủ để vài người bắt đầu mở laptop trong lúc ăn cơm.
Nếu áp dụng vào search bán lẻ Việt Nam
Bài toán của Uber Eats rất gần với search trong thương mại điện tử.
Người dùng Việt Nam hiếm khi gõ đúng tên catalog.
Họ thường tìm theo nhu cầu:
1tivi 50 inch giá 10 triệu
1máy lạnh phòng 18m2 tiết kiệm điện
1điện thoại pin trâu chụp hình đẹp
1laptop học IT chạy được AI
1tủ lạnh cho gia đình 4 người
Lexical search có thể cố gắng tách từ:
tivi50 inch10 triệu
Nhưng những truy vấn như:
1máy lạnh cho phòng nóng hướng tây
không đơn giản là match từ khóa.
Catalog có thể không hề chứa chữ hướng tây.
Nhưng hệ thống cần hiểu rằng người dùng đang quan tâm:
- Công suất làm lạnh
- Diện tích phòng
- Khả năng tiết kiệm điện
- Mức độ nắng nóng
- Có thể cần dư công suất
Semantic search giúp kéo truy vấn và sản phẩm gần nhau theo ý nghĩa.
Nhưng vector search một mình vẫn chưa đủ.
Ta cần filter bằng dữ liệu có cấu trúc:
1Giá <= 10 triệu
2Kích thước = 50 inch
3Còn hàng tại khu vực
4Có giao lắp
5Thương hiệu phù hợp
Một kiến trúc thực tế có thể kết hợp:
1Query understanding
2 │
3 ├── Trích xuất filter có cấu trúc
4 │
5 └── Tạo semantic embedding
6 │
7 ▼
8 Hybrid retrieval
9 lexical + filter + vector search
10 │
11 ▼
12 Re-ranking
Đây là điều rất đáng lưu ý.
Đừng bắt embedding làm toán.
Đừng nhờ vector tự hiểu rằng dưới 10 triệu là một điều kiện số học cứng.
Semantic search hiểu ý.
Database filter giữ kỷ luật.
Hai bên phối hợp mới ra kết quả tốt.
Đừng vội thay toàn bộ keyword search bằng vector search
Semantic search rất hấp dẫn.
Nhưng không phải mọi truy vấn đều cần model lớn.
Người dùng gõ:
1iphone 17 pro max 256gb
Lexical search có thể đã làm rất tốt.
Tên sản phẩm rõ.
Thông số rõ.
Ý định rõ.
Nếu semantic search tự do quá mức, nó có thể trả thêm:
- iPhone 17 Pro
- iPhone 16 Pro Max
- Điện thoại Android cùng phân khúc
Về ý nghĩa, chúng gần nhau.
Về nhu cầu mua hàng, có thể hoàn toàn sai.
Trong thực tế, hybrid search thường hợp lý hơn:
- Exact match cho mã sản phẩm và tên rõ ràng
- Lexical search cho từ khóa quan trọng
- Semantic search cho truy vấn tự nhiên, lỗi chính tả, từ đồng nghĩa và đa ngôn ngữ
- Filter cho điều kiện cứng
- Ranking model để phối hợp tất cả tín hiệu
Search không nên cưới một thuật toán rồi cắt đứt quan hệ với phần còn lại của thế giới.
Nó nên giống một ban nhạc.
Keyword giữ nhịp.
Vector chơi giai điệu.
Filter đứng cửa kiểm vé.
Ranker quyết định ai được lên sân khấu trước.
Những điều không nên sao chép máy móc từ Uber
Uber có catalog, lưu lượng và hạ tầng ở quy mô rất lớn.
Không phải hệ thống nào cũng cần:
- Qwen làm backbone
- DeepSpeed ZeRO-3
- Hàng trăm triệu training samples
- HNSW cho hàng tỷ document
- Hai cột blue-green trong cùng index
- Model refresh hai tuần một lần
- Ray orchestration
- Multi-stage neural re-ranking
Nếu website có 50.000 sản phẩm, ta có thể bắt đầu đơn giản hơn:
- Chuẩn hóa dữ liệu catalog.
- Sửa lỗi chính tả và synonym.
- Dùng hybrid lexical + vector search.
- Tạo embedding offline.
- Dùng một vector database hoặc search engine có HNSW.
- Thêm filter có cấu trúc.
- Theo dõi query không có kết quả.
- Đánh giá bằng dữ liệu click và conversion.
- Chỉ thêm model phức tạp khi hệ thống đơn giản đã chạm trần.
Đừng xây sân bay quốc tế chỉ để chở một rổ rau ra chợ.
Nhưng cũng đừng dùng cuốn sổ giấy rồi kỳ vọng search hiểu được:
Tôi cần một cái tivi xem bóng đá rõ, phòng hơi sáng, giá vừa phải và vợ tôi không thích màn hình quá lớn.
Điều mình tâm đắc nhất: Semantic search không phải một model
Ban đầu, khi nhìn bài viết của Uber, thứ nổi bật nhất là Qwen.
Sau đó là two-tower.
Rồi HNSW.
MRL.
Quantization.
Nhưng đọc kỹ hơn, mình thấy nhân vật chính không phải bất kỳ công nghệ nào trong số đó.
Nhân vật chính là sự đồng bộ.
Query Model phải hiểu cùng một thế giới với Document Model.
Document Embedding phải khớp với index.
Index mới phải đầy đủ như dữ liệu nguồn.
Cột cũ phải không bị thay đổi.
Model ID phải khớp metadata.
Cấu hình phải biết cột nào đang active.
Khi có lỗi, rollback phải xảy ra trước khi người dùng kịp nhận ra hệ thống đang nói tiếng khác.
Semantic search không phải:
1Text → Embedding → Vector DB
Đó chỉ là hình vẽ demo.
Semantic search production là:
1Ngôn ngữ
2→ Dữ liệu huấn luyện
3→ Model
4→ Phiên bản
5→ Embedding
6→ Feature Store
7→ Index
8→ ANN
9→ Filter
10→ Ranking
11→ Validation
12→ Deployment
13→ Monitoring
14→ Rollback
Model chỉ là một bánh răng.
Đẹp.
Thông minh.
Rất đắt.
Nhưng vẫn chỉ là một bánh răng.
Tổng kết
Một người dùng gõ vài chữ vào Uber Eats.
Trong chưa đầy một khoảnh khắc, câu chữ được đưa qua Query Tower.
Một vector được sinh ra.
Bộ lọc địa lý thu nhỏ cả thế giới xuống một khu vực có thể giao.
HNSW chạy qua mạng lưới hàng tỷ ứng viên.
Những vector gần nhất được kéo ra.
Một mạng nhỏ re-rank chúng.
Các lớp ranking tiếp tục cân nhắc ý nghĩa, vị trí và điều kiện vận hành.
Cuối cùng, màn hình hiện lên vài nhà hàng.
Người dùng chỉ thấy hình một tô mì.
Phía sau tô mì đó là hàng trăm triệu mẫu training, những GPU từng nóng lên để học ngôn ngữ, các batch job tạo embedding, những graph index nằm trong bộ nhớ, CPU tính khoảng cách, metadata kiểm tra model ID và hệ thống rollback đang đứng chờ như bình chữa cháy treo trên tường.
Không có gì là hoàn toàn ảo.
Một câu tìm kiếm không chỉ là chữ.
Nó chạy qua dây điện.
Đi vào chip.
Biến thành số thực.
Bị nén xuống int7.
Chạy qua một đồ thị nhiều tầng.
Va vào những giới hạn rất thật của địa lý, cửa hàng, kho hàng và thời gian giao.
Rồi trở lại màn hình dưới hình dạng một kết quả có vẻ hiển nhiên.
Đó là điều mình tâm đắc nhất từ hệ thống search của Uber.
Máy không cần người dùng viết thật đúng.
Nó phải hiểu đủ đúng.
Nhưng để tạo ra cảm giác “máy hiểu mình”, phía sau không chỉ cần một model ngôn ngữ mạnh.
Nó cần cả một nền công nghiệp nhỏ hoạt động đồng bộ, nơi chất lượng, tốc độ, chi phí và độ tin cậy liên tục kéo nhau về bốn hướng khác nhau.
Search tốt không phải tìm được tài liệu có nhiều chữ giống nhất.
Nó là khả năng nhìn vài ký tự vụng về của một con người đang đói, rồi tìm ra thứ người đó thực sự muốn trước khi họ mất kiên nhẫn và tự đi nấu mì gói.
Bình luận