Minh Họa 10.000 Trang Mỗi Tháng Bằng Ảnh Thật — Chỉ Với 500 Lượt Gọi API

Tìm kiếm một lần cho mỗi cụm chủ đề thay vì mỗi ảnh, và 40.000 lượt gọi API trở thành 500. Mô hình gộp-và-gán, worker sống sót qua giới hạn tốc độ, và một phần thẳng thắn về những gì ảnh chụp không thể khắc phục.

Chia sẻ
Một văn phòng đông đúc nơi đồng nghiệp làm việc cạnh nhau trên máy tính tại các bàn dài dùng chung.
Ảnh từ Unsplash

Có một phiên bản của bài toán minh họa nội dung mà không lượng gu thẩm mỹ nào giải quyết được: bạn không chọn một tấm ảnh, bạn đang lấp đầy 40.000 vị trí ảnh mỗi tháng trên 800 cụm chủ đề, 9 ngôn ngữ và 14 site khách hàng, và mỗi vị trí đều cần được cấp phép, ghi công, chỉnh kích thước và khác với vị trí bên cạnh. Ở quy mô đó câu hỏi không còn là “ảnh nào?” nữa, mà là “kiến trúc là gì?”

Bài viết này là câu trả lời ở tầng API: mẫu request giúp giảm số lượng gọi API đi hai bậc độ lớn, worker sống sót qua giới hạn tốc độ, cơ chế loại bỏ trùng lặp giúp một site 10.000 trang không trông giống như một bức tường toàn cùng một ảnh stock — và một phần trung thực về những gì hình ảnh không thể làm được cho bạn.

Nút thắt cổ chai thật sự ở quy mô 10.000 bài viết

Các đội xuất bản ở quy mô lớn — hệ sinh thái SEO lập trình, trang danh mục marketplace, các trang tổng hợp, mạng lưới affiliate, agency vận hành nội dung cho một danh mục khách hàng, pipeline bản địa hóa biến một bài viết thành 20 phiên bản — tất cả đều va phải cùng ba bức tường, theo cùng một thứ tự:

  1. Số lượng lệnh gọi. Tìm kiếm một lần cho mỗi vị trí ảnh nghĩa là 40.000 lệnh gọi API cho 10.000 bài viết bốn ảnh. Mọi nhà cung cấp đều tính giá, giới hạn và xét duyệt bạn dựa trên con số đó.
  2. Sự lặp lại. Đến khoảng trang thứ ba mươi, cùng một tấm ảnh bắt đầu xuất hiện lại. Đến trang thứ ba nghìn, site của bạn có một dấu ấn thị giác: được tạo hàng loạt.
  3. Sự phối hợp. Một trăm bài viết trong một cụm chủ đề nên trông như một chuỗi, không phải một trăm bảng Pinterest không liên quan — và bản dịch ngôn ngữ tiếp theo của cùng bài viết đó nên tái sử dụng cùng một tấm ảnh, chứ không tìm kiếm lại bằng một ngôn ngữ khác.

Hãy để ý điều không có trong danh sách đó: tìm một tấm ảnh đẹp. Một công cụ ngữ nghĩa trả về một trang các ứng viên khả dụng trong khoảng 130 mili giây. Việc truy xuất đã được giải quyết; việc phân phối thì chưa. Mọi thứ dưới đây nói về phân phối.

Vì sao dùng ảnh thật, đặc biệt ở quy mô này

Lý lẽ ủng hộ ảnh chụp so với ảnh tạo sinh mạnh hơn khi quy mô tăng lên, vì những lý do chủ yếu mang tính vận hành hơn là thẩm mỹ.

Ở mức 40.000 ảnh/tháng Tạo sinh chúng Tìm kiếm chúng
Thời gian mỗi ảnh Vài giây đến vài phút, cộng thêm các lần thử bị loại bỏ Một request (~130 ms) bao phủ cả một cụm chủ đề
Yếu tố chi phí Theo từng ảnh, mãi mãi Theo từng request — và một request phục vụ ~25 bài viết
Metadata bạn nhận được Không có gì. Bạn tự viết alt text và kích thước Kích thước, màu chủ đạo, blur hash, bản nháp caption, dòng ghi công
Nguồn gốc Được đánh dấu bởi máy là tổng hợp theo Đạo luật AI của EU kể từ ngày 2 tháng 8 năm 20261 Một nhiếp ảnh gia có tên, một ngày tháng, một URL nguồn mà độc giả có thể mở
Kiểu thất bại Chi tiết nghe hợp lý nhưng sai, phong cách đồng nhất Không có gì khớp với yêu cầu — bạn nhận về không kết quả, và có thể xử lý điều đó

