Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Filtering, sorting, search trong API

API and Technical Fundamentals cho BA Bài 23/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là BA cho một sàn thương mại điện tử như Tiki hay Shopee. Một ngày, đội vận hành gửi yêu cầu: "Cho tôi xem tất cả đơn hàng ở TP.HCM, đã thanh toán, giá trị trên 2 triệu, sắp theo ngày tạo mới nhất, và tìm những đơn có chứa từ 'gấp' trong ghi chú." Nghe rất đời thường, đúng không? Nhưng đằng sau câu nói đó là ba khái niệm kỹ thuật mà mọi API hiện đại đều phải xử lý: filtering (lọc), sorting (sắp xếp) và search (tìm kiếm).

Đây là phần "xương sống" của hầu hết mọi API trả về danh sách. Một endpoint kiểu GET /orders mà không có khả năng lọc, sắp xếp, tìm kiếm thì gần như vô dụng trong thực tế — vì không ai muốn tải về 2 triệu đơn hàng rồi tự lọc ở phía client. Là BA, bạn không cần tự code, nhưng bạn phải biết thiết kế và review phần này trong API spec. Nếu bạn viết yêu cầu mơ hồ ("hệ thống cho phép lọc đơn hàng"), dev sẽ tự quyết cú pháp, và bạn sẽ nhận về một API khó dùng, khó mở rộng, hoặc tệ hơn là không khớp với nhu cầu nghiệp vụ.

Bài này tập trung hoàn toàn vào cách diễn đạt filter/sort/search trên URL của API — không phải phân trang (pagination, đã có ở bài 22) và không phải cách lưu trữ trong database. Sau bài này, bạn sẽ đọc được một query string phức tạp, biết đề xuất cú pháp phù hợp với độ phức tạp nghiệp vụ, và tránh được những cái bẫy kinh điển.

Khái niệm cốt lõi

Filtering — lọc dữ liệu theo điều kiện

Filtering là việc thu hẹp tập kết quả về đúng những bản ghi thỏa điều kiện. Cách phổ biến và dễ đọc nhất là dùng query parameter với cặp key=value:

GET /api/orders?status=paid&city=HCM

Câu này có nghĩa: "lấy các đơn có status bằng paidcity bằng HCM". Theo quy ước, nhiều tham số khác nhau được hiểu là AND (và). Đây là dạng filter đơn giản nhất — gọi là exact match (so khớp chính xác).

Nhưng nghiệp vụ thực tế hiếm khi chỉ cần "bằng". Bạn sẽ cần "lớn hơn", "nhỏ hơn", "trong khoảng", "thuộc một danh sách". Lúc này bạn cần filter operators (toán tử lọc). Có vài trường phái diễn đạt:

Cách 1 — Hậu tố trên tên field (suffix):

GET /api/orders?amount_gte=2000000&amount_lte=5000000&created_at_gt=2026-01-01

Ở đây gte = greater than or equal (lớn hơn hoặc bằng), lte = less than or equal, gt = greater than. Cách này dễ đọc, nhiều framework hỗ trợ sẵn.

Cách 2 — Toán tử trong ngoặc (bracket notation):

GET /api/orders?amount[gte]=2000000&status[in]=paid,shipped

Stripe và nhiều API lớn dùng kiểu này. status[in]=paid,shipped nghĩa là status thuộc tập {paid, shipped}.

Cách 3 — DSL/ngôn ngữ truy vấn riêng (RSQL/FIQL):

Khi nghiệp vụ cần điều kiện phức tạp với cả AND/OR lồng nhau, người ta dùng một Domain-Specific Language. Phổ biến là RSQL/FIQL:

GET /api/orders?filter=status==paid;amount=gt=2000000,city==HCM

Trong RSQL: == là bằng, =gt= là lớn hơn, dấu ; là AND, dấu , là OR. Câu trên đọc là: "(status = paid AND amount > 2tr) OR city = HCM". DSL mạnh mẽ nhưng khó học, khó viết tay — chỉ nên dùng khi thật sự cần logic phức tạp.

Sorting — sắp xếp kết quả

Sorting quyết định thứ tự các bản ghi trả về. Quy ước gần như chuẩn ngành là tham số sort:

GET /api/orders?sort=created_at
GET /api/orders?sort=-created_at

