3.12 Dựng, cập nhật và xoá index
Mọi ví dụ ở các mục trước đều giả định index đã có sẵn và không đổi. Trong sản phẩm thật, tài liệu được thêm, sửa, xoá liên tục — và cách một index xử lý cập nhật quyết định phần lớn kiến trúc vận hành của nó.
Dựng index khi dữ liệu không vừa RAM
Phần tiêu đề “Dựng index khi dữ liệu không vừa RAM”Cách ngây thơ (một hash map term → postings trong RAM) chết ở corpus lớn. Hai
thuật toán cổ điển:
| Cách | Ý tưởng | Đánh đổi |
|---|---|---|
| BSBI (blocked sort-based) | đọc từng khối tài liệu → sinh cặp (termID, docID) → sắp → ghi ra đĩa → trộn nhiều đường | Cần bảng term→termID toàn cục |
| SPIMI (single-pass in-memory) | mỗi khối tự dựng dictionary riêng, đầy RAM thì ghi ra một index con → trộn sau | Một lượt, không cần bảng toàn cục. Cách hiện đại dùng |
Cả hai đều dẫn tới cùng một cấu trúc vận hành: nhiều index con, được trộn dần.
Lucene/Elasticsearch: segment bất biến
Phần tiêu đề “Lucene/Elasticsearch: segment bất biến”Đây là mô hình đáng hiểu nhất, vì gần như mọi công cụ tìm kiếm hiện đại đều làm giống:
tài liệu mới → buffer trong RAM → (refresh, mặc định 1 s) → segment mới, BẤT BIẾN, tìm được ngay → (merge, ngầm) → nhiều segment nhỏ gộp thành segment lớnHệ quả trực tiếp, cần biết trước khi gặp:
| Sự thật | Hệ quả vận hành |
|---|---|
| Segment bất biến | Không có “sửa tài liệu”. Sửa = xoá + thêm mới |
| Xoá là cờ đánh dấu (tombstone) | Tài liệu đã xoá vẫn nằm trên đĩa, vẫn tính vào df, cho tới lúc merge |
df lệch tạm thời | Điểm BM25 có thể xê dịch nhẹ sau xoá hàng loạt, tự đúng lại sau merge |
| Merge tốn I/O đột biến | p99 latency có gai. Đây là nguồn “chậm ngẫu nhiên” hay bị quy oan cho truy vấn |
| Cập nhật nhiều = rác nhiều | Tài liệu bị sửa 10 lần để lại 9 bản chết chờ merge |
refresh_interval là nút xoay quan trọng nhất mà ít người xoay. Mặc định 1 giây
nghĩa là “gần thời gian thực” — và mỗi lần refresh sinh một segment nhỏ, kéo theo
merge. Nếu sản phẩm chịu được độ trễ dữ liệu 30 giây, đặt 30s thường giảm đáng kể
tải ghi và làm p99 phẳng hơn. Với lần nạp lớn (bulk import): tắt refresh, nạp, rồi bật lại.
Postgres full-text: mô hình hoàn toàn khác
Phần tiêu đề “Postgres full-text: mô hình hoàn toàn khác”tsvector + GIN index nằm trong transaction. Không có segment, không có
refresh — commit là thấy. Đổi lại, cập nhật GIN đắt hơn, và Postgres bù bằng
fastupdate (danh sách chờ chèn) — thứ tự đến lượt nó tạo ra gai latency khi danh
sách chờ được xả.
Một dòng để chọn: cần nhất quán đọc-sau-ghi tức thì và corpus vừa phải → Postgres. Cần throughput ghi cao, corpus lớn, chịu được độ trễ dữ liệu vài giây → Lucene/Elasticsearch.
Reindex: việc sẽ phải làm, nên chuẩn bị trước
Phần tiêu đề “Reindex: việc sẽ phải làm, nên chuẩn bị trước”Bạn sẽ phải index lại toàn bộ. Chắc chắn. Vì:
- đổi tokenizer (và 3.1 cho thấy bạn sẽ đổi),
- đổi chiến lược chunking (Tầng 4),
- đổi model embedding — vector cũ và mới không so sánh được với nhau.
Nên thiết kế cho việc đó từ đầu:
| Việc | Vì sao |
|---|---|
| Ghi alias, không ghi thẳng tên index | Đổi index mới = trỏ lại alias, tức thời, có đường lùi |
| Giữ được nguồn sự thật để dựng lại | Index là dữ liệu dẫn xuất. Nếu không dựng lại được từ nguồn, bạn không có backup mà có một điểm chết |
| Đo lại golden set trước khi đổi alias | Reindex là lúc dễ làm tụt chất lượng nhất — và cũng là lúc dễ không nhận ra nhất (Phần 2) |
| Ghi phiên bản cấu hình vào index | “Index này dùng tokenizer nào, model nào” — sáu tháng sau không ai nhớ |