Operator Notes

Bảo vệ thao tác lưu khi thêm tìm kiếm bằng AI

Dữ liệu gốc phải được lưu an toàn ngay cả khi embedding, hàng đợi hoặc kho tìm kiếm đang chậm và lỗi.

Một tính năng AI đã làm thao tác lưu trở nên kém tin cậy

Khi thêm tìm kiếm bằng AI vào một ứng dụng nội bộ, Thành từng đặt phần tạo embedding và cập nhật kho tìm kiếm trong cùng luồng lưu tài liệu.

Luồng đầu tiên trông hợp lý:

Người dùng bấm Lưu
    -> Ghi dữ liệu vào PostgreSQL
    -> Gọi dịch vụ tạo embedding
    -> Cập nhật Qdrant
    -> Báo lưu thành công

Trong một lần kiểm thử, dịch vụ embedding trả về lỗi rate limit. Cách xử lý lúc đó rollback toàn bộ thao tác. Người dùng không lưu được dữ liệu gốc chỉ vì một khả năng tìm kiếm bổ sung chưa hoàn thành.

Sự cố này làm Thành nhìn lại ranh giới của hệ thống. Người dùng đang cố lưu tài liệu, không phải yêu cầu hệ thống chứng minh rằng tài liệu đã sẵn sàng cho AI tìm kiếm ngay trong cùng thời điểm.

Invariant cần được bảo vệ

Thành đặt lại một nguyên tắc cho luồng này:

Dữ liệu gốc được xem là đã lưu khi giao dịch chính hoàn tất. Dữ liệu phục vụ tìm kiếm phải có khả năng tạo lại và không được làm mất dữ liệu gốc.

PostgreSQL giữ dữ liệu nguồn. Embedding và dữ liệu trong Qdrant là dữ liệu dẫn xuất. Nếu phần dẫn xuất bị lỗi, hệ thống phải giữ được tài liệu gốc, trạng thái chưa lập chỉ mục và một đường phục hồi có thể quan sát.

Đây không chỉ là tối ưu thời gian phản hồi. Nó là quyết định thành phần nào được phép làm công việc chính của người dùng thất bại.

Transaction boundary mới

Thành và team tách luồng thành hai phần, đồng thời dùng transactional outbox để tránh khoảng trống giữa việc lưu dữ liệu và tạo công việc nền.

Cùng một database transaction
    -> Lưu dữ liệu gốc
    -> Ghi outbox event
    -> Commit

Xử lý nền
    -> Đọc outbox event
    -> Tạo job lập chỉ mục
    -> Worker tạo embedding
    -> Kiểm tra phiên bản hiện tại
    -> Cập nhật Qdrant

Nếu transaction không commit, cả dữ liệu gốc và outbox event đều không được ghi. Nếu dữ liệu đã commit nhưng việc đưa job vào hàng đợi tạm thời thất bại, outbox event vẫn còn để được xử lý lại.

Reconciliation chạy định kỳ như một lớp kiểm tra bổ sung. Nó đối chiếu dữ liệu nguồn với trạng thái lập chỉ mục và tạo lại công việc khi phát hiện tài liệu còn thiếu hoặc không khớp.

PostgreSQL, hàng đợi và Qdrant được nêu để giải thích quyết định kỹ thuật. Đây là mô hình công khai đã đơn giản hóa, không phải sơ đồ kiến trúc nội bộ.

Retry cần idempotency và kiểm tra phiên bản

Retry không chỉ có nghĩa là chạy lại cùng một đoạn mã. Một job có thể được nhận nhiều lần, và các lần sửa cùng một tài liệu có thể hoàn thành không đúng thứ tự.

Job lập chỉ mục dùng document_id làm idempotency key. Trước khi ghi vào Qdrant, worker đọc và kiểm tra phiên bản hiện tại của tài liệu. Nếu job đang xử lý một phiên bản đã cũ, nó không được phép ghi đè lên dữ liệu mới hơn.

Khi tài liệu được sửa, hệ thống xóa dữ liệu chỉ mục cũ rồi tạo embedding và index dữ liệu mới. Khi tài liệu bị xóa, dữ liệu tương ứng trong Qdrant cũng được xóa.

Chuỗi xóa cũ rồi index mới không phải một thao tác nguyên tử. Nếu worker dừng sau bước xóa, tài liệu gốc vẫn an toàn nhưng có thể tạm thời biến mất khỏi tìm kiếm. Retry và reconciliation đưa phiên bản hiện tại trở lại luồng lập chỉ mục.

Đây là eventual consistency có chủ đích. Thành chấp nhận một khoảng thời gian tìm kiếm chưa cập nhật để đổi lấy việc thao tác lưu không phụ thuộc vào dịch vụ AI.

Mô hình trạng thái

Logic vận hành phân biệt các trạng thái thay vì gộp tất cả thành một dấu thành công:

saved
  -> index_pending
  -> indexing
  -> indexed