Dòng metadata là dòng quyết định pipeline. Mọi kết quả tìm kiếm đã sẵn mang theo width, height, blur_hash, color_hex, alt_description và một chuỗi attribution.html dựng sẵn — đó chính là payload mà một templating engine cần để tạo ra một <img> không gây layout shift, có placeholder và có ghi công. Ảnh tạo sinh chỉ cho bạn một tệp và để lại sáu trường còn lại cho bạn tự phát minh.

Nơi ảnh tạo sinh vẫn thắng ở quy mô lớn. Minh họa cấp danh mục cho các nhóm phân loại trừu tượng (“chuyển đổi lên cloud”, “quỹ chỉ số”), sơ đồ, và một phong cách thương hiệu bạn muốn lặp lại xuyên suốt cả hệ sinh thái vì lý do thương hiệu. Ranh giới mà hầu hết các nhà xuất bản lớn chọn: ảnh chụp cho bất cứ thứ gì tồn tại trong thế giới thực, nghệ thuật tạo sinh cho bất cứ thứ gì chỉ tồn tại trong một lập luận. Chúng tôi đã trình bày đầy đủ lý lẽ cho ranh giới đó trong cách minh họa mọi bài viết bạn xuất bản.

Một request, một trăm ảnh

Đây là thay đổi duy nhất định hình lại phép tính. Endpoint tìm kiếm nhận per_page lên đến 100, và phân trang bằng cursor cho phép bạn tiếp tục duyệt qua cùng một tập kết quả đã xếp hạng. Vậy nên đơn vị công việc không phải là một tấm ảnh, mà là một pool.

Một request → một pool cho cả một cụm chủ đề
curl -sG "https://api.pexafy.com/api/v1/search/photos" \
  -H "X-Api-Key: $PEXAFY_API_KEY" \
  --data-urlencode "q=a woman signing paperwork with an insurance agent at a kitchen table in her home" \
  --data-urlencode "per_page=100" \
  --data-urlencode "orientation=landscape" \
  --data-urlencode "score_threshold=0.5" \
  --data-urlencode "fields=photo_id,urls,width,height,blur_hash,alt_description,attribution"

# → { "success": true, "data": [ …tối đa 100 ảnh… ],
#     "pagination": { "per_page": 100, "has_more": true, "next_cursor": "eyJyc…" },
#     "meta": { "took_ms": 122.33, "request_id": "7ff1fb9b-…" } }

Ba tham số ở đó đang âm thầm làm việc ở quy mô lớn:

  • fields — một sparse fieldset. Chỉ yêu cầu bảy trường mà template của bạn render và phản hồi sẽ không còn gửi kèm phần mô tả AI dài dòng cho mỗi ảnh. Trên 40.000 ảnh đó là sự khác biệt giữa một bản build chạy trôi chảy và một bản build bị chậm nghẽn.
  • score_threshold — phần đuôi của một trang 100 kết quả, theo định nghĩa, kém liên quan hơn phần đầu. Đặt một ngưỡng sàn nghĩa là một cụm chủ đề mỏng trả về 34 ảnh thay vì 100 ảnh tầm thường, và pipeline của bạn có thể phản ứng với điều đó thay vì xuất bản nó.
  • next_cursor — khi một cụm chủ đề thực sự cần 300 ảnh, hãy phân trang trên cùng một tập đã xếp hạng thay vì gửi ba truy vấn khác nhau chồng lấn lên nhau.

Và đây là phép tính, cho một site xuất bản 10.000 trang mỗi tháng với bốn ảnh mỗi trang:

Chiến lược Request / tháng Một lượt chạy ở giới hạn tốc độ gói free20 req/phút Có vừa hạn mức tháng của gói free không?
Tìm kiếm một lần cho mỗi vị trí ảnh 40.000 33 hours Không — cần gói trả phí
Tìm kiếm một lần cho mỗi bài viết 10.000 8 hours Không — cần gói trả phí
Tìm kiếm một lần cho mỗi cụm ~20 bài viếtmẫu hình trong bài viết này 500 25 minutes Có

Vẫn là 40.000 ảnh xuất bản trong cả ba dòng. Thứ duy nhất thay đổi là vòng lặp nằm ở đâu.

Pool và assign: mẫu hình mở rộng được

Toàn bộ kiến trúc là hai giai đoạn chạy ở các tần suất khác nhau, với một bảng ở giữa chúng:

