searchCriteria: filtering, sorting and paging
searchCriteria is an AND of ORs - filter groups, sort orders and paging - expressed either as a query string or built in PHP.
searchCriteria[filter_groups][0][filters][0][field]=status&searchCriteria[filter_groups][0][filters][0][value]=approved&searchCriteria[pageSize]=20A repository's getList method takes a SearchCriteriaInterface, Magento's storage-independent description of a query. Over REST you do not build one by hand: the framework assembles it from the query string of a GET request.
An AND of ORs
A search criteria holds filter groups. Filters inside one group are combined with OR. The groups themselves are combined with AND. So two conditions that must both hold go in two groups, and two alternatives go in one group. Each filter has a field, a value and an optional condition_type, which defaults to eq and may also be neq, like, nlike, in, nin, gt, gteq, lt, lteq, moreq, from, to, null, notnull, finset or nfinset.
Sorting uses searchCriteria[sortOrders][0][field] with [direction] set to ASC or DESC. Paging uses searchCriteria[pageSize] and searchCriteria[currentPage]. The casing is inconsistent and worth memorising: filter_groups, condition_type and filters are snake_case, while sortOrders, pageSize and currentPage are camelCase.
What trips people up
Without a pageSize the response contains every matching row, which times out on a large table. The limits that cap this live under Stores > Settings > Configuration > Services > Magento Web API > Web Api Input Limits, and in 2.4.9 they are switched off until Enable Input Limits is set to Yes. Once on, a supplied pageSize is rejected above the configured Maximum Page Size, which falls back to 300 when the field is left empty. There is no shipped default of 20 for Default Page Size: the field ships empty and the fallback injected in app/etc/di.xml is 999999, so set it yourself if a request without pageSize should be capped. A like filter needs its own % wildcards in the value.
Example
<?php
// app/code/Vendor/Reviews/Model/ApprovedReviewSearch.php
declare(strict_types=1);
namespace Vendor\Reviews\Model;
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Api\SortOrderBuilder;
use Vendor\Reviews\Api\Data\ReviewSearchResultsInterface;
use Vendor\Reviews\Api\ReviewRepositoryInterface;
class ApprovedReviewSearch
{
public function __construct(
private readonly ReviewRepositoryInterface $reviewRepository,
private readonly SearchCriteriaBuilder $searchCriteriaBuilder,
private readonly FilterBuilder $filterBuilder,
private readonly SortOrderBuilder $sortOrderBuilder
) {
}
public function forSku(string $sku): ReviewSearchResultsInterface
{
// Group 0: one filter. addFilter() is shorthand for the addFilters() call below it.
$this->searchCriteriaBuilder->addFilter('product_sku', $sku);
// Group 1: two filters in one group, so they are OR'd with each other
// and AND'd with group 0.
$this->searchCriteriaBuilder->addFilters([
$this->filterBuilder->setField('status')->setValue('approved')->create(),
$this->filterBuilder->setField('status')->setValue('featured')->create(),
]);
$sortOrder = $this->sortOrderBuilder
->setField('created_at')
->setDirection(SortOrder::SORT_DESC)
->create();
$criteria = $this->searchCriteriaBuilder
->setSortOrders([$sortOrder])
->setPageSize(20)
->create();
return $this->reviewRepository->getList($criteria);
}
}When to use it
- Pull only the orders changed since the last sync, newest first, 50 at a time.
- Return products whose status is enabled and whose price is above a threshold.
- Let a storefront filter a listing without exposing a raw query to the client.
More examples
The same query as a URL
This is the query string form of the PHP above, and the two are the same object once the framework is finished parsing. Note the group indexes: moving the second status filter into group 0 changes the meaning from AND to OR. The line breaks here are for reading only; a real request is one line with the brackets URL-encoded by your client.
GET /rest/default/V1/vendor-reviews/search
?searchCriteria[filter_groups][0][filters][0][field]=product_sku
&searchCriteria[filter_groups][0][filters][0][value]=24-MB01
&searchCriteria[filter_groups][1][filters][0][field]=status
&searchCriteria[filter_groups][1][filters][0][value]=approved
&searchCriteria[filter_groups][1][filters][1][field]=status
&searchCriteria[filter_groups][1][filters][1][value]=featured
&searchCriteria[sortOrders][0][field]=created_at
&searchCriteria[sortOrders][0][direction]=DESC
&searchCriteria[pageSize]=20
# product_sku = '24-MB01' AND (status = 'approved' OR status = 'featured')What a search response contains
Three keys come back from any getList route: items, search_criteria echoed back as the framework understood it, and total_count, which is the number of matching rows before paging. Read search_criteria when a filter appears to be ignored - if your field is missing from the echo, the query string was malformed and the filter was never applied. Use total_count, not count(items), to drive pagination in a client.
{
"items": [
{
"review_id": 42,
"product_sku": "24-MB01",
"rating": 5,
"status": "approved"
}
],
"search_criteria": {
"filter_groups": [
{
"filters": [
{
"field": "product_sku",
"value": "24-MB01",
"condition_type": "eq"
}
]
}
],
"page_size": 20
},
"total_count": 1
}Applying the criteria to your own collection
CollectionProcessorInterface::process translates the whole criteria - filter groups, sort orders and paging - into calls on the collection, so you never write that translation yourself. This also keeps values bound as parameters. Building the WHERE clause by hand with a concatenated value, or passing a raw string to addFieldToFilter, is how SQL injection reaches a Magento API: addFieldToFilter is not automatically safe.
<?php
// app/code/Vendor/Reviews/Model/ReviewList.php
declare(strict_types=1);
namespace Vendor\Reviews\Model;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Vendor\Reviews\Model\ResourceModel\Review\CollectionFactory;
class ReviewList
{
public function __construct(
private readonly CollectionFactory $collectionFactory,
private readonly CollectionProcessorInterface $collectionProcessor
) {
}
/**
* @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
* @return \Vendor\Reviews\Api\Data\ReviewInterface[]
*/
public function execute(SearchCriteriaInterface $searchCriteria): array
{
$collection = $this->collectionFactory->create();
$this->collectionProcessor->process($searchCriteria, $collection);
return $collection->getItems();
}
}
Discussion