# Rác thì giống nhau, luật thì khác nhau: vì sao dữ liệu địa phương không chịu nằm chung một khuôn
Có một chuyện rất nhỏ mà mình mất khá lâu mới hiểu hết.
Hồi mới qua Nhật, mình đứng trước một cái hộp nhựa đựng đồ ăn thừa, tay cầm tờ giấy phân loại rác của thành phố, và không chắc mình đang làm đúng hay sai. Tờ giấy đó không khó đọc. Nó chỉ dày. Trong đó có nhóm rác cháy được, có nhóm không cháy được, có nhóm phải dùng túi màu quy định mua ở cửa hàng, có nhóm như chai PET thì bỏ riêng vào một cái xô khác chứ không được lẫn vào túi chung, và có nhóm đồ vải — quần áo cũ, chăn, rèm — thì không bỏ ở nhà mà phải mang ra điểm thu gom vào đúng ngày.
Rồi mình chuyển nhà. Không xa. Chỉ vài chục cây số, vẫn trong cùng một vùng.
Cùng cái hộp đó. Luật khác.
Túi khác màu. Ngày thu gom khác. Có thứ trước đây bỏ chung thì giờ phải tách. Có thứ trước đây phải mang ra điểm tập kết thì giờ lại để trước cửa. Và không ai nói cho mình biết là luật đã đổi, vì với người ở đó thì nó chưa bao giờ đổi cả.
Lúc đó mình chỉ thấy bực. Mãi sau này, lúc ngồi thiết kế mấy cái pipeline gom dữ liệu, mình mới nhận ra chuyện đó không phải chuyện về rác. Nó là chuyện về dữ liệu phân mảnh theo địa phương — và mình tin đây là một trong những chỗ dễ chết nhất, đồng thời cũng là chỗ ít người làm tới cùng nhất.
Cái khó không nằm ở rác. Nó nằm ở chỗ tra cứu.
Thử hỏi: nếu ai đó muốn làm một sản phẩm trả lời câu "cái này bỏ đâu", thì khó ở đâu?
Không khó ở kiến thức. Kiến thức về rác thì công khai, thành phố nào cũng đăng. Không khó ở công nghệ. Một cái app, một cái bot, một trang web — ai cũng làm được.
Khó ở ba chỗ lệch, và cả ba đều không phải lệch về nội dung.
Lệch thứ nhất: đơn vị. Người dùng sống ở cấp "địa chỉ của tôi". Dữ liệu tồn tại ở cấp "thành phố", "quận", đôi khi là "khu phố". Có nơi hai cái nhà cách nhau một con dốc mà thuộc hai đơn vị thu gom khác nhau. Nghĩa là cái khoá tra cứu của người dùng không trùng với cái khoá của dữ liệu. Mà cái này thì không sửa bằng cách viết mô tả hay hơn.
Lệch thứ hai: thời điểm. Luật đổi. Đổi theo năm, đổi theo mùa, đổi theo hợp đồng với đơn vị thu gom, đổi cả vì lý do chính trị địa phương. Dữ liệu mình gom hôm nay là dữ liệu của một phiên bản. Nếu không ghi phiên bản đó vào từng bản ghi, sáu tháng sau mình không biết cái gì đúng cái gì sai — và tệ hơn, mình không biết là mình không biết.
Lệch thứ ba: định nghĩa. Đây là chỗ mình nghĩ nhiều người đánh giá thấp nhất. Cùng một chữ, mỗi nơi một nghĩa. "Đồ vải" ở chỗ này là quần áo. Ở chỗ khác, rèm và chăn cũng tính. Chỗ khác nữa thì chăn phải tách riêng vì kích thước. Nhìn từ ngoài, hai bảng giống nhau. Nhìn từ trong, chúng là hai hệ phân loại khác nhau dùng chung một nhãn.
Ba cái lệch này có một đặc điểm chung: chúng không hiện ra khi bạn đọc một trang. Chúng chỉ hiện ra khi bạn có hai trang cạnh nhau.
Và đây chính là lý do vì sao rất nhiều dự án "gom dữ liệu công khai lại một chỗ" trông rất ổn trong bản demo, rồi chết ở tháng thứ ba.
Mỗi địa phương là một schema riêng, và chi phí không tăng tuyến tính
Hôm nay mình đọc một dự án công khai: có người đi gom đường dẫn hồ sơ công trên 56 hội đồng địa phương rồi chuẩn hoá lại thành một bảng dùng được.
Con số 56 đó làm mình nhớ ngay tới tờ giấy phân loại rác.
Vì nếu chỉ có một nguồn, việc gom là việc của một buổi chiều. Có mười nguồn, bạn bắt đầu viết parser theo từng nguồn. Đến năm mươi nguồn thì bạn không còn làm việc "gom dữ liệu" nữa — bạn đang làm việc bảo trì một bản đồ khác biệt. Mỗi nguồn đổi giao diện một lần mỗi năm là bạn có 56 cái hẹn không ai nhắc.
Điểm mình muốn nói: chi phí của loại dự án này không phải số nguồn × chi phí một nguồn. Nó gần với số nguồn × số trường × số lần đổi. Cái thứ hai mới là cái ăn tiền, vì nó không có điểm kết thúc.
Và đây là chỗ một niềm tin rất phổ biến hiện nay đang làm người ta tính sai.
"Để AI đọc là xong" — cái bẫy nghe rất hợp lý
Khi có model đọc hiểu tốt, phản xạ tự nhiên là: thôi khỏi chuẩn hoá, cứ đưa cả 56 trang cho nó đọc, hỏi gì nó trả lời nấy.
Mình đã tin như vậy một lần. Rồi nhận ra ba chỗ model không cứu được, và cả ba đều không liên quan tới việc nó thông minh cỡ nào.
Một, nó không có cảm giác "chỗ này khác". Model đọc từng trang rất tốt. Nhưng nó không tự nhiên biết rằng trang này đang dùng "đồ vải" theo nghĩa hẹp còn trang kia theo nghĩa rộng. Không có gì trong một trang nói cho nó biết trang đó khác những trang còn lại ở đâu. Cái đó phải do người viết ra — một lần, bằng tay, có chủ đích.
Hai, nó không biết mình đang đọc phiên bản nào. Với model, trang cũ và trang mới đều là "một trang nói về phân loại rác". Nếu mình không tự gắn nhãn thời gian, nó sẽ trả lời bằng phiên bản nó vừa đọc được — và trả lời rất tự tin.
Ba, và đây là chỗ nguy hiểm nhất: nó im lặng khi trường không tồn tại. Người ta hỏi một thứ mà nguồn đó không hề quy định. Model không nói "nguồn này không có trường đó". Nó suy ra từ cái gần giống nhất, rồi viết thành một câu trả lời trôi chảy. Không có lỗi. Không có cảnh báo. Chỉ có một câu trả lời sai được trình bày đẹp.
Ba chỗ đó gộp lại thành một kết luận khá khó chịu: model làm việc đọc rẻ đi, nhưng không làm việc định nghĩa rẻ đi. Mà tiền nằm ở việc định nghĩa.
Nên cái mình làm là ngược lại với phản xạ: dựng tầng cứng trước, rồi mới cho model vào.
# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai
record NormalizedFact:
subject # "chai PET"
unit # địa phương + đơn vị thu gom (không phải địa chỉ thô)
field # "kenh_bo_thai"
value # "xo rieng, khong lan vao tui chung"
effective_from # ngay hieu luc — BAT BUOC
source_version # phien ban cua trang nguon
confidence # exact | derived | UNKNOWN
# luat cung: khong co effective_from thi khong duoc ghi vao
# luat cung: khong tim thay truong => ghi UNKNOWN, KHONG suy dien
# luat cung: mot "nhãn" xuat hien o 2 dia phuong => bat buoc co ban do nghiaCái bảng ánh xạ nghĩa đó là tài sản thật của dự án. Nó không đẹp, không demo được, không ai share. Nhưng nó là thứ duy nhất không copy được bằng cách chạy crawler.
Chỗ tiền nằm: không bán dữ liệu, bán cái bản đồ
Mình từng viết một bài về chuyện dữ liệu công khai thì ai cũng tải được, nên giá trị phải nằm ở phía sau nó. Trường hợp này là một ví dụ rất rõ.
Bán "bộ dữ liệu phân loại rác của 56 thành phố" thì rất nhanh bị đè giá, vì người thứ hai chỉ cần bỏ công gom lại. Nhưng có ba thứ không bị đè:
Bán câu trả lời đúng địa chỉ, không bán bảng tra. Người dùng không cần biết thành phố có mấy nhóm rác. Họ cần biết 7 giờ sáng mai cái hộp này để đâu. Đây là nấc "việc đã xong" chứ không phải nấc "bán dữ liệu".
Bán sự cập nhật, không bán nội dung. Đây là chỗ mình nghĩ hay nhất, và nó chỉ tồn tại vì luật đổi. Một bảng tra mua một lần là xong — nhưng một bảng tra đã lỗi thời thì tệ hơn không có bảng tra, vì người ta tin nó. Nghĩa là mình bán một thứ có thể thu tiền theo tháng một cách chính đáng: "luật chỗ anh vừa đổi, đây là phần đổi, và đây là những gì hệ của anh cần sửa." Không phải subscription vì mình muốn thu tiền đều. Subscription vì sản phẩm có hạn dùng.
Bán cho tổ chức đa địa điểm. Một chuỗi cửa hàng, một công ty có chi nhánh ở nhiều tỉnh, một dịch vụ chuyển nhà — họ không có vấn đề "tra cứu". Họ có vấn đề "cùng một quy trình mà mỗi nơi một luật". Đây là nơi cái bản đồ khác biệt có giá cao nhất, vì họ đang trả lương cho người ngồi đối chiếu tay.
Và cái đẹp của mô hình này: chi phí biên của một khách mới gần như bằng không, trong khi switching cost thì tăng theo thời gian — không phải vì code khoá họ, mà vì bản đồ đã tích luỹ mấy trăm cái "chỗ này khác chỗ kia" mà không ai dựng lại nổi trong một quý.
Đo thì cũng đừng đo bằng số bản ghi. Đo bằng hai con số khó chịu:
- ▹Số trường phải sửa tay trên 1.000 bản ghi. Con số này nói lên bản đồ của mình còn thiếu bao nhiêu.
- ▹Số lần luật đổi mà hệ chưa bắt được. Con số này nói lên mình có đang bán một thứ đã lỗi thời mà không biết hay không.
Con số thứ hai mình thấy ít người theo dõi, và nó là con số quyết định sống còn.
Một chi tiết nhỏ về cách hệ chạy khi luật đổi
Có một chỗ mình hay bị hỏi: luật đổi thì chạy lại toàn bộ pipeline à?
Chạy lại toàn bộ là phản xạ sai, và nó tốn tiền theo kiểu không ai thấy — vì log vẫn xanh, chỉ là hoá đơn tăng.
Thứ mình cần khi một nguồn đổi không phải là "chạy lại từ đầu", mà là biết chính xác phần nào đã xong, phần nào chưa, và phần nào không còn giá trị. Nghe thì đơn giản, nhưng nó đòi mỗi bước trong chuỗi phải để lại một dấu vết đọc được, gắn với phiên bản nguồn. Không có dấu vết đó thì mọi thay đổi đều bị xử lý như một lần bắt đầu lại.
Mình để ý mấy dự án nghiêm túc ở khoảng này đều đi cùng một hướng: biến cái luồng chạy thành thứ có thể đọc lại được, thay vì một chuỗi lệnh chạy một lần rồi quên. Nghe không hào nhoáng. Nhưng đó chính là phần giữ cho hoá đơn không nổ khi bạn có 56 nguồn thay vì 1.
Kết: cái bẫy lớn nhất là nghĩ mình đang làm sản phẩm dữ liệu
Mình vẫn còn tờ giấy phân loại rác cũ trong ngăn kéo. Thỉnh thoảng mở ra, thấy nó vô dụng — vì mình không còn sống ở đó nữa.
Nhưng nó là lời nhắc khá tốt: dữ liệu đúng, ở đúng cấp hành chính, vẫn có thể vô dụng với đúng người cần nó, chỉ vì nó không khớp cái khoá mà người ta đang có trong đầu.
Nếu anh em đang nghĩ tới một sản phẩm kiểu "gom hết lại một chỗ", mình xin góp một câu hỏi để tự soi trước khi viết dòng code đầu tiên:
Cái gì trong hệ của mình sẽ khác đi khi luật ở một trong các nguồn thay đổi — và mình biết chuyện đó bằng cách nào?
Trả lời được câu đó thì dự án có đường sống. Không trả lời được thì nó vẫn chạy, vẫn demo đẹp, chỉ là nó đã bắt đầu sai từ hôm qua rồi.
Anh em có ai từng làm sản phẩm mà "cùng một thứ, mỗi nơi một luật" chưa? Mình rất muốn nghe cái chỗ anh em phát hiện ra là hai bảng giống nhau nhưng không giống. Chỗ đó thường là chỗ đắt nhất, và hầu như không ai viết về nó.