Hình dạng của nó
                     ┌─────────── chạy hàng tuần, ~500 request ─────────┐
  cụm chủ đề ──▶ POOL   tìm kiếm một lần cho mỗi cụm, per_page=100
                     │       └─▶ lưu 100 dòng ảnh cho mỗi cụm
                     └──────────────────┬───────────────────────────────┘
                                        ▼
                              bảng image_pool
                        (cluster, photo_id, urls, blur_hash,
                         alt, credit, used_by_page, used_at)
                                        │
                     ┌──────────────────┴──── chạy mỗi lần xuất bản, 0 request ───┐
  bài viết ─────────▶ ASSIGN  chọn dòng chưa dùng tốt nhất cho
                     │        cụm này, đánh dấu đã dùng, render
                     └────────────────────────────────────────────────────────┘

Những gì cách làm này mang lại, theo thứ tự mức độ quan trọng ở quy mô lớn:

  1. Việc xuất bản không bao giờ bị chặn bởi API. Việc phân bổ là một lượt đọc cơ sở dữ liệu. Hook lưu của CMS, bản build tĩnh và tác vụ import hàng loạt lúc 3 giờ sáng của bạn đều chạy ở tốc độ cục bộ, ngoại tuyến, không có giới hạn tốc độ nào trên đường đi.
  2. Loại bỏ trùng lặp là miễn phí và chính xác. used_by_page IS NULL là toàn bộ tính năng. Hai trang không thể lấy cùng một tấm ảnh, vì việc phân bổ là một transaction, không phải một heuristic xếp hạng.
  3. Chạy lại thì rẻ và mang tính idempotent. Làm mới pool của một cụm khi pool sắp cạn hoặc khi bạn muốn ảnh mới hơn (after_date, hoặc sort_by=newest) — một request, và không có gì đã xuất bản bị dịch chuyển.
  4. Ngôn ngữ đi kèm miễn phí. 20 bản dịch của một bài viết là một trang trong mô hình của bạn với 20 lần render; chúng dùng chung photo_id đã gán và bạn không bao giờ tìm kiếm hai lần. Khi bạn thực sự muốn một phong cách ảnh chụp bản địa, hãy chạy truy vấn pool cho cụm đó bằng ngôn ngữ đích — công cụ nhận một câu bằng hơn 100 ngôn ngữ.

Worker, dưới dạng code

Khoảng sáu mươi dòng. Phần điền pool là phần duy nhất giao tiếp với mạng, nên đó là phần duy nhất cần cẩn thận — giới hạn số lượng đồng thời, tôn trọng 429, kết quả được ghi trong một transaction.

pool.py — điền một pool cho mỗi cụm, một cách lịch sự
import asyncio, os, time, httpx

SEARCH = "https://api.pexafy.com/api/v1/search/photos"
KEY    = os.environ["PEXAFY_API_KEY"]
FIELDS = "photo_id,urls,width,height,blur_hash,alt_description,attribution"

# Free = 20 req/phút. Giữ dưới một chút. Bộ điều tốc là TOÀN CỤC: một sleep theo từng task
# bên trong cổng đồng thời sẽ cho phép N worker bắn ra N× tốc độ.
PER_MIN = 19
gate    = asyncio.Semaphore(4)         # số kết nối đang chạy
_lock   = asyncio.Lock()
_slot   = 0.0                          # thời điểm rảnh tiếp theo trên trục thời gian chung

async def pace():
    """Cấp một slot request mỗi 60/PER_MIN giây, cho toàn bộ fleet."""
    global _slot
    async with _lock:
        now  = time.monotonic()
        _slot = max(now, _slot) + 60 / PER_MIN
        wait  = _slot - now
    if wait > 0:
        await asyncio.sleep(wait)

class QuotaExhausted(Exception): ...   # theo tháng: thử lại không giúp được gì

async def fill_pool(client, cluster) -> list[dict]:
    """Một request → tối đa 100 ảnh có bản quyền cho một cụm chủ đề."""
    for attempt in range(4):
        await pace()
        async with gate:
            r = await client.get(
                SEARCH,
                headers={"X-Api-Key": KEY},
                params={
                    "q": cluster["camera_brief"],   # một cảnh, không phải từ khóa
                    "per_page": 100,
                    "orientation": "landscape",
                    "score_threshold": 0.5,
                    "fields": FIELDS,
                },
                timeout=15,
            )
        if r.status_code == 429:
            retry_after = r.headers.get("Retry-After")
            if retry_after is None:            # không có Retry-After ⇒ hết hạn mức tháng,
                raise QuotaExhausted(cluster["id"])   # dừng toàn bộ lượt chạy
            await asyncio.sleep(int(retry_after))   # theo phút: sẽ tự hết
            continue
        r.raise_for_status()
        return r.json()["data"]
    return []                                  # ghi log; pool giữ nguyên dữ liệu cũ

