Сортировка и пагинация: from/size и search_after
Учимся листать результаты поиска и сортировать их, а главное — обходить проблему глубокой пагинации.
Пагинация — постраничный вывод результатов; в ES базовый способ — параметры
fromиsize, но у него есть предел.
from и size
По умолчанию _search возвращает 10 самых релевантных документов. Чтобы получить другую страницу, задают from (сколько пропустить) и size (сколько вернуть).
{
"from": 20,
"size": 10,
"query": { "match": { "title": "ноутбук" } }
}Это третья страница по 10 элементов (пропустить 20, взять 10). Просто и удобно — для первых страниц.
Сортировка
По умолчанию документы идут по _score (релевантность). Можно отсортировать по полю:
{
"sort": [ { "price": "asc" } ],
"query": { "match": { "title": "ноутбук" } }
}Внимание: при сортировке по полю релевантность не считается, и _score будет null. Сортировать можно по keyword, числам, датам — но не по анализируемому text.
Проблема глубокой пагинации
Запрос «дай страницу 1000 по 10» (from: 9990) выглядит безобидно, но дорог. Чтобы вернуть документы с 9991 по 10000, координатор должен собрать с каждого шарда топ-10000, всё пересортировать и выбросить первые 9990. Чем глубже страница, тем больше данных гоняется и сортируется впустую. Поэтому ES по умолчанию запрещает from + size > 10000 — это защита от тяжёлых запросов.
from=9990, size=10 --> каждый шард шлёт топ-10000
координатор сортирует 10000*N
отдаёт 10 штук, остальное в мусорРешение: search_after
Для глубокого листания (выгрузки, бесконечной ленты) используют search_after. Идея: вместо «пропусти N» — «дай то, что идёт после вот этого документа». Сортируем по уникальному ключу и передаём значения сортировки последнего показанного документа.
{
"size": 10,
"sort": [ { "price": "asc" }, { "_id": "asc" } ],
"search_after": [ 24990, "product-42" ],
"query": { "match_all": {} }
}Так каждая «страница» — это лёгкий запрос с курсором, без перебора всего, что до неё. Цена не растёт с глубиной.
Как работает под капотом
from/size заставляет каждый шард отдавать from + size кандидатов, чтобы координатор гарантированно собрал правильный топ после слияния — отсюда квадратичный рост стоимости с глубиной. search_after же сразу говорит шардам «начни после этого ключа сортировки», и каждый шард отдаёт только следующие size документов. Для стабильности обязательно добавлять в сортировку уникальный тай-брейкер (например, _id), иначе документы с одинаковым значением ключа могут потеряться или продублироваться.
Частые ошибки
- Глубокий from для выгрузок. Листать тысячами страниц через
from— путь к таймаутам. Для полной выгрузки или глубоких страниц —search_after. - search_after без уникального ключа. Без тай-брейкера в сортировке курсор неустойчив — добавляйте
_idпоследним полем сортировки. - Сортировка по text-полю. Анализируемое поле не сортируется по значению; используйте keyword-версию.
Итоги
from/size— простая пагинация для первых страниц;from + sizeограничен (по умолчанию 10000).- Глубокая пагинация через
fromдорога: координатор собирает и сортирует всё до нужной страницы. - Для глубокого листания и выгрузок —
search_afterс уникальным тай-брейкером в сортировке.