Tự xây pipeline theo dõi thị trường việc làm ngành data engineering

Công nghệ21 tháng 10, 2026·8 phút đọc

Một kỹ sư đang chuyển hướng sang data engineering kể lại quá trình xây pipeline giám sát thị trường tuyển dụng bằng DuckDB, dbt snapshot và incremental loading. Bài viết tiết lộ cú lừa từ API của Remotive, khiến anh phải chuyển sang RemoteOK giữa chừng, cùng bài học về SCD Type 2 và hard deletes.

Tự xây pipeline theo dõi thị trường việc làm ngành data engineering

Có một nghịch lý khá thú vị: bạn chưa được nhận vào vị trí mình mong muốn, nhưng lại dành thời gian xây dựng hạ tầng cấp production để theo dõi chính thị trường tuyển dụng cho vị trí đó. Với những người đang học data engineering theo hướng "learning in public", đây lại chính là cách học hiệu quả nhất.

Một kỹ sư đang chuyển từ hệ thống phân tích sang data engineering vừa chia sẻ dự án thứ ba trong hành trình của mình. Lần này, anh xây một pipeline chuyên theo dõi các vị trí data engineer, analytics engineer, ETL developer và những vai trò liên quan đang mở trên thị trường.

Chọn stack và cú "va tường" đầu tiên

Trước khi bắt tay vào code, anh khảo sát xem nhà tuyển dụng thực sự tìm kiếm công cụ gì. Kết quả khá rõ ràng:

  • Apache Airflow vẫn chiếm ưu thế áp đảo trong các tin tuyển dụng
  • Kestra, Dagster và Prefect đều đang phát triển nhưng xuất hiện ít hơn
  • Với warehouse, BigQuery và Snowflake là hai cái tên được nhắc nhiều nhất

Ban đầu anh chọn Airflow làm orchestrator và BigQuery làm warehouse. Nhưng kế hoạch nhanh chóng gặp trở ngại. Google Cloud yêu cầu khoản trả trước 30 USD bắt buộc để liên kết tài khoản thanh toán ở một số quốc gia — ngay cả khi dùng gói miễn phí. Với một dự án học tập, đây là khoản đầu tư không đáng.

"DuckDB không mang trọng lượng như BigQuery trên CV. Nhưng dbt trừu tượng hóa hầu hết cú pháp đặc thù của từng warehouse, nên kỹ năng transform và snapshot tôi học được vẫn chuyển giao nguyên vẹn."

Anh chuyển sang DuckDB — nhẹ hơn, không cần thanh toán, và vẫn học được đầy đủ các khái niệm cốt lõi.

Hacker News và bài học về incremental loading

Nguồn dữ liệu đầu tiên là Hacker News, được chọn vì API trả về ID tăng dần cho mỗi story — trường hợp lý tưởng cho incremental loading.

Incremental loading là kỹ thuật chỉ nạp dữ liệu mới hoặc đã thay đổi thay vì nạp lại toàn bộ. Trước dự án này, mọi pipeline anh từng xây đều kéo toàn bộ dữ liệu mỗi lần chạy. Cách đó ổn ở quy mô nhỏ nhưng sụp đổ ngay khi bạn quan tâm đến hiệu suất hoặc giới hạn tốc độ API.

Cách làm đơn giản: lưu lại một file nhỏ ghi nhớ ID story cao nhất đã xử lý, và mỗi lần chạy chỉ lấy những ID lớn hơn mốc đó.

Kết quả rất thuyết phục: lần chạy đầu kéo 500 story, lần chạy sau một phút chỉ kéo 6 story. Đúng như mong đợi.

Một chi tiết nhỏ đáng chú ý: Hacker News trả về trường by cho tác giả. Đây là từ khóa dành riêng trong SQL, nên DuckDB từ chối tạo bảng cho đến khi trường này được đặt trong dấu nháy kép ở mọi nơi xuất hiện.

Cú lừa từ API của Remotive

Đây có lẽ là bài học giá trị nhất của cả bài viết.

Ban đầu anh chọn Remotive cho nguồn việc làm vì có API miễn phí, tài liệu rõ ràng, hỗ trợ tham số tìm kiếm và phân loại. Anh viết extractor truy vấn sáu chức danh khác nhau và gộp kết quả lại.

Vấn đề: mọi truy vấn đều trả về đúng 15 công việc giống hệt nhau, cùng tiêu đề, cùng thứ tự, bất kể tìm gì.

Anh nghi ngờ rate limiting vì tài liệu Remotive khuyến cáo không quá 2 request mỗi phút, còn anh thì bắn 6 request liên tiếp. Anh viết lại extractor để chỉ gửi một request duy nhất — vẫn ra đúng 15 việc đó.

Sau đó anh chạy kiểm tra trực tiếp với tham số limit=3:

req = Request('https://remotive.com/api/remote-jobs?limit=3', headers={'User-Agent': 'Mozilla/5.0'})
data = json.loads(urlopen(req).read())
print('job-count field:', data.get('job-count'))  # 15
print('jobs returned:', len(data.get('jobs', [])))  # 15

Vẫn là 15. Chính trường job-count của API cũng khai báo tổng là 15. Mọi tham số đều bị bỏ qua — search, category, limit không có tác dụng gì. API công khai của Remotive chỉ trả về một mẫu cố định chung chung.

Đây không phải lỗi trong code của anh. Anh đã xác minh trực tiếp nhiều lần từ máy của mình trước khi chấp nhận sự thật: nguồn dữ liệu thứ hai của toàn bộ dự án đã hỏng, và anh phải chuyển hướng giữa chừng.

Chuyển sang RemoteOK và lọc hai lớp