async def refill(clusters):
    async with httpx.AsyncClient(http2=True) as client:
        pools = await asyncio.gather(*(fill_pool(client, c) for c in clusters))
    for cluster, photos in zip(clusters, pools):
        db.upsert_pool(cluster["id"], photos)     # ON CONFLICT DO NOTHING trên photo_id

Hai chi tiết ở đó là sự khác biệt giữa một worker chạy không cần giám sát và một worker làm bạn phải thức dậy giữa đêm. Bộ điều tốc là toàn cục, không phải theo từng task: đặt sleep bên trong cổng đồng thời là sai lầm kinh điển — bốn worker mỗi cái tạm dừng 3 giây sau request riêng của nó tạo ra bốn request mỗi 3 giây, khoảng 75 lần mỗi phút, và gói free bắt đầu trả về 429 ngay lập tức. Và hai loại 429 không phải cùng một con thú: loại theo phút mang theo Retry-After và tự hết, loại hết-hạn-mức-tháng thì không mang theo và sẽ không bao giờ có — thử lại loại đó là một vòng lặp đâm vào tường.

Phần assign, phần chạy ở mỗi lần xuất bản, không bao giờ chạm vào mạng:

assign.py — xác định, có transaction, không lặp lại ở bất cứ đâu
# Tính duy nhất được thực thi bởi schema, không phải bởi truy vấn cẩn thận.
# Một tấm ảnh có thể nằm trong nhiều pool của các cụm khác nhau; nó chỉ được XUẤT BẢN một lần.
# CREATE UNIQUE INDEX one_use_per_photo ON image_pool (photo_id)
#        WHERE used_by_page IS NOT NULL;

def assign_hero(page_id: str, cluster_id: str) -> dict | None:
    for _ in range(5):                # thua trong một race chỉ đơn giản là lấy ảnh tiếp theo
        try:
            return db.query_one("""
                UPDATE image_pool p SET used_by_page = %s, used_at = NOW()
                WHERE p.id = (
                    SELECT c.id FROM image_pool c
                    WHERE c.cluster_id = %s AND c.used_by_page IS NULL
                      AND NOT EXISTS (              -- được cụm khác dùng chưa?
                          SELECT 1 FROM image_pool u
                          WHERE u.photo_id = c.photo_id
                            AND u.used_by_page IS NOT NULL
                      )
                    ORDER BY c.rank ASC
                    LIMIT 1 FOR UPDATE SKIP LOCKED  -- an toàn với worker chạy song song
                )
                RETURNING photo_id, urls, width, height, blur_hash, alt, credit
            """, [page_id, cluster_id])
        except UniqueViolation:      # hai cụm giành nó trong cùng một mili giây
            continue
    return None                     # pool cạn → mở rộng brief, làm mới lại

# Render: mọi thuộc tính đến từ dòng dữ liệu — không layout shift, không đoán mò.
# <img src="{urls[regular]}" width="{width}" height="{height}"
#      alt="{alt}" loading="lazy" style="background:{blur_placeholder}">
# <figcaption>{credit}</figcaption>

Hai kiểu thất bại, hai cơ chế, và cả hai đều cần thiết. FOR UPDATE SKIP LOCKED xử lý tính đồng thời bên trong một cụm: tạo trang song song — và ở quy mô này bạn sẽ làm vậy — và hai worker cùng vươn tới cùng một ảnh xếp hạng cao nhất trong cùng mili giây.

Partial unique index xử lý điều mà mọi người luôn quên: cùng một tấm ảnh xuất hiện hợp lệ trong pool của nhiều cụm, vì các cụm lân cận (“bảo hiểm nhà cho người thuê”, “…cho chủ nhà cho thuê”) trả về các kết quả chồng lấn. Một kiểm tra used_by_page IS NULL theo từng cụm không nhìn thấy điều đó — mỗi cụm tin rằng tấm ảnh vẫn còn trống. NOT EXISTS giữ cho truy vấn trung thực, và index khiến việc bị sai không thể xảy ra dưới điều kiện đồng thời: kẻ thua trong race sẽ gặp violation, thử lại, và lấy ảnh tiếp theo. Không có nó, “không có hai trang nào chia sẻ ảnh chính” chỉ là một lời tuyên bố, không phải một sự đảm bảo.

Khi một pool cạn giữa lúc build, cơ chế dự phòng hoạt động tốt nhất là mở rộng thay vì lặp lại: bỏ mệnh đề cuối của camera brief, chạy lại truy vấn pool một lần, và chỉ khi đó mới tái sử dụng ảnh đã gán lâu nhất — với một quy tắc cứng rằng nó không bao giờ xuất hiện trên một trang trong cùng cụm.

