AACsearch
Поисковый API

Фильтры, сортировка и пагинация

Как настроить поиск товаров — фильтры по цене и бренду, сортировка по цене и дате, пошаговый просмотр результатов.

Фильтры, сортировка и пагинация

Когда вы выполняете поиск товаров, часто нужно ограничить результаты по цене, бренду или доступности, а также упорядочить их по релевантности, цене или дате. Эта страница показывает, как это делается.

Как ограничить результаты (фильтры)

Простые фильтры — точное совпадение

Найти товары конкретного бренда, в наличии или локали:

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:&lt;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"
}

Меньше данных = быстрее загрузка.

On this page

Фильтры, сортировка и пагинацияКак ограничить результаты (фильтры)Простые фильтры — точное совпадениеДиапазон ценыНесколько значений (OR)Несколько условий одновременно (AND)Комбинирование с группировкойФасеты — статистика для UI фильтровФасеты с активными фильтрамиКак упорядочить результаты (сортировка)По цене (дешевле или дороже в начале)По дате добавления (новые товары)По релевантности (по умолчанию)Комбинирование (сначала релевантность, потом цена)Типичные варианты сортировкиПошаговый просмотр результатов (пагинация)Показ результатов постраничноОтвет включает информацию о пагинацииВычисление в кодеОптимизация для больших каталоговЭкранирование значенийБезопасный билдер фильтровРецепты e-commerce фильтровКатегория (одна категория, только in-stock)Drill-down по нескольким брендам/категориямДиапазон цены (с веткой «на распродаже»)Locale-aware listingSearch-as-you-type«Похожие товары» (vector hybrid)Выбор нужных полей для ускорения