Memo kỹ thuật · Search & Relevance

Vì sao tìm "Noo's Question" lại trả về "Survey Poll S-135"

Phạm vi: module k174yh8ddk5fhbf9r2n3hn2a7s898bae — tab Surveys/Polls Trạng thái: Kết luận — giữ nguyên hành vi hiện tại Ngày: 2026-07-07
Kết luận ngắn

Không phải bug hiển thị dữ liệu sai. Đây là hệ quả của cách Convex full-text search tách từ (tokenize) và xếp hạng theo số từ khớp, không yêu cầu khớp toàn bộ cụm tìm kiếm.

Chuỗi displayId dạng S-135 bị tách thành hai token s + 135 vì dấu gạch ngang là ký tự phân tách. Chuỗi query Noo's Question cũng bị tách thành noo + s + question vì dấu nháy đơn cũng là ký tự phân tách. Hai bên trùng nhau ở token rác s — nên gần như mọi survey/poll có ID dạng S-xxx đều "khớp" một phần, dù nội dung không liên quan.

Quyết định: giữ nguyên cơ chế .withSearchIndex() hiện tại — không can thiệp code. Xem lý do ở mục 04.

01 Convex full-text search hoạt động thế nào

Các điểm dưới đây lấy trực tiếp từ tài liệu chính thức của Convex, vì đây là phần dễ hiểu nhầm nhất và là gốc rễ của vấn đề.

Tokenize
Văn bản được tách bằng Tantivy SimpleTokenizer — tách trên khoảng trắng và dấu câu (gồm -, ', v.v), sau đó lowercase. Mỗi token tối đa 32 ký tự.
Khớp nhiều từ
Tài liệu không nói "phải khớp toàn bộ từ". Convex "tách query thành các từ riêng và xếp hạng gần đúng các document khớp các từ này" — nghĩa là khớp một phần vẫn được trả về, chỉ xếp hạng thấp hơn khớp đầy đủ.
Prefix match
Chỉ từ cuối cùng trong query được prefix-match (hỗ trợ gõ tới đâu tìm tới đó). Các từ trước đó phải khớp chính xác (case-insensitive).
Fuzzy/typo
Đã deprecated kể từ 2025-01-15 — hiện tại không còn tự sửa lỗi chính tả như trước.
Relevance
Xếp hạng theo BM25 + số từ khớp, tần suất xuất hiện, độ dài field, độ gần nhau của các từ khớp.
Giới hạn index
Mỗi search index chỉ có đúng 1 search field, tối đa 16 filter field (.eq()) — filter là so khớp tuyệt đối, áp dụng sau khi search index đã trả kết quả.

Cách app đang dùng nó

Field searchContent trên bảng surveys được ghép sẵn từ displayId + title + module title/code trong SurveySearchContentHelper.buildSurveySearchContent surveySearchContent.helper.ts:13, rồi được index bằng .searchIndex('search_surveys', { searchField: 'searchContent' }). Truy vấn thực hiện ở survey.repo.ts:167-172 qua .withSearchIndex(...).take(200) trước khi cắt theo trang.

02 Vì sao va chạm token xảy ra

Sơ đồ dưới minh hoạ đúng ví dụ trong screenshot: query Noo's Question so với hai document thật trong kết quả trả về.

Query
Noo's Question
noo s question
3 token tìm kiếm
searchContent
S-140 · Noo's question
s 140 noo question
Khớp mạnh — đúng ý
searchContent
S-135 · Survey Poll S-135
s 135 survey poll
Khớp yếu — chỉ trùng "s"
Token s trong Noo's và token s tách ra từ S-135cùng một token theo tokenizer. Vì matching không yêu cầu khớp hết, document thứ ba vẫn lọt vào kết quả — chỉ xếp hạng thấp hơn.

Đối chiếu với kết quả trong screenshot

IDTênToken khớpVì sao xuất hiện
S-140Noo's questionnoo, s, questionKhớp đúng
S-146Noo's question 1noo, s, questionKhớp đúng
S-145Noonoo, sKhớp đúng (tên)
S-147Noonoo, sKhớp đúng (tên)
S-135Survey Poll S-135sChỉ trùng "s" từ ID
S-134Survey Poll 134sChỉ trùng "s" từ ID
S-133Survey Poll 133sChỉ trùng "s" từ ID
S-132Survey Poll Double ChecksChỉ trùng "s" từ ID

04 Vì sao không sửa

Mọi cách can thiệp đều đụng vào chính cơ chế đang giữ cho tab Surveys/Polls nhanh, và đều đổi một rủi ro vận hành lấy một cải thiện nhỏ về độ chính xác:

Đổi searchContent
Bỏ dấu - trong displayId khi build lại field index sẽ vá đúng case S-xxx, nhưng là thay đổi dữ liệu đã lưu — cần backfill toàn bộ record cũ, và vẫn còn khả năng va chạm token khác trong tương lai (không triệt để).
Hậu kiểm in-memory
Lọc lại sau khi .withSearchIndex() trả kết quả vẫn phải chạy trên tập đã lấy về (take(200)) ở mọi lần search — thêm một bước xử lý trên request path, tức vẫn ảnh hưởng hiệu năng, chỉ là ở mức nhỏ hơn full scan.
Tách 2 field/2 query
Convex search index giới hạn đúng 1 search field, nên tách ID và nội dung nghĩa là 2 truy vấn song song + merge/rank thủ công — phức tạp nhất và vẫn cộng thêm round-trip, tức cũng ảnh hưởng hiệu năng.

Nói cách khác: đây là đặc thù kỹ thuật (trade-off) vốn có của withSearchIndex — Convex đánh đổi độ chính xác tuyệt đối lấy tốc độ, bằng cách xếp hạng theo số từ khớp thay vì lọc AND/substring đầy đủ. Giữ nguyên hành vi này là lựa chọn nhất quán với lý do ban đầu app dùng search index thay vì quét bảng.