Một cụm, một pool: một truy vấn thực tế

Hãy lấy một hệ sinh thái so sánh: bảo hiểm nhà, hai mươi trang — “bảo hiểm nhà cho người thuê”, “…cho chủ nhà cho thuê”, “một hợp đồng bảo hiểm thực sự bao gồm những gì”, mười hai trang theo thành phố, ba hướng dẫn bồi thường. Một camera brief cho cả cụm, một request, và đây là phần đầu của pool, trả về trong 122 ms:

GET /search/photos — “a woman signing paperwork with an insurance agent at a kitchen table in her home” · 122 ms
Phần đầu của pool: sáu cảnh khác biệt từ một tập kết quả đã xếp hạng — một chữ ký, một mẫu đơn ở cận cảnh, một hợp đồng đang được giải thích, giấy tờ trên bàn làm việc, một cặp đôi cùng một đại lý, một cuộc so sánh bên tách cà phê. Đó là sáu trong số hai mươi trang đã được bao phủ; request gộp nhóm yêu cầu per_page=100 và giữ lại phần còn lại. Chạy chính xác truy vấn này →

Các brief quan trọng hơn code. Hai quy tắc còn tồn tại sau khi va chạm với một hệ sinh thái cụm chủ đề thực tế: viết brief cho cụm chủ đề, không phải cho từng trang (tiêu đề trang gần như giống hệt nhau trong các tập lập trình, nên brief theo từng trang trả về ảnh gần như giống hệt nhau), và mô tả một cảnh mà một máy ảnh có thể chụp được thay vì mô tả chủ đề — bảo hiểm nhà trả về ảnh cận cảnh văn bản và không gì khác. Chúng tôi đã công bố prompt có thể copy-paste tạo ra các brief này trong bài viết về minh họa mọi bài đăng.

Giữ tính nhất quán thị giác cho một chuỗi 200 trang

Thất bại đối lập với sự lặp lại là sự thiếu nhất quán: hai mươi trang trong một cụm, mỗi trang có một tấm ảnh đúng về mặt kỹ thuật, nhưng ở hai mươi phong cách thị giác khác nhau. Cách khắc phục là một endpoint — GET /photos/{id}/similar — chạy một lần trên tấm ảnh bạn đã duyệt cho trang trụ cột:

GET /photos/019e143f…/similar — nhiều ảnh giống ảnh chính đã duyệt · 48 ms
Cùng ánh sáng, cùng căn phòng, cùng trang phục, khoảnh khắc khác nhau — vì độ tương đồng thị giác tìm ra phần còn lại của một buổi chụp của nhiếp ảnh gia, không chỉ cùng một chủ thể. Phân bổ những ảnh này xuyên suốt một cụm và chuỗi ảnh trông như được đặt hàng chứ không phải được ghép lại.

Còn hai đòn bẩy nữa đáng để tích hợp vào truy vấn pool khi tính nhất quán thương hiệu là một yêu cầu: color_hex kèm một color_tolerance giữ cả một hệ sinh thái trong một bảng màu, và photographer gắn một cụm với toàn bộ tác phẩm của một nhiếp ảnh gia. Cả hai đều là các tham số truy vấn thông thường — không giới hạn theo gói với các bộ lọc tìm kiếm.

Giới hạn tốc độ, hạn mức và chi phí thực tế

Chọn gói dựa trên tốc độ burst, không dựa trên khối lượng. Một khi bạn đã gộp pool, hạn mức hàng tháng gần như không còn là ràng buộc với hầu hết mọi người nữa; điều quyết định gói của bạn là một lần làm mới toàn bộ cần hoàn thành nhanh đến mức nào.

Gói Request / tháng= số cụm có thể làm mới Giới hạn tốc độ Số API key Làm mới 1.000 cụmthời gian thực tế, một lượt
Free — $0 5,000 20 / phút 1 50 min
Starter — $5 25,000 30 / phút 3 34 min
Pro — $19 100,000 60 / phút 5 17 min
Team — $99agency: một key mỗi khách hàng 500,000 200 / phút 25 5 min
Business — $249 1,000,000 300 / phút unlimited 4 min
Enterprise — $599 2,000,000 500 / phút unlimited 2 min

Hãy đọc cột đầu tiên như số cụm chủ đề: một request gộp nhóm điền đầy một cụm, nên hạn mức hàng tháng chính là số cụm bạn có thể làm mới, và cột cuối cùng là một lượt chạy toàn bộ trên một hệ sinh thái 1.000 cụm mất bao lâu ở giới hạn tốc độ của gói đó. Cả hai đều là giá trị thực tế từ bảng giá — trang giá công bố cùng các giới hạn tốc độ theo giờ thay vì theo phút.