indexing
  -> retrying
  -> failed
  -> indexed

saved chỉ xác nhận dữ liệu gốc đã được commit. indexed mới xác nhận phiên bản hiện tại đã có trong kho tìm kiếm. Một tài liệu ở trạng thái retrying hoặc failed vẫn tồn tại trong dữ liệu nguồn và có thể được đưa vào luồng phục hồi.

Giao diện không nên hứa “hoàn tất” nếu hệ thống mới hoàn thành thao tác lưu. Khi trải nghiệm người dùng cần hiển thị trạng thái tìm kiếm, ba ý nghĩa phải được tách rõ: đã lưu, đang lập chỉ mục và chưa có trong tìm kiếm.

Các failure mode đã được kiểm thử

Team đã kiểm thử thực tế các tình huống sau:

Tình huống Dữ liệu gốc Trạng thái tìm kiếm Phản ứng mong đợi
Dịch vụ embedding bị rate limit Đã lưu Chưa cập nhật Job retry với thời gian chờ tăng dần
Enqueue tạm thời thất bại Đã lưu cùng outbox event Chưa có job Outbox được xử lý lại
Worker dừng sau khi xóa index cũ Đã lưu Tạm thời thiếu Retry hoặc reconciliation tạo lại index hiện tại
Cùng một job chạy nhiều lần Đã lưu Không tạo phiên bản trùng document_id giữ tính idempotent
Job cũ hoàn thành sau job mới Đã lưu phiên bản mới Có nguy cơ ghi dữ liệu cũ Worker kiểm tra phiên bản và từ chối ghi sai thứ tự

Các bài kiểm thử này chứng minh các đường lỗi đã được thực thi và quan sát. Chúng không chứng minh hệ thống không bao giờ mất đồng bộ, cũng không tạo ra bảo đảm exactly-once.

Recovery và observability

Queue không tự giải quyết tính nhất quán. Nó tạo một nơi để nhìn thấy và quản lý công việc chưa hoàn tất. Team cần theo dõi ít nhất các tín hiệu sau:

  • Thời gian từ lúc dữ liệu được lưu đến lúc phiên bản hiện tại xuất hiện trong tìm kiếm.
  • Số job đang chờ và số lần retry.
  • Số job đã dừng sau giới hạn retry.
  • Số bản ghi được reconciliation phát hiện chưa có chỉ mục hoặc sai phiên bản.
  • Kết quả của các lần đưa tài liệu trở lại luồng đồng bộ.

Sau số lần thử lại có giới hạn, job lỗi được giữ lại để người vận hành xem và kích hoạt phục hồi. Worker không retry vô hạn vì cách đó có thể che khuất một lỗi kéo dài và tiếp tục tiêu tốn tài nguyên.

Điều đã triển khai và giới hạn còn lại

Trong phạm vi hiện tại, hệ thống đã có:

  • Transactional outbox giữa giao dịch lưu và công việc nền.
  • Reconciliation định kỳ giữa dữ liệu nguồn và trạng thái lập chỉ mục.
  • Idempotency key theo document_id.
  • Kiểm tra phiên bản hiện tại trước khi worker ghi vào Qdrant.
  • Retry có giới hạn và đường giữ lại job không thể hoàn thành.
  • Cơ chế xóa dữ liệu cũ và index lại khi tài liệu thay đổi.

Thiết kế này không cung cấp strong consistency giữa PostgreSQL và Qdrant. Tài liệu vừa lưu hoặc vừa sửa có thể chưa xuất hiện ngay trong tìm kiếm. Khoảng thời gian đó cần được đo và quan sát nếu tìm kiếm trở thành một phần quan trọng của vận hành.

Điều bạn có thể áp dụng

Khi thêm AI vào một luồng đang chạy, bạn có thể bắt đầu bằng năm câu hỏi:

  1. Người dùng đang cần hoàn thành thao tác chính nào?
  2. Dữ liệu nào là nguồn và dữ liệu nào có thể tạo lại?
  3. Nếu lưu thành công nhưng enqueue thất bại, hệ thống giữ dấu vết ở đâu?
  4. Retry có an toàn khi job chạy lặp hoặc hoàn thành sai thứ tự không?
  5. Ai có thể nhìn thấy và phục hồi tài liệu chưa được lập chỉ mục?

Nếu câu trả lời chỉ là “đưa sang queue”, thiết kế vẫn chưa hoàn tất. Hàng đợi chỉ có giá trị khi transaction boundary, idempotency, version check, observability và recovery cùng được định nghĩa.

Phạm vi công bố

Bài viết trình bày mô hình trách nhiệm và các failure mode đã được kiểm thử. Nó không công bố schema, endpoint, thông tin xác thực, topology triển khai, dữ liệu nội bộ hoặc chi tiết vận hành riêng tư. Tên công nghệ chỉ được dùng để làm rõ loại trách nhiệm của từng thành phần.