Dấu trừ - đứng trước nghĩa là giảm dần (descending); không có dấu là tăng dần (ascending). Cú pháp này gọn và rất phổ biến (GitHub, JSON:API dùng kiểu tương tự).

Bạn cũng cần sắp theo nhiều trường (multi-sort) — ví dụ "ưu tiên đơn VIP trước, trong cùng nhóm thì đơn mới nhất lên đầu":

GET /api/orders?sort=-priority,-created_at

Thứ tự liệt kê quan trọng: priority được áp dụng trước, created_at chỉ phân định khi priority bằng nhau. Một biến thể khác dùng cặp field/direction tường minh:

GET /api/orders?sort_by=created_at&order=desc

Cách nào cũng được, miễn là nhất quán toàn hệ thống. Là BA, bạn nên chốt một quy ước duy nhất trong tài liệu để mọi endpoint hành xử giống nhau.

Search — tìm kiếm theo văn bản

Search khác filter ở chỗ nó thường là tìm kiếm mờ (fuzzy/full-text) trên văn bản, không phải so khớp chính xác. Quy ước thường dùng tham số q (query) hoặc search:

GET /api/products?q=son+kem+lì

Server sẽ tìm các sản phẩm có chứa các từ "son", "kem", "lì" trong tên hoặc mô tả, thường có chấm điểm độ liên quan (relevance) và bỏ qua dấu, viết hoa/thường. Đây là điểm khác biệt cốt lõi: filter trả lời câu hỏi "có khớp điều kiện không?" (đúng/sai), còn search trả lời "khớp tới mức nào?" (xếp hạng theo độ liên quan).

Trong nhiều API, ba thứ này kết hợp tự nhiên với nhau:

GET /api/products?q=son&category=makeup&price[lte]=300000&sort=-rating

"Tìm 'son' trong nhóm trang điểm, giá dưới 300k, sắp theo đánh giá cao nhất."

Tình huống thực tế

Tình huống 1 — Sàn TMĐT "ChợViệt" và cái bẫy filter AND/OR

ChợViệt (giả định) ra mắt API GET /products cho đối tác tích hợp. Ban đầu BA chỉ viết yêu cầu: "Cho phép lọc theo danh mục, thương hiệu, khoảng giá." Dev triển khai kiểu đơn giản:

GET /products?category=skincare&brand=Cocoon&price_gte=100000

Mọi thứ êm đẹp cho đến khi một đối tác lớn muốn: "Lấy sản phẩm thuộc nhóm skincare HOẶC haircare, của thương hiệu Cocoon." Họ thử ?category=skincare&category=haircare nhưng hệ thống hiểu nhầm thành AND (sản phẩm vừa thuộc skincare vừa thuộc haircare — kết quả rỗng), hoặc chỉ lấy giá trị cuối. Đối tác gửi ticket phàn nàn API "thiếu tính năng cơ bản".

Đội ngũ phải bổ sung cú pháp in: ?category[in]=skincare,haircare. Bài học: ngay từ giai đoạn viết spec, BA phải hỏi nghiệp vụ rõ ràng "khi có nhiều giá trị cho cùng một field, đó là AND hay OR?". Mặc định một field nhận một giá trị thì không đủ; bạn cần định nghĩa toán tử in cho các trường có thể nhận nhiều giá trị. Quy ước an toàn: nhiều field khác nhau = AND, nhiều giá trị cùng field qua in = OR trong nội bộ field đó.

Tình huống 2 — Ngân hàng số và rủi ro injection từ filter

Một ngân hàng số ở Đông Nam Á xây API nội bộ GET /transactions cho dashboard vận hành. Để "linh hoạt", một dev sáng tạo cho phép truyền điều kiện gần như nguyên văn:

GET /transactions?filter=amount > 1000000 AND account LIKE 'VN%'

Nghe rất tiện, nhưng đây là thảm họa bảo mật: chuỗi filter được ghép thẳng vào câu SQL phía sau, mở toang cửa cho SQL injection. Trong một đợt rà soát bảo mật, đội security phát hiện có thể truyền filter=1=1 OR DROP... và thao túng truy vấn.