Vậy nên một hệ sinh thái 10.000 trang/tháng theo mẫu hình gộp pool tiêu tốn ~500 request và trả không đồng nào. Điều đẩy các đội lên các gói cao hơn hiếm khi là khối lượng — đó thường là một trong ba điều: minh họa lại toàn bộ một hệ sinh thái có sẵn trong một đêm, cô lập key theo từng khách hàng (một agency muốn mỗi khách hàng một key để việc sử dụng có thể quy trách nhiệm mà không cần chia sẻ bí mật phức tạp), hoặc một cửa sổ build được đo bằng phút thay vì giờ.

Hai chi tiết vận hành giúp bạn tiết kiệm một đêm gỡ lỗi. Mọi phản hồi đều mang theo X-RateLimit-Limit và X-RateLimit-Remaining, để một worker có thể tự điều tiết thay vì phải đoán. Và một 429 thực sự là giới hạn tốc độ theo phút thì mang theo Retry-After, trong khi một chặn do hạn mức tháng thì cố tình không — thử lại sẽ không giải quyết được điều đó, và một worker phân biệt được hai loại này sẽ ngừng đập liên tục vào một endpoint đã không còn gì để cho.

Khi agent thực hiện việc xuất bản

Nếu nội dung của bạn được tạo ra bởi một agent — và ở những quy mô này ngày càng phổ biến — lớp gộp pool không biến mất, nó chỉ dịch chuyển vị trí. Hãy trao cho agent các công cụ tìm kiếm và nó sẽ điền đầy pool trong khi vẫn giữ camera brief của cụm trong ngữ cảnh, sử dụng máy chủ Model Context Protocol tại mcp.pexafy.com/mcp: ba công cụ, search_photos (một câu), search_photos_by_image (một ảnh tham chiếu, tùy chọn kèm một câu) và get_similar_photos (công cụ tạo tính nhất quán chuỗi ảnh nêu ở trên).

Kết nối vào một agent headless — một connector, một key
# Claude Code / CI runner
claude mcp add --transport http pexafy https://mcp.pexafy.com/mcp \
  --header "Authorization: Bearer $PEXAFY_API_KEY"

# hoặc commit .mcp.json để mọi worker trong fleet đều kế thừa nó
{
  "mcpServers": {
    "pexafy": {
      "type": "http",
      "url": "https://mcp.pexafy.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}

Hướng dẫn hoạt động tốt ở quy mô lớn là một hướng dẫn theo lô, không phải theo từng bài viết — và nó ánh xạ một-một vào hai giai đoạn ở trên:

Làm mới một cụm, do agent điều khiển
You  Đây là 12 cụm chủ đề từ lịch nội dung. Với mỗi cụm,
     hãy viết một camera brief, lấy về 100 ảnh landscape, và ghi
     pool xuống Postgres. Đánh dấu bất kỳ cụm nào trả về dưới 40
     kết quả trên ngưỡng 0.5 — các brief đó cần viết lại.

Agent  → search_photos(q="a woman signing paperwork with an insurance
           agent at a kitchen table in her home", orientation="landscape")
      ← 100 ảnh · 122 ms · 87 vượt ngưỡng
      … 11 cụm khác …

      ✓ 11 pool đã ghi (1.043 ảnh, tất cả đều có ghi công)
      ⚠ "cân bằng lại quỹ chỉ số" → 12 kết quả. Brief quá trừu tượng;
        đề xuất: "một người tại bàn bếp kiểm tra các con số trên một
        laptop với một cuốn sổ tay và cà phê bên cạnh"

Dòng cuối cùng đó là lý do để đưa agent vào vòng lặp này thay vì một script: kiểu thất bại của việc minh họa theo chương trình là một brief tồi, và một brief tồi chính xác là thứ mà một mô hình ngôn ngữ có thể nhận ra và viết lại. Các script vẫn phụ trách phần assign, nơi tính xác định quan trọng còn tính sáng tạo thì không.

Điều mà ảnh chụp không thể khắc phục

Một phần trung thực, vì kiểu thất bại nó mô tả tốn kém. Ảnh chụp thật, có ghi công cải thiện một trang. Nó không biến nội dung sơ sài thành nội dung tốt, và không pipeline hình ảnh nào thay đổi cách các công cụ tìm kiếm đối xử với các trang được sản xuất hàng loạt tồn tại để xếp hạng chứ không phải để giúp ích. Chính sách chống spam của Google nêu rõ điều này: lạm dụng nội dung quy mô lớn (scaled content abuse) bao gồm việc tạo ra nhiều trang với giá trị ít ỏi bất kể có sử dụng tự động hóa hay không, và các hình ảnh minh họa trên những trang đó không liên quan gì đến phán quyết đó.2

Vậy nên cách nhìn nhận hữu ích là hẹp và đúng: ảnh chụp là một tín hiệu chất lượng bạn kiểm soát trên những trang vốn đã xứng đáng tồn tại. Nơi nó thực sự đem lại lợi ích rõ ràng:

  • Nguồn gốc mà độc giả có thể kiểm chứng. Một dòng ghi công với tên nhiếp ảnh gia và một URL nguồn là một tuyên bố có thể kiểm tra được — đối lập với một ảnh không rõ nguồn trên một trang có tác giả không rõ danh tính.
  • Khả năng tiếp cận và Core Web Vitals. width/height từ phản hồi loại bỏ layout shift, blur_hash tạo ra một placeholder thực sự, và alt_description là một bản nháp alt text mà template của bạn có thể cải thiện thay vì tự bịa ra. Nhân với 40.000 vị trí và đây chính là toàn bộ câu chuyện chất lượng hình ảnh của cả site.
  • Không trông giống được sản xuất hàng loạt. Loại bỏ trùng lặp và tính nhất quán chuỗi ảnh chính là thứ ngăn một hệ sinh thái có một dấu ấn thị giác. Đó là một cái giá về mặt nhận thức có thật với hậu quả có thật, và nó hoàn toàn nằm trong tầm kiểm soát của bạn.

Và giấy phép vẫn chi phối tấm ảnh. Việc ghi công không bắt buộc theo điều khoản API của Pexafy — mọi kết quả đều đi kèm attribution.html và attribution.plain sẵn sàng để render — nhưng giấy phép gắn liền với thư viện gốc áp dụng cho việc sử dụng tấm ảnh đó của bạn, và ở mức 40.000 ảnh mỗi tháng, việc tự động render dòng ghi công thì rẻ hơn nhiều so với việc kiểm tra lại về sau.

Nên bắt đầu từ đâu vào thứ Hai

  1. Nhóm backlog của bạn thành các cụm gồm 15–30 trang có thể hợp lý dùng chung một buổi chụp ảnh. Đây là bước duy nhất thực sự thủ công, và nó là một bảng tính, không phải một dự án.
  2. Viết một camera brief cho mỗi cụm — một cảnh, 12 đến 25 từ. Tạo chúng bằng một mô hình, sau đó đọc lại; các brief nêu tên chủ đề thay vì mô tả cảnh sẽ lộ ra ngay trong nháy mắt.
  3. Điền một pool bằng một request duy nhất per_page=100 và xem qua phần đầu của nó. Nếu mười ảnh đầu tiên không dùng được, brief sai — không phải công cụ.
  4. Thêm cột used_by_page trước khi bạn xuất bản bất cứ điều gì. Nó tốn năm phút bây giờ và một cuộc di trú qua 3.000 trang đang chạy sau này.
  5. Chạy toàn bộ trên gói free cho đến khi một cửa sổ build buộc bạn phải nâng cấp. Ở 500 request mỗi tháng, sẽ mất một thời gian đấy.

Tài liệu tham khảo & chú thích

1 Đạo luật AI của EU, Điều 50 — nghĩa vụ minh bạch áp dụng kể từ ngày 2 tháng 8 năm 2026: các nhà cung cấp hệ thống tạo ra hình ảnh, âm thanh, video hoặc văn bản tổng hợp phải đánh dấu đầu ra ở định dạng có thể đọc bằng máy và khiến chúng có thể phát hiện được là được tạo ra một cách nhân tạo. Điều này ràng buộc các nhà cung cấp và triển khai AI; nó không phải một quy tắc về việc một website được phép xuất bản những hình ảnh nào.

2 Chính sách chống spam của Google Search — lạm dụng nội dung quy mô lớn: tạo ra nhiều trang chủ yếu để thao túng thứ hạng và mang lại ít giá trị cho người dùng, bất kể được tạo ra thông qua tự động hóa, công sức con người hay sự kết hợp của cả hai. Chất lượng minh họa không phải một yếu tố trong đánh giá đó, đó chính xác là lý do bài viết này tách biệt hai vấn đề đó.

Câu hỏi thường gặp

Làm sao để lấy ảnh cho hàng nghìn trang SEO lập trình?
Tìm kiếm một lần cho mỗi cụm chủ đề, không phải mỗi trang. Một yêu cầu GET /api/v1/search/photos duy nhất với per_page=100 trả về tối đa 100 ảnh có ghi công cho một cụm; bạn lưu chúng vào bảng pool và gán mỗi ảnh cho một trang tại thời điểm xuất bản. Một hệ thống 10.000 trang/tháng với bốn ảnh mỗi trang chỉ cần khoảng 500 yêu cầu thay vì 40.000, vừa đủ nằm trong gói miễn phí (5.000 yêu cầu/tháng).
API ảnh stock nào tốt nhất cho sản xuất nội dung khối lượng lớn?
Các tiêu chí quan trọng khi vượt qua vài nghìn ảnh mỗi tháng là: một yêu cầu có thể trả về bao nhiêu ảnh, liệu nhiều thư viện có được trả về trong một schema chuẩn hóa hay không, giới hạn tốc độ theo phút, và liệu có cần xét duyệt ứng dụng để được truy cập sản xuất hay không. Pexafy trả về tối đa 100 ảnh mỗi yêu cầu từ 9 thư viện miễn phí trong một schema duy nhất, cấp khóa hoạt động ngay lập tức, và cho phép 20 yêu cầu/phút ở gói miễn phí lên đến 300/phút ở gói Business. Chúng tôi so sánh mọi API miễn phí theo các tiêu chí này trong một bài viết riêng.
Làm sao để tránh cùng một ảnh stock xuất hiện trên nhiều trang?
Lưu photo_id của mọi ảnh bạn xuất bản và lấy ảnh từ pool theo giao dịch (transaction) — trong SQL, một câu lệnh UPDATE … WHERE used_by_page IS NULL … FOR UPDATE SKIP LOCKED. Việc khử trùng lặp khi đó trở nên chính xác thay vì mang tính xác suất, và các worker build song song không thể tranh giành cùng một ảnh xếp hạng cao nhất. Đây chỉ là một cột dữ liệu, và việc bổ sung sau này trên hàng nghìn trang đang hoạt động sẽ tốn kém hơn nhiều so với thêm ngay từ đầu.
Một yêu cầu API có thể trả về bao nhiêu ảnh?
Tối đa 100 ảnh, thông qua per_page=100, và phân trang kiểu con trỏ (cursor pagination) cho phép bạn tiếp tục duyệt cùng một tập kết quả đã xếp hạng để có pool sâu hơn. Kết hợp với score_threshold để một cụm ít dữ liệu trả về 40 kết quả khớp mạnh thay vì 100 kết quả khớp lỏng lẻo, và với fields để chỉ trả về các thuộc tính mà template của bạn hiển thị, giúp giữ phản hồi nhỏ gọn ở quy mô lớn.
Ảnh thật có giúp các trang được sản xuất ở quy mô lớn xếp hạng tốt hơn không?
Không — và điều này đáng được nói rõ. Chính sách chống spam của Google định nghĩa lạm dụng nội dung quy mô lớn (scaled content abuse) là việc tạo ra nhiều trang chủ yếu để thao túng thứ hạng mà mang lại ít giá trị cho người dùng, bất kể có dùng tự động hóa hay không; ảnh minh họa trên các trang đó không làm thay đổi đánh giá này. Ảnh chụp thật, có ghi công là một tín hiệu chất lượng trên các trang vốn đã xứng đáng tồn tại: nó mang nguồn gốc có thể kiểm chứng, cung cấp chiều rộng, chiều cao, blur hash và văn bản thay thế (alt text) giúp bảo vệ Core Web Vitals và khả năng tiếp cận, đồng thời giữ cho hệ thống không trông như được sản xuất hàng loạt.
Một AI agent có thể tự động điền đầy pool ảnh không?
Có, thông qua máy chủ MCP (Model Context Protocol) được lưu trữ của Pexafy tại mcp.pexafy.com/mcp, cung cấp search_photos, search_photos_by_image và photo_similar. Đưa cho agent một danh sách các cụm chủ đề và nó sẽ viết một bản mô tả máy ảnh (camera brief) cho mỗi cụm, lấy các pool và đánh dấu những bản brief trả về quá ít kết quả khớp mạnh — đây chính là kiểu lỗi thực sự của minh họa lập trình. Việc gán ảnh vẫn nằm trong script của bạn, nơi tính xác định (determinism) quan trọng hơn sự sáng tạo.

Đừng săn lùng từ khóa nữa. Hãy mô tả điều bạn muốn nói.

Tìm kiếm 9M+ ảnh miễn phí sử dụng được theo ý nghĩa — trong mọi ngôn ngữ, dưới 100 ms.