Semantic search vs keyword search
Tìm theo nghĩa mạnh khi diễn đạt khác nhau, nhưng thua trắng trước mã lỗi, tên riêng, số hiệu phiên bản. Biết trước truy vấn nào sẽ hỏng — và khi nào cần hybrid.
TL;DR: Semantic search thắng khi hai câu diễn đạt khác nhau nhưng cùng nghĩa (bài 01), nhưng thua trắng trước định danh chính xác — mã lỗi, tên endpoint, số phiên bản, số hoá đơn. Lý do nằm ở gốc: những chuỗi đó không có khái niệm "gần nghĩa", chỉ có trùng hoặc không trùng, còn semantic lại đi tìm thứ nghe giống. Keyword search (BM25 — xếp hạng theo tần suất và độ hiếm của từ khoá) khớp CHUỖI CHÍNH XÁC nên thắng tuyệt đối ở loại truy vấn này. Hybrid chạy song song cả hai đường tìm kiếm rồi hoà điểm, ăn được cả hai thế mạnh thay vì chọn một trong hai.
Nhóm nghỉ phép ở bài 01 mừng ra mặt vì semantic search cuối cùng cũng tìm ra tài liệu HR dù nhân viên gõ khác hẳn từ khoá. Tự tin, họ mở rộng cùng công cụ đó sang toàn bộ tài liệu kỹ thuật nội bộ — runbook, API doc, changelog. Tuần đầu tiên, một kỹ sư gõ "mã lỗi ERR-PAYMENT-4023 là gì" vào ô tìm kiếm, kỳ vọng ra đúng trang runbook xử lý mã đó. Kết quả trả về: một bài hướng dẫn cấu hình cổng thanh toán — không nhắc tới mã lỗi nào cả, mà điểm similarity vẫn cao, vì cả hai đoạn đều "nói về thanh toán". Vài ngày sau, một truy vấn khác — "/api/v2/transfers dùng để làm gì" — trả về tài liệu của /api/v2/withdrawals, một endpoint hoàn toàn khác, chỉ giống nhau ở chỗ cùng nằm trong cụm "chuyển khoản".
Cùng một công cụ vừa thắng đậm ở bài 01, giờ thua trắng. Bài này giải thích vì sao — và quan trọng hơn, giúp bạn đoán trước loại truy vấn nào sẽ hỏng, thay vì phát hiện ra sau khi người dùng report bug.
Bạn đã thấy tokenizer cắt một chuỗi lạ — số, mã, ký hiệu — thành nhiều mảnh subword theo quy tắc thống kê, không theo quy luật ngữ nghĩa nào. Bài này dùng lại đúng cơ chế đó: một mã lỗi bị băm thành các mảnh token rời rạc trước khi vào embedding, nên model không đối xử với nó như MỘT đơn vị định danh duy nhất — nó chỉ thấy vài mảnh chữ và số.
1. Analogy — danh bạ điện thoại: mô tả mập mờ và số gọi chính xác
Bạn nhớ mang máng "anh nào bên vận hành, tên có chữ Minh, hay gọi lúc nửa đêm" — mô tả mập mờ này vẫn giúp đồng nghiệp đoán ra đúng người, dù mỗi người kể một kiểu. Nhưng khi cần BẤM một số điện thoại, không có khái niệm "số gần đúng" nào chấp nhận được: bấm sai một chữ số cuối, bạn gọi trúng một người hoàn toàn xa lạ — không phải "gần đúng người cần gọi", mà là SAI HẲN, không liên quan gì.
| Đời thường | Tìm kiếm |
|---|---|
| Mô tả mập mờ ("anh gì bên vận hành") | Truy vấn diễn đạt tự do, không chứa định danh |
| Nhiều người đoán đúng ý dù khác cách nói | Semantic search — khớp theo nghĩa |
| Số điện thoại phải đúng từng chữ số | Mã lỗi, endpoint, version phải khớp đúng từng ký tự |
| Sai 1 chữ số = gọi nhầm người khác hẳn, không phải "gần đúng" | Sai 1 ký tự = tài liệu khác hẳn, không phải "gần nghĩa" |
Mô tả thì có thể mập mờ mà vẫn ra đúng người; số điện thoại thì không có "gần đúng" — chỉ có đúng hoặc gọi nhầm.
Analogy hỏng ở chỗ số điện thoại luôn là 10 chữ số cùng một khuôn, còn định danh trong hệ thống thật đa dạng hình dạng hơn nhiều (chữ, số, dấu gạch ngang, dấu chấm) — nhưng bản chất "không có gần đúng, chỉ có trùng hoặc sai" thì giữ nguyên ở cả hai.
2. Truy vấn nào sẽ hỏng?
Năm truy vấn dưới đây sẽ được đưa vào một hệ semantic search đã lập chỉ mục toàn bộ tài liệu kỹ thuật nội bộ. Với mỗi truy vấn, đoán xem semantic search có khả năng trả về đúng tài liệu hay không — viết ra "Được" hoặc "Hỏng" cho từng dòng trước khi đọc tiếp:
- "cách xử lý khi thanh toán bị treo"
- "mã lỗi
ERR-PAYMENT-4023là gì" - "endpoint nào xử lý chuyển tiền giữa hai tài khoản"
- "
/api/v2/transfersdùng để làm gì" - "phiên bản
v2.3.1sửa lỗi gì"
3. Vì sao ranh giới là "có định danh", không phải "chủ đề"?
Đối chiếu với dự đoán của bạn: câu 1 và 3 semantic làm tốt, câu 2, 4, 5 hỏng. Để ý câu 3 và câu 4 hỏi cùng một endpoint, chỉ khác cách hỏi — đó chính là ranh giới của cả bài. Câu 3 diễn đạt tự nhiên, không chứa chuỗi định danh; đúng bài toán semantic được huấn luyện để giải, giống cơ chế "xin nghỉ phép năm" ở bài 01. Câu 4 chứa nguyên văn /api/v2/transfers — một chuỗi định danh literal — và đó là chỗ semantic gãy.
Nhắc lại từ bài 01: không gian nghĩa được học từ việc kéo gần các cặp được huấn luyện coi là liên quan. Nhưng ERR-PAYMENT-4023 và ERR-PAYMENT-4024 không phải hai cách diễn đạt của cùng một ý — chúng là hai khoá tra cứu độc lập, có thể trỏ tới hai lỗi không liên quan. Chưa tập huấn luyện nào dạy model "hai mã liền số thì cùng nghĩa", vì bản thân chúng không mang nghĩa để học. Vị trí của chúng trong không gian vector là hệ quả ngẫu nhiên của việc tokenizer băm chúng ra sao, không phải bằng chứng chúng có liên quan hay không.
embed("ERR-PAYMENT-4023 timeout khi goi cong thanh toan")
-> tokenizer bam thanh cac manh subword: ["ERR", "-", "PAY", "MENT", "-", "402", "3", ...]
-> embedding gop cac manh do lai thanh MOT vector chu de "thanh toan"
-- Doi 4023 thanh 4024, ranh gioi cat subword co the đoi khac hoan toan
-- => khoang cach vector giua hai ma KHONG phan anh dang tin cay "cung loi hay khac loi"
Điểm cosine similarity (bài 02) vẫn tính ra một con số hợp lệ cho cặp (câu hỏi, tài liệu) chứa mã lỗi — chỉ là con số đó đo đúng thứ semantic đã học được (chủ đề "thanh toán"), không đo thứ bạn thật sự cần (khớp đúng khoá tra cứu).
4. Vì sao Keyword/BM25 thắng tuyệt đối ở đây?
BM25 (Best Matching 25) là thuật toán xếp hạng cho tìm kiếm từ khoá: chấm điểm một tài liệu dựa trên tần suất từ khoá xuất hiện trong đó (term frequency) và độ hiếm của từ khoá đó trên toàn tập tài liệu (inverse document frequency) — từ càng hiếm mà xuất hiện càng nhiều lần, tài liệu đó càng được chấm cao. Khác semantic, BM25 không đo "gần nghĩa" — nó đo sự có mặt của đúng chuỗi/token.
Khi ERR-PAYMENT-4023 xuất hiện y nguyên trong một trang runbook, BM25 tìm ra ngay: điểm số phụ thuộc mã đó có mặt hay không, không phụ thuộc trang đó "nghe có liên quan" hay không. Đây là mặt lật ngược của điểm yếu keyword từng gặp ở bài 01 — "xin nghỉ phép năm" và "đăng ký ngày nghỉ" không chung từ khoá nên keyword tìm rỗng. Hai công cụ đặt cược ngược nhau: keyword thua ở diễn đạt khác nhau cùng ý, semantic thua ở chuỗi định danh không có "gần nghĩa".
5. Hybrid hoạt động thế nào?
Hybrid search không phải "thêm bộ lọc từ khoá lên trên semantic" — đó là chạy song song hai công cụ độc lập trên cùng truy vấn, rồi hoà hai danh sách kết quả thành một bảng xếp hạng duy nhất:
function hybridSearch(query, docs):
semanticResults <- semanticSearch(embed(query), docs) -- xep hang theo cosine similarity
keywordResults <- keywordSearch(query, docs) -- xep hang theo BM25 (khop tu khoa)
combined <- mergeScores(semanticResults, keywordResults) -- cong diem hoac hoa theo thu hang
return topK(combined)
// Time: O(chi phi hai duong tim kiem cong lai) Space: O(so tai lieu ung vien)