RemoteOK là API việc làm miễn phí lâu năm. Nhưng trước khi xây bất cứ thứ gì, anh kiểm tra xem nó có trả dữ liệu tươi hay không.

RemoteOK có tham số tags nhưng không mấy chính xác. Một công việc có thể gắn 30-40 tag kỹ năng chung chung, nên tìm data có thể trả về cả một vị trí "Lead Paralegal". Do đó anh tách extraction thành hai bước:

  • Dùng tags=data để lấy tập ứng viên rộng
  • Dùng Python kiểm tra tiêu đề với danh sách từ khóa chặt chẽ hơn
ROLE_KEYWORDS = [
    "data engineer", "analytics engineer", "etl", "data platform",
    "business intelligence", "big data", "data scientist", "data analyst",
]

def matches_target_role(job: dict) -> bool:
    title = (job.get("position") or "").lower()
    return any(keyword in title for keyword in ROLE_KEYWORDS)

Kết quả: trong 100 công việc, chỉ 16 là thực sự liên quan. Bài học rõ ràng — bộ lọc của API không đủ dùng một mình, cần kết hợp với kiểm tra chặt chẽ ở phía ứng dụng.

Theo dõi công việc đóng lại với dbt snapshot

Đây là phần cốt lõi mà cả dự án hướng tới.

Cả hai extractor ghi trạng thái hiện tại vào DuckDB mỗi lần chạy. Với RemoteOK, loader thay thế toàn bộ bảng vì API chỉ cho biết việc nào đang mở ngay lúc này. Tự thân bảng đó không bao giờ cho biết một công việc từng tồn tại — nếu tin tuyển dụng biến mất giữa hai lần chạy, nó đơn giản là mất tích, không dấu vết.

dbt snapshot giải quyết vấn đề này bằng cách so sánh bảng hiện tại với lần trước và lưu lại lịch sử thay vì ghi đè. Đây chính là Slowly Changing Dimensions (SCD) Type 2 — khái niệm mà mọi tin tuyển dụng data engineering đều nhắc đến nhưng gần như chẳng tutorial nào cho thấy nó hoạt động thực tế.

Hãy hình dung: một khách hàng tên Sarah sống ở Chicago. Vài tháng sau cô chuyển đến New York. Nếu chỉ cập nhật thành phố mới, ta mất thông tin cô từng sống ở Chicago. Nhưng nếu doanh nghiệp cần phân tích doanh số theo nơi khách ở tại thời điểm mua hàng thì sao?

SCD Type 2 giữ lại bản ghi cũ và tạo bản ghi mới với thông tin cập nhật, dùng ngày tháng và cờ trạng thái để đánh dấu phiên bản nào có hiệu lực khi nào.

Cấu hình snapshot quan trọng nhất nằm ở dòng hard_deletes='invalidate':

{{ config(
    target_schema='snapshots',
    unique_key='job_id',
    strategy='check',
    check_cols=['job_title', 'company', 'tags', 'location', 'salary_min', 'salary_max', 'url'],
    hard_deletes='invalidate',
) }}
select * from {{ ref('stg_remoteok_jobs') }}

Mặc định dbt snapshot bỏ qua các dòng biến mất khỏi nguồn. Thiết lập này nói điều ngược lại: nếu một job_id tồn tại lần trước mà vắng mặt lần này, đóng dòng đó lại bằng timestamp dbt_valid_to thay vì âm thầm xóa.

Anh đã kiểm thử thủ công trước khi tin tưởng nó, chạy snapshot với hai tin giả lập cùng mở, rồi giả lập một tin đóng và một tin mới xuất hiện:

('2001', 'Senior Data Engineer',  dbt_valid_from=run1, dbt_valid_to=None)  # vẫn mở
('2002', 'Analytics Engineer II', dbt_valid_from=run1, dbt_valid_to=run2)  # đóng giữa hai lần chạy
('2003', 'ETL Developer',         dbt_valid_from=run2, dbt_valid_to=None)  # mở lần chạy này

Đúng hành vi mong muốn, và dbt xử lý toàn bộ mà không cần viết một dòng logic so sánh nào.

Anh cũng bổ sung test ngoài mặc định của dbt: ngoài kiểm tra not-null và uniqueness tiêu chuẩn, có một test tùy chỉnh báo lỗi lớn nếu bảng RemoteOK trả về 0 dòng — đúng kiểu thất bại âm thầm mà Remotive vừa dạy anh phải cảnh giác. Cùng với đó là kiểm tra độ tươi của dữ liệu, cảnh báo nếu nguồn nào chưa nạp lại trong 24 giờ.

Bước tiếp theo

Hiện cả hai nguồn vẫn chạy thủ công, từng script một. Phần tiếp theo sẽ gói các extractor và lệnh dbt vào một DAG của Airflow — nghĩa là học mô hình tư duy của orchestrator thứ hai sau một năm chỉ biết Kestra.

Khi pipeline chạy theo lịch, câu hỏi thú vị hơn mở ra: dữ liệu thực sự nói lên điều gì? Vai trò nào mở lâu nhất, chức danh nào xuất hiện và biến mất nhanh nhất? Tin "data engineer" và "analytics engineer" có hành vi khác nhau không? Đó là phần mà pipeline không còn là bài tập nữa, mà trở thành tấm gương soi vào chính hành trình tìm việc của tác giả.

Với độc giả Việt Nam đang chuyển hướng sang data engineering, đây là ví dụ đáng tham khảo: bạn không cần hạ tầng đắt tiền hay tài khoản cloud trả phí để học những khái niệm cốt lõi như incremental loading, SCD Type 2 hay orchestration. DuckDB, dbt và một chút kiên nhẫn là đủ để xây thứ gì đó vừa học được vừa dùng được thật.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