Giải pháp: chuyển sang cú pháp có cấu trúc, whitelist (danh sách trắng) các field được phép lọc và các toán tử được phép, ví dụ chỉ chấp nhận amount[gte], account[prefix], không cho truyền SQL thô. Mọi giá trị đều được tham số hóa (parameterized query). Bài học: "linh hoạt tối đa" trong filter thường đồng nghĩa với "rủi ro tối đa". Là BA, khi review spec, hãy luôn hỏi: "Những field nào được phép lọc? Toán tử nào được phép? Có whitelist không?" Đừng bao giờ để API nhận biểu thức tự do từ client mà không kiểm soát.

Tình huống 3 — App giao đồ ăn và sự nhập nhằng giữa search và filter

Một startup giao đồ ăn (kiểu GrabFood/ShopeeFood) có endpoint GET /restaurants. Đội product yêu cầu "thanh tìm kiếm" cho người dùng gõ tên quán. Dev đặt nó vào filter exact match: ?name=Phở Hòa. Kết quả: người dùng gõ "pho hoa" (không dấu) hoặc "Phở Hoà" (sai chính tả) đều ra rỗng. Tỷ lệ tìm kiếm thất bại lên tới 40%, ảnh hưởng trực tiếp doanh thu.

Vấn đề là họ dùng filter (so khớp chính xác) cho một bài toán vốn cần search (tìm mờ, bỏ dấu, xếp hạng theo độ liên quan). Khi tách bạch lại: ?q=pho hoa cho ô tìm kiếm tự do (full-text, bỏ dấu, fuzzy), còn ?cuisine=vietnamese&is_open=true cho các bộ lọc chính xác — trải nghiệm cải thiện rõ rệt. Bài học: đừng nhầm lẫn search với filter. Tên quán, mô tả, ghi chú là dữ liệu văn bản tự do → dùng q với full-text. Trạng thái, danh mục, khoảng giá là dữ liệu có cấu trúc → dùng filter chính xác.

Hướng dẫn từng bước

Khi bạn cần đặc tả phần filter/sort/search cho một endpoint danh sách, hãy làm theo trình tự sau:

  • Liệt kê các field nghiệp vụ cần lọc. Ngồi với stakeholder, hỏi: "Trong thực tế người dùng/đối tác sẽ lọc theo cái gì?" Ví dụ với đơn hàng: status, city, amount, created_at, customer_type. Đây là cơ sở cho whitelist.
  • Xác định toán tử cho từng field. Field dạng enum (status) thường chỉ cần =in. Field số/ngày (amount, created_at) cần gte/lte/gt/lt cho khoảng. Field text tự do → để dành cho search, không phải filter.
  • Chọn một phong cách cú pháp và ghi vào tài liệu chuẩn. Suffix (amount_gte) hay bracket (amount[gte])? Chọn một và áp dụng nhất quán cho mọi endpoint. Đừng để mỗi dev một kiểu.
  • Định nghĩa sorting. Chốt tham số (sort với tiền tố -), liệt kê các field được phép sort, và quy định sort mặc định khi client không truyền gì (ví dụ mặc định -created_at). Đây là chi tiết hay bị quên.
  • Tách bạch search. Quyết định q áp dụng trên những trường nào (tên + mô tả?), có bỏ dấu tiếng Việt không, có hỗ trợ fuzzy không. Ghi rõ kỳ vọng.
  • Quy định cách kết hợp. Nói rõ: nhiều field = AND; có cần OR cấp cao không (nếu có mới cân nhắc DSL như RSQL). Đừng vẽ vời DSL khi nghiệp vụ chỉ cần AND đơn giản.
  • Đặc tả hành vi lỗi và mặc định. Field không được phép lọc thì server làm gì — báo lỗi 400 hay lờ đi? Toán tử sai thì sao? Giá trị sai kiểu (truyền chữ vào field số) thì sao? Ghi rõ trong spec.
  • Viết ví dụ thực tế trong tài liệu. Mỗi endpoint nên có 2-3 ví dụ query đầy đủ kèm giải thích, để dev và đối tác hiểu ngay.

Lỗi thường gặp & mẹo

Lỗi 1 — Không định nghĩa sort mặc định. Khi client không truyền sort, nếu server trả về thứ tự "ngẫu nhiên" (theo database), kết hợp với phân trang sẽ gây trùng/sót bản ghi giữa các trang. Luôn có một sort mặc định ổn định, lý tưởng là theo một field duy nhất như id hoặc created_at.