Chưa có benchmark định lượng nào đủ tin cậy cho một con số cụ thể kiểu "hybrid tăng recall X%" — con số đó phụ thuộc quá nhiều vào tỉ lệ truy vấn chứa định danh trong domain thật, nên bài này không trích số. Điều chắc chắn là định tính: trên tập truy vấn có lẫn mã lỗi, endpoint, số phiên bản, hybrid thường vượt vector thuần rõ rệt, vì đường keyword vẫn bắt được đúng những gì đường semantic bỏ lỡ — còn trên truy vấn thuần diễn đạt tự nhiên, phần đóng góp của keyword gần như bằng không.
6. Pitfall tổng hợp
❌ Nhầm 1 — coi hybrid là "semantic trước, lọc từ khoá sau":
✅ Lọc sau chỉ hoạt động trên kết quả semantic đã trả về — nếu semantic không đưa ERR-PAYMENT-4023 vào top-k ban đầu, lọc từ khoá phía sau không còn gì để lọc. Hybrid đúng nghĩa chạy BM25 độc lập, song song, không phụ thuộc semantic tìm thấy trước hay không.
❌ Nhầm 2 — nghĩ thêm hybrid là xong, không cần suy nghĩ gì thêm:
✅ Trọng số hoà điểm giữa hai đường không có công thức mặc định đúng cho mọi domain — hệ nhiều mã định danh (banking, DevOps) cần nghiêng về keyword hơn hệ tài liệu thuần văn bản (HR, chính sách nội bộ).
7. 📚 Deep Dive
- Muennighoff et al. — MTEB: Massive Text Embedding Benchmark (2022) — cùng nguồn đã dẫn ở bài 01: các tác vụ benchmark đo tương đồng nghĩa và truy hồi theo chủ đề, không có tác vụ nào đo "khớp định danh chính xác" — đúng khoảng trống bài này khai thác.
Ghi chú: nguồn đối chiếu ngày 2026-07-23. Lập luận cơ chế trong bài là suy luận từ tính chất embedding đã học ở bài 01 và cơ chế subword ở module Cỗ máy LLM, không phải trích trực tiếp từ nguồn trên.
8. Liên hệ các bài khác
- Bài 01 — Embedding, không gian nghĩa — nguồn của "gần nghĩa": vì sao semantic mạnh ở diễn đạt khác nhau, và chính giới hạn đó gây ra thất bại ở bài này.
- Bài 02 — Cosine similarity — phép đo "gần" mà bài này chỉ ra: vẫn tính ra điểm số hợp lệ, chỉ là đo sai thứ cần đo khi truy vấn chứa định danh.
- Bài 04 — Chunking — quyết định cắt tài liệu ảnh hưởng cả semantic lẫn keyword, nhưng theo hai cách khác nhau.
- Bài 06 — RAG failure modes — khi retrieval chọn nhầm tài liệu (như ca
ERR-PAYMENT-4023ở đầu bài) chính là gốc của một loại failure mode cụ thể trong RAG. - Tokenization — cơ chế subword đứng sau lý do định danh "vỡ vụn" trước khi vào embedding.
9. Tóm tắt
- Semantic thắng khi truy vấn diễn đạt tự nhiên; thua khi chứa định danh (mã lỗi, endpoint, version) — định danh không có khái niệm "gần nghĩa", chỉ có trùng hoặc không.
- Cùng nội dung: hỏi tự nhiên thì semantic thắng, hỏi kèm chuỗi định danh literal thì semantic thua — ranh giới là cách hỏi, không phải chủ đề.
- Keyword và semantic đặt cược ngược nhau — mỗi bên thắng đúng chỗ bên kia thua; hybrid chạy song song cả hai rồi hoà điểm.
- Chưa có benchmark định lượng đáng tin cho "hybrid tốt hơn X%" — bằng chứng hiện có là định tính.
10. Tự kiểm tra
Q1Câu 'endpoint nào xử lý chuyển tiền giữa hai tài khoản' semantic trả đúng, còn '/api/v2/transfers dùng để làm gì' lại hỏng, dù cùng hỏi về một endpoint. Vì sao?▸
Câu đầu diễn đạt tự nhiên, không chứa định danh — đúng bài toán semantic được huấn luyện để giải. Câu sau chứa nguyên văn /api/v2/transfers, một chuỗi định danh literal mà không gian nghĩa không có khái niệm "gần" — chỉ có "trùng" hoặc "không trùng". Ranh giới nằm ở cách hỏi, không phải nội dung được hỏi.
Q2Một đồng nghiệp nói: 'mã lỗi ERR-PAYMENT-4023 và ERR-PAYMENT-4024 chắc gần giống nhau vì số liền nhau.' Dùng cơ chế tokenization và embedding để phản biện.▸
Chưa tập dữ liệu nào dạy model rằng hai mã số liền nhau thì cùng nghĩa, vì bản thân chuỗi số đó không mang nghĩa để học. Vị trí vector của chúng chỉ là hệ quả của việc tokenizer băm chuỗi ký tự đó ra sao, không phải bằng chứng cho việc hai mã có liên quan — chúng có thể trỏ tới hai lỗi hoàn toàn khác nhau.
Q3Vì sao nói keyword/BM25 và semantic search 'đặt cược ngược nhau'? Điều đó giải thích gì về việc mỗi bên thắng đúng chỗ bên kia thua?▸
Keyword đặt cược vào việc chuỗi/token phải khớp — thắng khi truy vấn chứa đúng chuỗi cần tìm, thua khi người dùng diễn đạt khác từ. Semantic đặt cược vào khoảng cách trong không gian nghĩa đã học — thắng ở diễn đạt khác nhau cùng ý, thua khi cần khớp đúng một định danh không có "gần nghĩa". Hai cách đặt cược đối lập nên vùng thắng của bên này chính là vùng thua của bên kia.
Q4BM25 chấm điểm một tài liệu dựa trên điều gì? Vì sao cơ chế đó không bị 'lừa' bởi việc hai chuỗi định danh trông giống nhau?▸
BM25 chấm dựa trên tần suất một từ khoá xuất hiện trong tài liệu và độ hiếm của nó trên toàn tập tài liệu. Đây là phép đo sự có mặt của đúng chuỗi/token, không phải khoảng cách nghĩa, nên hai định danh trông giống nhau (như hai mã lỗi liền số) không được coi là "gần" trừ khi trùng từng ký tự.
Q5Vì sao hybrid search chạy 'semantic trước rồi lọc từ khoá sau' không phải là hybrid đúng nghĩa?▸
Lọc sau chỉ hoạt động trên kết quả mà semantic đã trả về từ đầu. Nếu semantic không đưa tài liệu đúng vào top-k ban đầu, tài liệu đó đã bị loại trước khi BM25 có cơ hội chấm điểm. Hybrid đúng nghĩa chạy BM25 độc lập, song song với semantic, rồi mới hoà hai danh sách kết quả.
Q6Cho truy vấn 'cách reset mật khẩu khi quên email đăng ký' — dự đoán semantic hay keyword sẽ thắng, và giải thích vì sao.▸
Truy vấn không chứa định danh nào (không mã lỗi, không endpoint, không version) — đây là câu diễn đạt tự nhiên về một ý định, thuộc nhóm semantic thắng: người dùng có thể hỏi nhiều cách khác nhau và semantic vẫn kéo được các đoạn cùng ý định lại gần nhau, đúng cơ chế đã thấy ở bài 01.
Bài tiếp theo: Chunking
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên