Crawl 100,000 tin phòng trọ mỗi tuần và phân tích bằng Gemini AI
Mình build một nền tảng tìm phòng trọ tên Homigo (homigo.life). Nó tổng hợp tin đăng cho thuê từ nhiều nguồn (Facebook Groups, Chợ Tốt, Batdongsan), dùng AI phân tích và trích xuất thông tin, rồi cho người dùng lọc theo giá, quận, loại phòng, tiện ích.
Bài này mình chia sẻ kiến trúc kỹ thuật phía sau — cho anh em nào quan tâm đến crawling, AI pipeline, hoặc đang làm dự án tương tự.
Bài toán
Thị trường phòng trọ Việt Nam có đặc điểm:
- Nguồn tin chính là Facebook Groups — mỗi thành phố 100-200 group.
- Mỗi bài đăng viết tự do, không chuẩn hóa: "ptr 2tr5 hc wifi ml" = phòng trọ 2.5 triệu, Hải Châu, wifi, máy lạnh.
- Các nền tảng BĐS (Chợ Tốt, Batdongsan) có data chuẩn nhưng ít tin.
Mục tiêu: gom tất cả về 1 chỗ, chuẩn hóa, cho filter.
Tech Stack
- Runtime: Bun
- Backend: Elysia (Bun-native framework)
- Database: MongoDB
- Crawler: Puppeteer + puppeteer-extra-plugin-stealth
- AI: Google Gemini API
- Frontend: React + Vite
- Hosting: Cloudflare Pages (FE) + VPS (BE)
1. Crawler Architecture
Thách thức với Facebook
Facebook là nguồn tin lớn nhất nhưng cũng khó thu thập nhất. Mình đã research khá nhiều trước khi chọn approach:
- mbasic.facebook.com — phiên bản HTML thuần của Facebook, nhẹ, dễ parse. Nhưng Facebook đã ngừng hỗ trợ mbasic, nên hướng này không còn khả thi.
- API chính thức — Facebook Graph API không cho phép truy cập nội dung group theo kiểu này.
Cuối cùng mình dùng Puppeteer để mở browser, load group, rồi parse DOM trực tiếp để lấy nội dung bài đăng. Cách này đơn giản, nhưng phải maintain parser thường xuyên vì Facebook hay thay đổi cấu trúc HTML.
Cách hoạt động:
Browser mở group → scroll feed → Parse DOM lấy nội dung bài đăng → Lưu MongoDB
Multi-source crawl
Ngoài Facebook, mình crawl thêm Chợ Tốt và Batdongsan bằng cách gọi API hoặc parse HTML tùy nguồn. Mỗi nguồn có adapter riêng vì cấu trúc data khác nhau. Tất cả normalize về cùng 1 schema trước khi lưu.
Kết quả crawl
- 100,000+ tin/tuần từ tất cả các nguồn
- Tỷ lệ xử lý thành công: 82% (phần còn lại do timeout, tin trùng, hoặc group private)
2. AI Pipeline — Gemini Extraction
Sau khi crawl, mỗi bài đăng cần được phân tích để trích xuất structured data.
Input (bài đăng raw)
Cho thuê phòng trọ đường Lê Duẩn, Q. Hải Châu, ĐN
Dt 20m2, có wc riêng, wifi, máy lạnh, giá 2tr5/th
Ko chung chủ, giờ giấc tự do
Lh: 0905xxxxxx
Output (structured data)
{
"rentalPrice": 2500000,
"rentalArea": 20,
"rentalDistrict": "Hải Châu",
"rentalCity": "Đà Nẵng",
"rentalRoomType": "phòng trọ",
"rentalAmenities": ["wifi", "máy lạnh", "wc riêng"],
"isRental": true,
"isBroker": false
}
Gemini prompt
Mình dùng Gemini với structured output (JSON mode). Prompt yêu cầu extract các field cụ thể từ nội dung bài đăng tiếng Việt, bao gồm:
- Giá thuê (normalize về VND/tháng)
- Diện tích (m²)
- Quận/Huyện, Thành phố
- Loại phòng (trọ, căn hộ mini, ở ghép, nguyên căn, KTX)
- Tiện ích
- Chính chủ hay môi giới
Tỷ lệ chính xác: 80-90% — đủ tốt cho bộ lọc, nhưng mình vẫn đang cải thiện.
Multi-key rotation
Free tier Gemini có giới hạn request/phút. Để xử lý hàng nghìn bài/ngày, mình implement multi-key rotation:
Key pool: [key1, key2, key3, ..., keyN]
→ Mỗi request chọn key có ít usage nhất
→ Key nào bị rate limit → skip, dùng key tiếp
→ Atomic counter để tránh race condition
Đơn giản nhưng hiệu quả. Không bị bottleneck bởi 1 key.
3. Database & Query
Schema design
Mỗi post lưu cả raw data lẫn AI-extracted fields. Fields được flatten (không nested) để query nhanh:
rentalPrice, rentalArea, rentalDistrict, rentalCity,
rentalRoomType, rentalAmenities, isBroker, isRental,
createdTime, source, originalUrl
Compound indexes
Query chính là filter + sort, nên cần compound index:
{ rentalRoomType: 1, createdTime: -1, _id: -1 }
{ rentalPrice: 1, createdTime: -1, _id: -1 }
{ rentalDistrict: 1, createdTime: -1, _id: -1 }
Cursor-based pagination thay vì skip/limit — performance tốt hơn nhiều khi dataset lớn.
Aggregation pipeline cho thống kê giá
$match (filter by city/district/roomType)
→ $group (by district: avg, min, max, count)
→ $match (count >= 5, loại noise)
→ $sort (count desc)
Thêm whitelist district hợp lệ cho mỗi thành phố để loại tin bị AI extract sai quận.
4. Frontend
React + Vite, deploy trên Cloudflare Pages. Không có gì đặc biệt ngoài:
- Horizontal chip filters (kiểu Nhà Tốt)
- Map integration hiển thị vị trí tin đăng
- Dynamic OG tags cho SEO (Cloudflare Functions intercept bot traffic)
Kết quả
- 100,000+ tin/tuần thu thập
- 300 users/ngày, đang tăng
- 5,000+ lượt click "Xem bài gốc" (kết nối người thuê ↔ chủ trọ)
- 1 người build và vận hành
Bài học kỹ thuật
- GraphQL intercept > DOM parsing cho Facebook. DOM thay đổi liên tục, GraphQL response ổn định hơn nhiều.
- Flatten schema cho MongoDB khi cần filter nhiều fields. Nested objects + query = chậm.
- Multi-key rotation giải quyết rate limit AI API đơn giản mà hiệu quả.
- Cursor pagination bắt buộc cho dataset lớn. Skip/limit chết ở page 100+.
Sản phẩm: homigo.life App Store & Google Play: Search "Homigo"
Anh em có câu hỏi về phần nào thì comment, mình sẽ đi sâu thêm.
All Rights Reserved