Lỗi 2 — Cho phép lọc/sort trên field tùy ý. Nếu client sort theo một cột không có index trong DB, một query có thể làm sập hệ thống. Whitelist field được phép sort/filter, không mở toang.

Lỗi 3 — Nhầm encode trên URL. Các ký tự như khoảng trắng, dấu +, &, dấu tiếng Việt phải được URL-encode. "Phở Hòa" trong query string sẽ thành Ph%E1%BB%9F%20H%C3%B2a. Khi test bằng Postman/curl, để ý vấn đề encode kẻo kết quả sai mà tưởng do logic.

Lỗi 4 — Bỏ qua giới hạn độ dài URL. Filter quá phức tạp nhồi hết vào query string có thể vượt giới hạn độ dài URL (thường ~2000 ký tự an toàn). Nếu filter cực phức tạp, cân nhắc dùng POST với body — nhưng đó là ngoại lệ, không phải mặc định.

Mẹo 1 — Thiết kế "thân thiện với người gõ tay". Cú pháp ?status=paid&sort=-created_at ai cũng đọc và sửa được trên thanh địa chỉ. DSL kiểu RSQL thì không. Ưu tiên sự đơn giản trừ khi nghiệp vụ thực sự đòi hỏi.

Mẹo 2 — Phân biệt rạch ròi ba khái niệm trong đầu. Filter = đúng/sai (boolean), Sort = thứ tự, Search = độ liên quan (relevance). Mỗi cái dùng tham số riêng, đừng trộn lẫn.

Mẹo 3 — Hỏi về tiếng Việt không dấu ngay từ đầu. Ở thị trường Việt Nam, người dùng gõ không dấu rất nhiều. Đảm bảo search có khả năng chuẩn hóa dấu (accent-insensitive), nếu không sẽ mất một lượng lớn kết quả hợp lệ.

Bài tập thực hành

  • Đọc hiểu query: Cho URL GET /api/orders?status[in]=paid,shipped&amount[gte]=500000&city=DaNang&sort=-created_at,-amount&q=giao gấp. Hãy viết bằng tiếng Việt API này đang lấy dữ liệu gì, theo thứ tự nào, và phần nào là filter, phần nào là search.
  • Thiết kế cú pháp: Bạn là BA cho API GET /properties của một sàn bất động sản. Stakeholder cần lọc theo: quận (có thể chọn nhiều quận), khoảng giá, số phòng ngủ tối thiểu, và tìm theo mô tả ("gần trường học"). Hãy đề xuất một query string đầy đủ cho yêu cầu: "căn ở quận 1 hoặc quận 3, giá 3-5 tỷ, từ 2 phòng ngủ, có chữ 'gần trường' trong mô tả, sắp giá tăng dần."
  • Review spec: Một dev đề xuất cho phép client truyền ?filter= với biểu thức RSQL tự do cho API public. Hãy viết 3 câu hỏi/quan ngại bạn sẽ nêu trong buổi review, dựa trên các bài học ở phần tình huống thực tế.
  • Sort mặc định: Endpoint GET /transactions hiện không định nghĩa sort mặc định và đang phân trang. Hãy mô tả vấn đề có thể xảy ra và đề xuất cách khắc phục.

Tóm tắt

Filtering, sorting và search là ba khả năng nền tảng của bất kỳ API danh sách nào, và việc đặc tả tốt phần này thuộc về trách nhiệm của BA. Ghi nhớ ba ý cốt lõi: Filter trả lời "có khớp điều kiện không" — dùng query parameter với toán tử (=, in, gte, lte), nhiều field hiểu là AND, cần whitelist để an toàn. Sort quyết định thứ tự — dùng sort với tiền tố - cho giảm dần, hỗ trợ multi-sort, và luôn có sort mặc định ổn định. Search trả lời "khớp tới mức nào" — dùng q cho full-text tìm mờ trên văn bản, đặc biệt lưu ý tiếng Việt không dấu.

Bốn nguyên tắc vàng để mang theo: (1) nhất quán cú pháp toàn hệ thống; (2) whitelist field và toán tử để tránh injection và quá tải; (3) đừng nhầm search với filter; (4) luôn định nghĩa hành vi mặc định và hành vi lỗi. Khi bạn review một API spec mà thấy đủ những điều này, đó là dấu hiệu của một thiết kế chững chạc — và bạn, với tư cách BA, chính là người canh cổng cho chất lượng đó.