Lấy xếp hạng từ khóa qua Places API
Tổng quan
| Mục | Nội dung |
|---|---|
| Trạng thái | 🔵 Đề xuất |
| Issue | - |
| Phụ trách | - |
Chức năng lấy xếp hạng tìm kiếm cho từng từ khóa bằng Google Places API (Text Search). Chạy song song với hệ thống scraping hiện tại, dữ liệu xử lý được phân biệt bằng data_source trên mappy_gbp_locations (1 = Places API, 2 = Scraping).
| Mục | Nội dung |
|---|---|
| Phương thức | Google Places API (New) — Text Search |
| Chi phí | $0 (Field Mask: places.id → Basic SKU miễn phí) |
| Đối tượng | Cả 3 hệ thống (MAPPY / GCOR / PIPIT) |
| Tần suất | 1 lần/ngày (cùng thời điểm với scraping) |
| Thực thi | Python script (main.py) |
Nội dung đề xuất
Bối cảnh & Vấn đề
- Hệ thống scraping hiện tại dùng Playwright để mở Google Search → gặp nhiều vấn đề: reCAPTCHA, timeout, anti-detection phức tạp
- Tốn tài nguyên server để chạy headless browser
- Kết quả không ổn định (phụ thuộc DOM, layout Google thay đổi thường xuyên)
- Cần phương pháp bổ sung để đối chiếu và tăng độ tin cậy của ranking
Giải pháp đề xuất
Sử dụng Google Places API (New) — Text Search để lấy ranking thông qua place_id. Chạy song song với scraping, lưu vào cùng bảng mappy_* hiện có.
Đặc điểm chính:
- Gọi API đơn giản (HTTP POST), không cần browser
- Chi phí $0 khi chỉ lấy
places.id(Basic SKU) - So khớp chính xác bằng
placeId(đã có sẵn trongraw_jsontạimetadata.placeId) - Không bị ảnh hưởng bởi reCAPTCHA hay thay đổi DOM
Danh sách chức năng
| # | Chức năng | Mô tả | Ưu tiên |
|---|---|---|---|
| 1 | Trích xuất placeId | Lấy metadata.placeId từ raw_json của mappy_gbp_locations | Cao |
| 2 | Text Search ranking | Gọi API, tìm vị trí place_id trong kết quả | Cao |
| 3 | Ranking mapping | Lưu vị trí trực tiếp (1-based), 0 = ngoài phạm vi, 997 = lỗi | Cao |
| 4 | Multi API key | Nhiều API key, mỗi key có --threads thread riêng. Tổng thread = threads × số key | Cao |
| 5 | Rate limiting | --min_interval kiểm soát tần suất gọi API (độc lập mỗi thread) | Trung bình |
| 6 | 429 Retry | --max_retries tự động retry khi bị rate limit | Trung bình |
| 7 | data_source filter | Chỉ xử lý location có data_source=1 (API) | Cao |
Tech Stack
| Hạng mục | Công nghệ | Lý do chọn |
|---|---|---|
| Ngôn ngữ | Python 3.10+ | Cùng ngôn ngữ với scraping hiện tại, requests gọi API đơn giản |
| HTTP | requests | Nhẹ, đồng bộ, tương thích tốt với thread |
| DB | SQLAlchemy + PyMySQL | Cùng ORM với scraping, hỗ trợ shared DB pool |
| Log upload | boto3 | Upload log file lên S3 |
| Môi trường | Docker (Amazon ECS) | Cùng infra với scraping, 1 CPU / 2GB RAM |
| Scheduler | Amazon EventBridge | Trigger batch hàng ngày (giống hiện tại) |
Cấu hình Docker
FROM python:3.10-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV TZ=Asia/Tokyo
RUN apt-get update && \
apt-get install -y --no-install-recommends tzdata && \
ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && \
echo $TZ > /etc/timezone && \
apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /code
COPY requirements.txt /code/
RUN pip install --upgrade pip && pip install -r requirements.txt
COPY . /code/
CMD ["python", "main.py", "--threads", "4", "--type_keyword", "all", "--min_interval", "500"]# requirements.txt
PyMySQL==1.1.2
SQLAlchemy==2.0.44
SQLAlchemy-Utils==0.42.0
python-dotenv==1.0.1
requests==2.32.5
cryptography==46.0.3
boto3==1.38.0Ví dụ chạy:
# Local
docker build -t places-api-ranking .
docker run --env-file .env places-api-ranking
# Override arguments
docker run --env-file .env places-api-ranking \
python main.py --threads 8 --type_keyword normal
# ECS Task Definition (cùng cấu hình với scraping)
# CPU: 1024 (1 vCPU), Memory: 2048 (2GB)
# Override command: ["python", "main.py", "--threads", "4", "--type_keyword", "all"]Cấu hình AWS Infrastructure
Sử dụng chung AWS account (881980194724) và region (ap-northeast-1) với hệ thống scraping hiện tại.
ECR (Container Registry)
| Mục | Giá trị |
|---|---|
| Repository | python310 (đã có sẵn) |
| URI | 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com/python310 |
| Tag | :latest |
# Build & Push
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t places-api-ranking .
docker tag places-api-ranking:latest 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com/python310:latest
docker push 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com/python310:latestBatch Job Definition
Tạo job definition mới, tham khảo scraper_def hiện tại.
| Mục | Giá trị |
|---|---|
| Tên | places_api_def |
| Loại | container |
| Platform | Fargate / LINUX / X86_64 |
| Image | 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com/python310:latest |
| vCPU | 1.0 |
| Memory | 2048 MB |
| Execution Role | ecsTaskExecutionRole (dùng chung với scraper) |
| Timeout | 21600 giây (6 tiếng) |
| Network | assignPublicIp: ENABLED |
| Command | [] (Lambda truyền khi submit) |
EventBridge (Scheduler)
| Mục | Giá trị |
|---|---|
| Rule name | places-api-ranking-daily |
| Schedule | Cùng thời điểm với scraping hiện tại (hàng ngày) |
| Target | Lambda function (tích hợp vào flow sync_database → Batch submit → sync_result) |
※ Biến môi trường (
PLACES_API_KEYS...) được truyền qua containerOverrides khi Lambda submit Batch job (giống cách scraping hiện tại).
Sơ đồ cấu trúc
Biến môi trường
| Tên biến | Bắt buộc | Mô tả |
|---|---|---|
GOOGLE_PLACES_API_KEYS | Có | API keys Google Places (phân cách bằng dấu phẩy). Mỗi key nên từ GCP project riêng |
GOOGLE_PLACES_API_KEY | Có* | API key đơn lẻ (fallback nếu không set GOOGLE_PLACES_API_KEYS) |
PLACES_API_BASE_URL | Không | Override URL endpoint (mặc định: https://places.googleapis.com) |
SCRAPER_MYSQL_HOST | Có | MySQL host |
SCRAPER_MYSQL_DATABASE | Có | MySQL database name |
SCRAPER_MYSQL_USER | Có | MySQL user |
SCRAPER_MYSQL_PASSWORD | Có | MySQL password |
SCRAPER_MYSQL_PORT | Không | MySQL port (mặc định: 3306) |
SCRAPER_ACCESS_KEY_ID | Có | AWS access key cho upload S3 |
SCRAPER_SECRET_ACCESS_KEY | Có | AWS secret key |
SCRAPER_SCRAPING_BUCKET | Có | S3 bucket cho log |
Lưu ý: Mỗi API key nên từ GCP project riêng để có QPM quota độc lập. Các key cùng project chia sẻ cùng 1 QPM limit.
Flow tổng thể
Mapping giá trị ranking
Kết quả từ Text Search API lưu trực tiếp vị trí tìm thấy (1-based).
| Kết quả | Giá trị ranking | Ý nghĩa |
|---|---|---|
| Tìm thấy ở vị trí N | N (1~60) | Thứ hạng thực tế |
| Không tìm thấy (hết 60 kết quả) | 0 | Ngoài phạm vi |
| Lỗi API (sau khi retry hết) | 997 | Lỗi không mong đợi |
Khác với scraping: Scraping dùng -11/-12/-13 cho Local Pack top 3. Places API không phân biệt Local Pack, lưu thứ hạng thực tế.
Ranking status
| Giá trị | Hằng số | Ý nghĩa |
|---|---|---|
| 1 | STATUS_SUCCESS | Tìm thấy trong kết quả |
| 2 | STATUS_OUT_OF_RANGE | Không tìm thấy (ranking = 0) |
| 3 | STATUS_UNEXPECTED_ERROR | Lỗi API (ranking = 997) |
Thiết kế Database
Chiến lược: Dùng chung bảng mappy_*
Places API và scraping ghi kết quả vào cùng bảng mappy_*. Hai hệ thống không xung đột vì mỗi bên xử lý tập location riêng: cột data_source trên mappy_gbp_locations quyết định location nào thuộc hệ thống nào.
| Bảng | Vai trò | Ghi chú |
|---|---|---|
mappy_search_ranking_update_logs | Log thực thi theo ngày | 1 record/ngày |
mappy_search_ranking_processes | Theo dõi process (normal/reverse) | type: 0=normal, 1=reverse |
mappy_temp_search_rankings | Kết quả ranking thô (1 record/keyword) | Dữ liệu chính |
mappy_search_ranking_exports | Dữ liệu export tổng hợp (1 record/location) | JSON array, tối đa 8 keyword |
Cột data_source trên mappy_gbp_locations
Cột data_source dùng để phân chia tập location cho mỗi hệ thống xử lý. Không phải để phân biệt kết quả ranking.
| Giá trị | Hằng số | Ý nghĩa |
|---|---|---|
| 1 | DATA_SOURCE_API | Location do Places API xử lý |
| 2 | DATA_SOURCE_RANK_SEARCH | Location do scraping xử lý |
- Places API chỉ query keyword của location có
data_source = 1 - Scraping chỉ query keyword của location có
data_source = 2 - Kết quả ranking của cả hai hệ thống đều lưu vào cùng bảng
mappy_temp_search_rankings,mappy_search_ranking_exports
Trích xuất place_id
Lấy trực tiếp từ mappy_gbp_locations.raw_json tại metadata.placeId. Không cần thêm cột mới.
import json
# Ví dụ trích xuất từ raw_json
raw = json.loads(raw_json)
placeId = raw["metadata"]["placeId"] # "ChIJg9Jc9EiHGGARCDHlvl19Coo"
lat = raw["latlng"]["latitude"] # 35.6928321
lng = raw["latlng"]["longitude"] # 139.9285475Xác định tọa độ tìm kiếm
Ưu tiên lấy tọa độ theo thứ tự:
- User settings:
mappy_user_settings.settings→keywordSettings.locations[location_id] - raw_json:
mappy_gbp_locations.raw_json→latlng.latitude/longitude - Default: Tokyo area (35.6441649, 139.347756) — fallback khi cả 2 nguồn trên không có
ER Diagram
Định nghĩa bảng: mappy_temp_search_rankings
| No | Tên cột (logic) | Tên cột (vật lý) | Kiểu dữ liệu | NULL | Key | Mô tả |
|---|---|---|---|---|---|---|
| 1 | ID | id | int | NO | PK | Tự tăng |
| 2 | ID log thực thi | update_log_id | int | NO | IDX | mappy_search_ranking_update_logs.id |
| 3 | ID process | process_id | int | NO | IDX | mappy_search_ranking_processes.id |
| 4 | User ID | user_id | int | NO | IDX | mappy_users.id |
| 5 | Location ID | gbp_location_id | int | NO | IDX | mappy_gbp_locations.id |
| 6 | Keyword ID | keyword_id | int | NO | IDX | mappy_keywords.id |
| 7 | System ID | system_id | int | NO | - | 1:MAPPY / 2:GCOR / 3:PIPIT |
| 8 | Ranking | ranking | int | YES | - | Vị trí (1-60), 0=ngoài phạm vi, 997=lỗi |
| 9 | Ranking status | ranking_status | tinyint | YES | - | 1:thành công / 2:ngoài phạm vi / 3:lỗi |
| 10 | Ngày ranking | ranking_at | date | NO | - | Ranking của ngày nào |
| 11 | Thời điểm gọi API | search_at | datetime | NO | - | Thời điểm thực tế gọi API |
| 12 | Ngày tạo | created_at | datetime | NO | - | Thời điểm tạo record |
| 13 | Ngày cập nhật | updated_at | datetime | NO | - | Thời điểm cập nhật record |
Định nghĩa bảng: mappy_search_ranking_exports
Export data là JSON array tổng hợp cho 1 location, chứa tối đa 8 keyword và thống kê top 3/10/20.
| No | Tên cột (logic) | Tên cột (vật lý) | Kiểu dữ liệu | NULL | Key | Mô tả |
|---|---|---|---|---|---|---|
| 1 | ID | id | int | NO | PK | Tự tăng |
| 2 | ID log thực thi | update_log_id | int | NO | - | mappy_search_ranking_update_logs.id |
| 3 | ID process | process_id | int | NO | IDX | mappy_search_ranking_processes.id |
| 4 | User ID | user_id | int | NO | IDX | mappy_users.id |
| 5 | Location ID | gbp_location_id | int | NO | IDX | mappy_gbp_locations.id |
| 6 | System ID | system_id | int | NO | - | 1:MAPPY / 2:GCOR / 3:PIPIT |
| 7 | Ngày ranking | ranking_at | date | NO | - | Ranking của ngày nào |
| 8 | Data | data | longtext | NO | - | JSON array (xem cấu trúc bên dưới) |
| 9 | Ngày tạo | created_at | datetime | NO | - | |
| 10 | Ngày cập nhật | updated_at | datetime | NO | - |
Cấu trúc JSON data:
[
"2026-04-07T10:30:00", // datetime
"user_login_id", // login_id
"store_001", // store_code
123, // gbp_location_id
"1234567890", // location_name_id
"Tên cửa hàng", // display_name
1, // flag
4, // keyword_count
"keyword1", "keyword2", "keyword3", "keyword4", "", "", "", "", // 8 keyword slots
35.6595, // lat
139.7004, // lng
null, // reserved
3, 15, 0, null, null, null, null, null, // 8 ranking slots
null, // reserved
1, 2, // has_top3, top3_count
1, 3, // has_top10, top10_count
1, 4, // has_top20, top20_count
1 // system_id
]Flow xử lý 1 keyword
CLI Arguments
python main.py [options]| Argument | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
--threads | int | 4 | Số thread mỗi API key. Tổng thread = threads × số key |
--type_keyword | str | all | all (normal+reverse) / normal / reverse |
--min_interval | int | 500 | Khoảng cách tối thiểu giữa các API request (ms, mỗi thread) |
--max_retries | int | 3 | Số lần retry khi gặp 429 |
--retry_delay | int | 5000 | Thời gian chờ trước mỗi retry (ms) |
--max-groups | int | 0 | Giới hạn số group xử lý (0=unlimited, dùng để test) |
Quan trọng:
--threadslà số thread mỗi key, không phải tổng. Ví dụ: 3 key +--threads 4= 12 thread tổng.
Spec API Text Search
Endpoint
POST {PLACES_API_BASE_URL}/v1/places:searchTextMặc định PLACES_API_BASE_URL=https://places.googleapis.com. Có thể override bằng env var để test với mock server.
Headers
Content-Type: application/json
X-Goog-Api-Key: {API_KEY}
X-Goog-FieldMask: places.id,nextPageTokenRequest Body
{
"textQuery": "渋谷 歯医者",
"languageCode": "ja",
"locationBias": {
"circle": {
"center": {
"latitude": 35.6595,
"longitude": 139.7004
},
"radius": 5000.0
}
}
}Response mẫu
{
"places": [
{ "id": "ChIJ..." },
{ "id": "ChIJ..." },
{ "id": "ChIJ..." }
],
"nextPageToken": "..."
}Logic tính ranking
def find_ranking(places, place_id):
"""Tìm vị trí của place_id trong kết quả API.
Returns:
Vị trí (1-based) nếu tìm thấy, 0 nếu không.
"""
if not places or not place_id:
return 0
for index, place in enumerate(places):
if place.get("id") == place_id:
return index + 1
return 0Sequence Diagram
Xử lý lỗi
| Lỗi | HTTP Status | Xử lý |
|---|---|---|
| Vượt quota | 429 | Chờ retry_delay ms rồi retry (tối đa max_retries lần), hết retry → ranking=997 + ERROR log |
| Lỗi xác thực | 401 | PlacesAPIError type=AUTH_ERROR, ranking=997 |
| Bị chặn | 403 | PlacesAPIError type=BLOCKED, ranking=997 |
| Server error | 5xx | PlacesAPIError type=UNEXPECTED, ranking=997 |
| Timeout | - | PlacesAPIError type=TIMEOUT, ranking=997 |
| Lỗi kết nối | - | PlacesAPIError type=UNEXPECTED, ranking=997 |
Xử lý lỗi DB: Context manager get_db() tự rollback khi exception và re-raise lên caller.
Multi API Key và xử lý Thread
Thread model
--threads là số thread mỗi key. Tổng thread = --threads × số key.
# 1 key + 4 threads/key = 4 thread tổng
GOOGLE_PLACES_API_KEYS=keyA --threads 4
Thread-0 ← keyA Thread-1 ← keyA
Thread-2 ← keyA Thread-3 ← keyA
# 3 key + 4 threads/key = 12 thread tổng
GOOGLE_PLACES_API_KEYS=keyA,keyB,keyC --threads 4
chunk 0→keyA chunk 1→keyB chunk 2→keyC chunk 3→keyA
chunk 4→keyB chunk 5→keyC chunk 6→keyA chunk 7→keyB
chunk 8→keyC chunk 9→keyA chunk 10→keyB chunk 11→keyCCách scale: Thêm key vào env var → tự động tăng thread tổng. Không cần sửa code.
Group và phân phối chunk
Toàn bộ keyword được query 1 lần, group theo user_id × gbp_location_id × system_id, rồi chia thành chunks liên tục (không phải round-robin).
Tổng: 120 groups, 12 threads (3 key × 4 threads/key)
chunk_size = ceil(120/12) = 10
Thread-0: group 0-9 (keyA)
Thread-1: group 10-19 (keyB)
Thread-2: group 20-29 (keyC)
Thread-3: group 30-39 (keyA)
...
Thread-11: group 110-119 (keyC)Ưu điểm:
- Không chia cắt group (lấy toàn bộ → group → phân phối, đơn giản)
- Normal và reverse chạy liên tiếp trong cùng group (chia sẻ
request_timer) - Mỗi thread có rate limiter độc lập
- 1 DB pool dùng chung (
pool_size=15,max_overflow=10, tối đa 25 connections)
Rate Limiting & Quota
Google Places Text Search API mặc định 600 QPM (Queries Per Minute). Thực tế Google áp dụng burst limit ~10 req/giây, không chỉ theo phút.
Dữ liệu thực tế (2026/03/25): 4 batch peak ~395 req/phút (dưới 600) nhưng vẫn bị 429. Nguyên nhân: 4 batch × 3.41 req/giây = 13.65 req/giây → vượt 10 req/giây burst limit.
Cách min_interval hoạt động
Trước mỗi API call (bao gồm cả pagination pages):
elapsed = now - request_timer
if elapsed < min_interval:
sleep(min_interval - elapsed)
request_timer = nowrequest_timerđược chia sẻ giữa normal và reverse trong cùng group- Mỗi thread có
request_timerđộc lập - Pagination pages (trang 2, 3) cũng được rate-limit
Ước tính throughput
| Số key | --threads | --min_interval | Tổng threads | Tổng req/s | Tổng QPM | Đánh giá |
|---|---|---|---|---|---|---|
| 1 | 4 | 500ms | 4 | 8 | 480 | Cấu hình ban đầu |
| 2 | 4 | 500ms | 8 | 16 | 960 | Khuyến nghị |
| 3 | 4 | 500ms | 12 | 24 | 1440 | Max ổn định trên 2GB RAM |
| 1 | 4 | 300ms | 4 | ~13 | ~800 | Gần burst limit |
Flow retry 429
Lần 1 → 429 → chờ retry_delay → Lần 2 → 429 → chờ → ... → Lần max_retries+1- Mỗi lần retry ghi WARNING log
- Hết retry vẫn fail → ranking=997, ghi ERROR log
- Sau
sleep(retry_delay)resetrequest_timer
Giải pháp tăng throughput
Thêm API key (khuyến nghị)
Mỗi Google Cloud project có rate limit riêng (~10 req/giây). Chỉ cần thêm key vào GOOGLE_PLACES_API_KEYS.
# Cấu hình ban đầu (1 key × 4 threads = 4 threads)
GOOGLE_PLACES_API_KEYS=keyA --threads 4 # ~8 req/s
# Scale up (3 key × 4 threads = 12 threads)
GOOGLE_PLACES_API_KEYS=keyA,keyB,keyC --threads 4 # ~24 req/s| Số key | threads/key | Tổng threads | Rate limit tổng | Thời gian (49,912 req) | RAM |
|---|---|---|---|---|---|
| 1 | 4 | 4 | 10 req/giây | 83 phút | ~160MB |
| 2 | 4 | 8 | 20 req/giây | 42 phút | ~240MB |
| 3 | 4 | 12 | 30 req/giây | 28 phút | ~320MB |
| 4 | 4 | 16 | 40 req/giây | 21 phút | ~400MB |
Xin tăng quota
Qua Google Cloud Console → Quotas → Increase Requests. IDs Only SKU (miễn phí) nên khả năng duyệt cao. Review 1~2 ngày.
Kết hợp: 3 key × quota tăng lên 1,200 QPM/key = 60 req/giây → 49,912 req xử lý trong 14 phút.
Log
Log file
- Thư mục:
log/debug_aws/ - Combined log:
{YYYY-MM-DD_HH-MM-SS}_total.log(tất cả thread) - Per-key log:
{YYYY-MM-DD_HH-MM-SS}_key{suffix}.log(lọc theo API key) - Upload S3:
s3://{bucket}/log/apigoogle/{filename}
Log format
2026-04-07 10:30:18,015 [INFO][T1][...a1b2] [1/1212] location='渋谷歯科', keywords=8
2026-04-07 10:30:18,378 [DEBUG][T1][...a1b2] [uid=1 sys=1 loc=123] [N][1/8] '渋谷 歯医者': rank=1, results=60
2026-04-07 10:30:18,900 [WARNING][T2][...c3d4] [uid=2 sys=1 loc=456] [R][3/8] '歯医者 渋谷': 429 rate limited (attempt 1/4), waiting 5000ms...
2026-04-07 10:30:23,910 [INFO][T2][...c3d4] [uid=2 sys=1 loc=456] [R][3/8] '歯医者 渋谷': retry succeeded (attempt 2/4)| Field | Ý nghĩa |
|---|---|
[T{n}] | Thread số n |
[...{suffix}] | 4 ký tự cuối của API key |
[uid=X sys=Y loc=Z] | Context: user_id, system_id, gbp_location_id |
[N]/[R] | Normal / Reverse |
[i/total] | Keyword thứ i trong group |
attempt X/Y | Lần thử X trên tổng Y |
Giới hạn tài nguyên (1 CPU / 2GB RAM)
GIL và I/O-bound
Workload này là I/O-bound nên thread vẫn hiệu quả dù có GIL:
Mỗi keyword: ~293ms (thực tế trung bình) Chờ HTTP response từ Google API
~5-10ms Ghi DB
< 1ms Python code (parse JSON, tính rank)→ CPU idle ~98% thời gian → nhiều thread chạy song song hiệu quả.
Ước tính tài nguyên
| Tài nguyên | Ước tính |
|---|---|
| RAM / thread | ~15-20MB |
| RAM base process | ~80MB |
| RAM (4 threads, 1 key) | ~80 + 4×20 = ~160MB |
| RAM (12 threads, 3 key) | ~80 + 12×20 = ~320MB |
| CPU trung bình | < 20% |
| DB connections | pool_size=15, max_overflow=10, tối đa 25 |
Cấu hình khuyến nghị
| Số key | --threads | Tổng threads | RAM | Tổng QPM | Đánh giá |
|---|---|---|---|---|---|
| 1 | 4 | 4 | ~160MB | 480 | Cấu hình ban đầu |
| 2 | 4 | 8 | ~240MB | 960 | Khuyến nghị |
| 3 | 4 | 12 | ~320MB | 1440 | Max ổn định trên 2GB |
| 4 | 4 | 16 | ~400MB | 1920 | Cần tăng QPM quota |
Cấu hình ban đầu: GOOGLE_PLACES_API_KEYS=key1 + --threads 4 --min_interval 500
Cấu trúc source code
places_api/
├── main.py # Entry point: CLI args, orchestration, threading
├── services/
│ └── places_api.py # Google Places API client (search_text, find_ranking)
├── database/
│ ├── database.py # SQLAlchemy engine, session, get_db() context manager
│ ├── models.py # ORM models (mappy_* tables)
│ └── repositories.py # DB queries (CRUD operations)
├── logger.py # Structured logging (per-thread, per-key file handlers)
├── migrations/
│ └── gplaces_tables.sql # DDL cho các bảng mới
├── Dockerfile
├── requirements.txt
└── .env.exampleTest cases
| # | Nội dung test | Cách kiểm tra |
|---|---|---|
| 1 | Trích xuất placeId | Lấy đúng giá trị từ raw_json.metadata.placeId |
| 2 | Ranking tìm thấy | Giá trị ranking = vị trí thực tế (1-based) |
| 3 | Ngoài phạm vi | Không tìm thấy trong 60 kết quả → ranking=0, status=2 |
| 4 | Lỗi API | Sau max_retries → ranking=997, status=3 |
| 5 | Pagination | Lấy đủ tối đa 3 trang qua nextPageToken |
| 6 | 429 retry | Response 429 → retry đúng max_retries lần với retry_delay |
| 7 | Multi API key | Key phân phối round-robin cho chunks (chunk_idx mod số key) |
| 8 | Thread model | Tổng thread = --threads × số key |
| 9 | Rate limiting | min_interval kiểm soát đúng khoảng cách request (cả pagination pages) |
| 10 | normal/reverse | Mỗi group chạy normal → reverse liên tiếp, chia sẻ request_timer |
| 11 | Chunk distribution | Group được chia thành chunks liên tục cho các thread |
| 12 | data_source filter | Chỉ xử lý location có data_source=1 |
| 13 | Export data | JSON array đúng format với 8 keyword slots, top 3/10/20 counts |
| 14 | S3 log | Combined + per-key log files upload lên S3 |
| 15 | update_log status | Set RUNNING khi bắt đầu, process set SUCCEEDED/FAILED khi kết thúc |
| 16 | --max-groups | Giới hạn đúng số group khi test |
| 17 | get_db() exception | DB error được rollback và re-raise (không bị nuốt) |
Yêu cầu phi chức năng
| # | Hạng mục | Yêu cầu |
|---|---|---|
| 1 | Chi phí | $0/tháng (Field Mask: places.id → Basic SKU miễn phí) |
| 2 | Hiệu năng | Ban đầu: 1 key 4 thread 480 QPM. Thêm key = tăng tuyến tính |
| 3 | Tài nguyên | 1 CPU / 2GB RAM — 1 key 4 thread ~160MB, 3 key 12 thread ~320MB |
| 4 | Khả dụng | Phụ thuộc SLA Google Places API (99.9%) |
| 5 | Lưu trữ | Lịch sử ranking lưu vô thời hạn |
| 6 | Bảo mật | API Key quản lý qua biến môi trường server |
| 7 | Giám sát | Log upload S3, per-key log file để debug |
| 8 | Khả năng mở rộng | Thêm key vào env → tự động tăng thread và throughput |
Điều kiện tiên quyết & Ràng buộc
Điều kiện tiên quyết
- Google Cloud Project đã bật Places API (New)
- API Key đã được tạo và có quyền truy cập Text Search (khuyến nghị nhiều project/key)
mappy_gbp_locations.raw_jsonchứametadata.placeIdhợp lệmappy_gbp_locationscó cộtdata_source(1=API, 2=scraping)- Python 3.10+ với các thư viện trong
requirements.txt - AWS S3 credentials cho upload log
Ràng buộc
- Text Search API không thể phát hiện Knowledge Panel (-1 / -2)
- Kết quả tìm kiếm tối đa 60 kết quả (20 x 3 trang) — vị trí 61+ coi là ngoài phạm vi
locationBiasradius cố định 5000m- Kết quả từ scraping và Places API có thể không trùng khớp hoàn toàn (logic ranking khác nhau)
- Export data giới hạn tối đa 8 keyword/location (keyword thứ 9+ không có trong export, nhưng vẫn lưu trong
mappy_temp_search_rankings) - Slack thông báo chưa implement (TODO)
Ước tính công sức
Đội ngũ
| Vai trò | Số người | Nội dung phụ trách |
|---|---|---|
| Thiết kế | 1 | Xác nhận yêu cầu → Tạo thiết kế → Review → Chỉ thị sản xuất |
| Sản xuất | 1 | Dựa trên ISSUE tạo code → Code review → Test → Deploy |
Chi tiết công sức
| # | Hạng mục | Công sức (người-ngày) | Phụ trách |
|---|---|---|---|
| 1 | Xác nhận yêu cầu & tạo thiết kế | 1.0 | Thiết kế |
| 2 | Migration DB (thêm data_source, indexes) | 0.5 | Sản xuất |
| 3 | Core logic: Text Search API + ranking | 1.5 | Sản xuất |
| 4 | Multi API key + Thread + CLI + Rate limiting | 1.0 | Sản xuất |
| 5 | Export data + Error handling | 1.0 | Sản xuất |
| 6 | Logging + S3 log upload | 0.5 | Sản xuất |
| 7 | Test tích hợp | 1.0 | Sản xuất |
| 8 | Deploy & xác nhận | 0.5 | Sản xuất |
| Tổng cộng | 7.0 |
Schedule
| タスク | 担当 | 日数 | 4/7 | 4/8 | 4/9 | 4/10 | 4/11 | 4/12 | 4/13 | 4/14 | 4/15 | 4/16 | 4/17 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 火 | 水 | 木 | 金 | 土 | 日 | 月 | 火 | 水 | 木 | 金 | |||
| Xác nhận yêu cầu & thiết kế | Thiết kế | 1d | |||||||||||
| Migration DB (data_source + indexes) | Sản xuất | 1d | |||||||||||
| Review thiết kế & chỉ thị | Thiết kế | 1d | |||||||||||
| Core logic: API + ranking | Sản xuất | 2d | |||||||||||
| Multi API key + Thread + Rate limit | Sản xuất | 1d | |||||||||||
| Export data + error handling | Sản xuất | 1d | |||||||||||
| Logging + S3 upload | Sản xuất | 1d | |||||||||||
| Test tích hợp | Sản xuất | 1d | |||||||||||
| Deploy & xác nhận | Sản xuất | 1d |