Фильтры, сортировка и пагинация
Как настроить поиск товаров — фильтры по цене и бренду, сортировка по цене и дате, пошаговый просмотр результатов.
Фильтры, сортировка и пагинация
Когда вы выполняете поиск товаров, часто нужно ограничить результаты по цене, бренду или доступности, а также упорядочить их по релевантности, цене или дате. Эта страница показывает, как это делается.
Как ограничить результаты (фильтры)
Простые фильтры — точное совпадение
Найти товары конкретного бренда, в наличии или локали:
brand:=Apple # Только Apple товары
availability:=in_stock # Только в наличии
price:>100 # Только дороже $100
price:<500 # Только дешевле $500Диапазон цены
Найти товары в определённом диапазоне цены:
price:[50..200] # От $50 до $200 включительно
price:(50..200) # От $50 до $200 исключительно (50 и 200 не входят)
price:>100 && price:<500 # От $100 до $500 (другой синтаксис)Несколько значений (OR)
Если нужны товары из нескольких брендов:
brand:=Sony || brand:=Samsung || brand:=LGИли эквивалентно:
brand:=[Sony, Samsung, LG]Несколько условий одновременно (AND)
Найти товары Apple в наличии дешевле $500:
brand:=Apple && availability:=in_stock && price:<500Комбинирование с группировкой
(brand:=Apple || brand:=Samsung) && availability:=in_stock && price:[100..1000]Результат: товары Apple ИЛИ Samsung, И в наличии, И в диапазоне цены.
Фасеты — статистика для UI фильтров
Когда вы выполняете поиск, попросите AACsearch вернуть статистику по полям (сколько товаров каждого бренда, в каких категориях и т.д.). Это нужно для отображения фильтров в боковой панели приложения.
{
"q": "headphones",
"facetBy": "brand,categories,availability"
}Ответ включит количество товаров для каждого значения:
{
"hits": [...товары...],
"facetCounts": [
{
"fieldName": "brand",
"counts": [
{ "value": "Sony", "count": 45 },
{ "value": "Bose", "count": 28 },
{ "value": "Sennheiser", "count": 19 }
]
},
{
"fieldName": "availability",
"counts": [
{ "value": "in_stock", "count": 89 },
{ "value": "out_of_stock", "count": 23 }
]
}
]
}Используйте эти цифры для отображения фильтров: "Sony (45)", "Bose (28)", "В наличии (89)" и т.д.
Фасеты с активными фильтрами
Когда пользователь уже применил фильтр (например, "только Sony"), счётчики фасетов обновляются — они показывают статистику уже отфильтрованных товаров:
{
"q": "headphones",
"filterBy": "brand:=Sony",
"facetBy": "categories,availability"
}Теперь в фасетах показаны только категории и наличие товаров Sony, а не всех товаров.
Как упорядочить результаты (сортировка)
По цене (дешевле или дороже в начале)
sortBy: price:asc # Дешевые товары в начале
sortBy: price:desc # Дорогие товары в началеПо дате добавления (новые товары)
sortBy: created_at:desc # Новые товары в начале
sortBy: created_at:asc # Старые товары в началеПо релевантности (по умолчанию)
sortBy: _text_match:descЭто значение по умолчанию — товары отсортированы по релевантности поисковому запросу.
Комбинирование (сначала релевантность, потом цена)
Сортируйте по нескольким полям одновременно. Сначала по основному критерию, потом по дополнительному:
sortBy: _text_match:desc,price:ascТовары будут отсортированы сначала по релевантности, а если релевантность одинаковая, то по цене (дешевле в начале).
Типичные варианты сортировки
| Вариант | Код |
|---|---|
| Самые релевантные | _text_match:desc |
| Цена: дешевле в начале | price:asc |
| Цена: дороже в начале | price:desc |
| Новые товары первыми | created_at:desc |
| Скидочная цена: дешевле | sale_price:asc |
Пошаговый просмотр результатов (пагинация)
Показ результатов постранично
Выводите товары по 10-20 в одном запросе, потом показывайте следующую страницу:
{
"q": "laptop",
"page": 1, // Страница (начиная с 1)
"perPage": 20 // Товаров на странице (макс. 100)
}Ответ включает информацию о пагинации
{
"hits": [...20 товаров...],
"found": 142, // Всего найдено товаров
"page": 1, // Текущая страница
"perPage": 20
}Вычисление в коде
const results = { found: 142, perPage: 20 };
const totalPages = Math.ceil(results.found / results.perPage); // 7 страниц
const hasMore = results.page < totalPages; // Есть ещё страницы?
// Для кнопки "Дальше"
if (hasMore) {
showButton("Следующая страница");
}Оптимизация для больших каталогов
Если в каталоге более 100 тысяч товаров, глубокая пагинация может быть медленной. Используйте вместо этого "курсор" — последний ID:
{
"q": "laptop",
"filterBy": "id:>last_seen_id", // ID последнего товара с предыдущей страницы
"sortBy": "id:asc",
"perPage": 20
}Экранирование значений
Значения фильтров парсит filter-engine AACSearch. Большинство строк работают как есть, но несколько символов имеют специальное значение и требуют backtick-кавычек (или эскейпа \) внутри значения:
| Символ | Значение в грамматике | Внутри значения писать как |
|---|---|---|
, | Разделитель элементов массива | `…` |
&& | Оператор AND | `…` |
|| | Оператор OR | `…` |
( ) | Группировка | `…` |
[ ] | Range / array delimiter | `…` |
: | Разделитель поля/оператора | `…` |
` | Сама кавычка | Эскейп: \` |
Примеры:
# Бренд содержит запятую — обязательно в кавычках
brand:=`Ben & Jerry's`
# Категория содержит литеральный AND
categories:=`Home && Garden`
# Заголовок содержит обратную кавычку
title:=`The \`Quoted\` Title`Числовым и булевым значениям эскейп не нужен. Числа могут быть отрицательными (price:>-100), плавающие — через точку (price:<99.99).
Значения, контролируемые пользователем, всегда экранируются до конкатенации. Стандартный путь — безопасный билдер ниже; никогда не интерполируйте пользовательский ввод прямо в filter-строку.
Безопасный билдер фильтров
Ручная конкатенация filter-строки из пользовательского ввода — это инъекция: клиент с вводом ") || price:>0 || (1:=1 обойдёт ваши бизнес-правила. Всегда стройте фильтры из структурированного представления.
Минимальный TypeScript-билдер:
type AtomicFilter =
| { field: string; op: "="; value: string | number | boolean }
| { field: string; op: "!="; value: string | number | boolean }
| { field: string; op: ">" | "<" | ">=" | "<="; value: number }
| { field: string; op: "in"; values: Array<string | number> }
| { field: string; op: "range"; min: number; max: number; inclusive?: boolean };
type FilterTree =
| AtomicFilter
| { and: FilterTree[] }
| { or: FilterTree[] };
const FIELD_RE = /^[a-z_][a-z0-9_]*$/;
function escapeValue(v: string | number | boolean): string {
if (typeof v === "number" || typeof v === "boolean") return String(v);
return "`" + String(v).replace(/`/g, "\\`") + "`";
}
function fieldOrThrow(field: string): string {
if (!FIELD_RE.test(field)) throw new Error(`invalid field name: ${field}`);
return field;
}
export function buildFilter(node: FilterTree): string {
if ("and" in node) return node.and.map(buildFilter).join(" && ");
if ("or" in node) return "(" + node.or.map(buildFilter).join(" || ") + ")";
const f = fieldOrThrow(node.field);
if (node.op === "in") return `${f}:=[${node.values.map(escapeValue).join(", ")}]`;
if (node.op === "range") {
const [lo, hi] = node.inclusive === false ? ["(", ")"] : ["[", "]"];
return `${f}:${lo}${node.min}..${node.max}${hi}`;
}
return `${f}:${node.op}${escapeValue(node.value)}`;
}Использование на storefront:
const filter = buildFilter({
and: [
{ field: "brand", op: "in", values: form.brands },
{ field: "price", op: "range", min: form.minPrice, max: form.maxPrice },
{ field: "availability", op: "=", value: "in_stock" },
...(form.sale ? [{ field: "sale_price", op: "<", value: form.maxPrice } as const] : []),
],
});Билдер форсит два инварианта: имена полей соответствуют строгому regex (никакой инъекции через имя), строковые значения backtick-эскейпятся (никакой инъекции через значение). Повторите ту же форму на сервере.
Рецепты e-commerce фильтров
Канонические filter-выражения для каталога. Подставляйте значения из UI фасетов.
Категория (одна категория, только in-stock)
{
"indexSlug": "products",
"q": "*",
"filterBy": "categories:=`Audio` && availability:=in_stock",
"facetBy": "brand,price",
"sortBy": "_text_match:desc,popularity_score:desc"
}Drill-down по нескольким брендам/категориям
filterBy: "categories:=[`Electronics`, `Audio`] && brand:=[`Sony`, `Bose`] && availability:=in_stock"string[] через :=[a, b] — OR внутри поля, AND между полями.
Диапазон цены (с веткой «на распродаже»)
filterBy: "categories:=`Audio` && availability:=in_stock && (sale_price:[10..100] || (sale_price:<0 && price:[10..100]))"«Sale-товары в 10–100 плюс не-sale в том же диапазоне, когда sale_price отсутствует». Без OR-группы sale-flagged товары без sale_price отфильтруются — типичный баг.
Locale-aware listing
filterBy: "locale:=`en` && categories:=`Audio` && availability:=in_stock"Для мультилокального storefront-а всегда фильтр по локали. Иначе французский покупатель увидит англоязычные товары.
Search-as-you-type
{
"q": "wireless",
"queryBy": "title,brand",
"perPage": 5,
"includeFields": "id,title,brand,price,image_url",
"filterBy": "availability:=in_stock"
}Suggestions почти всегда включают in-stock — out-of-stock suggestion это UX-ловушка.
«Похожие товары» (vector hybrid)
{
"q": "*",
"vectorQuery": "embedding:([…вектор текущего товара…], k:50)",
"filterBy": "id:!=`product-123` && availability:=in_stock && price:[1000..15000]",
"perPage": 12
}id:!= исключает текущий товар; price-range — тот же сегмент, чтобы рекомендации не уехали в другой ценовой пояс.
Выбор нужных полей для ускорения
Если вам не нужны все поля (например, пока не показываете описание), попросите вернуть только необходимые:
{
"q": "laptop",
"includeFields": "id,title,price,brand",
"excludeFields": "description,detailed_specs"
}Меньше данных = быстрее загрузка.
Несколько поисков в одном запросе
Как выполнить несколько поисков одновременно — для автодополнения, поиска одновременно в нескольких индексах.
Релевантность поиска
Обработка запроса, веса queryBy, typo tolerance, synonyms, curations, ранжирование — developer-справочник, как движок решает, что вернуть.