Спроектируем стабильную страницу результатов и выберем способ последовательного чтения.
Примеры course-products используют учебную схему и товар из главы 5 «Mapping и типы полей». Разборы упражнений ниже используют отдельный публичный набор тренажёра.
- Порядок выдачи
- Глубина страницы
- Контракт результата
Спроектируем стабильную страницу результатов и выберем способ последовательного чтения.
Курс ориентирован на Elasticsearch 9.4.3. Разборы упражнений используют публичный набор products; остальные запросы показывают отдельные учебные индексы и могут требовать подготовки или прав, недоступных в тренажёре. Ответы сокращены, score может меняться. Для расширенных функций проверяйте лицензию и вариант развёртывания.
Порядок выдачи
Сортировка по полю меняет роль score: если нужен текстовый порядок, задайте его явно. Для многозначного поля sort mode выбирает одно значение, missing определяет позицию пропусков. При равных значениях нужен уникальный tie-breaker с doc values, например отдельный product_id типа keyword. _id не стоит использовать как обычное поле сортировки.
СамопроверкаГлубина страницы
from/size удобны для первых страниц, но шарды должны собрать кандидатов и для пропущенной части. search_after передаёт sort-значения последнего результата и подходит для последовательного чтения. PIT фиксирует поисковое представление между запросами, уменьшая пропуски и повторы при обновлениях. Курсор привязан к запросу и порядку сортировки; PIT нужно продлевать и закрывать.
СамопроверкаКонтракт результата
_source filtering уменьшает возвращаемый JSON, но не обещает избежать всех затрат загрузки _source. fields и docvalue_fields решают другие способы извлечения значений. Highlighting показывает фрагменты, а field collapsing сворачивает группы результатов; hits.total при collapse не становится числом групп. Scroll предназначен для пакетного обхода, а не пользовательской навигации.
Полученный PIT id используют в запросах search_after и затем закрывают.
POST /course-products/_pit?keep_alive=1mКак читать результат
Полученный PIT id используют в запросах search_after и затем закрывают.
Разбор на примере каталога
Если два товара стоят 99, сортировка только по price не задаёт однозначного порядка между ними. Добавьте product_id, сохраните массив sort последнего hit и передайте его как search_after. При использовании PIT запрос отправляют с pit.id и согласованной сортировкой, а не открывают новый PIT на каждой странице. Не превращайте курсор в обещание произвольного перехода на страницу 100: последовательная пагинация и прямой номер страницы — разные интерфейсы.
Продолжение по ключам сортировки
{
"size": 10,
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
},
{
"product_id": "asc"
}
],
"search_after": [
99,
"sku-1"
]
}Как читать результат
Значения 99 и sku-1 — учебный курсор. В реальном ответе берут весь массив sort последнего hit. Этот пример показывает только курсор; для согласованного обхода меняющегося индекса добавляют PIT и используют его id в последующих запросах.
Сортировка по цене
sort — упорядоченный список ключей. Elasticsearch сравнивает первый, а второй использует только при равенстве первого. Пример запрашивает самые дорогие товары с id по возрастанию для равных цен. Направления задаются как asc и desc.
Сортируйте по полям с подходящими индексированными значениями: price, rating, id, brand.keyword. Анализируемое name здесь не подходит как keyword-ключ. При явной сортировке не по оценке _score может быть null: вычисление релевантности не понадобилось. null не означает неудачный поиск.
Разберём другой сценарий
{
"query": {
"match_all": {}
},
"size": 2,
"_source": [
"name",
"price"
],
"sort": [
{
"price": "desc"
},
{
"id": "asc"
}
]
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": [
{
"_id": "10",
"_score": null,
"_source": {
"name": "Logitech Wireless Charger 10",
"price": 2499
}
},
{
"_id": "20",
"_score": null,
"_source": {
"name": "Samsung Wireless Charger 20",
"price": 2499
}
}
]
}
}Как читать результат
Цены убывают. При равенстве максимальной цены первым будет меньший id. Второй ключ не пересортировывает весь результат, а разрешает равенство первого.
Вторая страница
from — смещение с нуля, а не номер страницы. Для страницы p длиной n используется from = (p − 1) × n и size = n. Пример запрашивает четвёртую страницу по три элемента: from равен 9. Фильтрация и сортировка происходят до применения смещения.
Используйте одну детерминированную сортировку на всех страницах. Иначе равные значения могут приводить к пересечениям или пропускам. Тренажёр ограничивает выдачу 100 документами и from + size значением 500: это небольшие учебные страницы, а не глубокая промышленная пагинация.
Разберём другой сценарий
{
"query": {
"match_all": {}
},
"size": 3,
"_source": [
"name",
"price"
],
"sort": [
{
"id": "asc"
}
],
"from": 9
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": [
{
"_id": "10",
"_score": null,
"_source": {
"name": "Logitech Wireless Charger 10",
"price": 2499
}
},
{
"_id": "11",
"_score": null,
"_source": {
"name": "Dell Wireless Headphones 11",
"price": 49
}
},
{
"_id": "12",
"_score": null,
"_source": {
"name": "apple Premium Wireless Headphones 12",
"price": 99
}
}
]
}
}Как читать результат
ID равны 10, 11 и 12, тогда как total описывает все 300 совпадений. Длина страницы и размер отбора отвечают на разные вопросы.
Поля ответа
Фильтрация _source выбирает исходные JSON-свойства для каждого результата. Она не меняет запрос, оценку и доступные поля сортировки. Пример возвращает только name и category, но сортирует по id: ключ сортировки не обязан присутствовать в ответе.
_id и _score — метаданные результата вне _source. Сокращение исходных полей поэтому не удаляет идентификатор. Указывайте исходные пути полей: подполе mapping вроде brand.keyword не означает отдельное свойство _source.
Разберём другой сценарий
{
"query": {
"match_all": {}
},
"size": 2,
"_source": [
"name",
"category"
],
"sort": [
{
"id": "asc"
}
]
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": [
{
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless",
"category": "Smartphones"
}
},
{
"_id": "2",
"_score": null,
"_source": {
"name": "Reference device",
"category": "Audio"
}
}
]
}
}Как читать результат
В каждом source-объекте ровно name и category. У результата сохраняются _id и _score, а порядок по-прежнему определяется id.
Страница каталога для API
Контракт API может одновременно задавать отбор, порядок, страницу и проекцию. Сначала ограничьте наличие и индексированный рейтинг, исключите preorder. Затем отсортируйте rating по убыванию, price и id по возрастанию. Наконец выберите вторую страницу по три элемента и только name, price, rating в _source. Каждый шаг имеет самостоятельный смысл.
exists по rating до сортировки удаляет отсутствующие и null-значения из набора: не нужно полагаться на размещение пропусков по умолчанию. Уникальный id последним ключом делает равенство рейтинга и цены воспроизводимым. Даже правильные ID нарушают контракт, если возвращается лишнее исходное поле.
Разберём другой сценарий
{
"query": {
"bool": {
"filter": [
{
"term": {
"in_stock": true
}
},
{
"exists": {
"field": "rating"
}
}
],
"must_not": [
{
"term": {
"status": "preorder"
}
}
]
}
},
"size": 3,
"_source": [
"name",
"price",
"rating"
],
"sort": [
{
"rating": "desc"
},
{
"price": "asc"
},
{
"id": "asc"
}
],
"from": 3
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 86,
"relation": "eq"
},
"hits": [
{
"_id": "299",
"_score": null,
"_source": {
"name": "Dell Premium Travel Laptop 299",
"price": 1299,
"rating": 4.9
}
},
{
"_id": "38",
"_score": null,
"_source": {
"name": "Samsung Headphones Premium 38",
"price": 1000,
"rating": 4.8
}
},
{
"_id": "158",
"_score": null,
"_source": {
"name": "Samsung Headphones Premium 158",
"price": 1000,
"rating": 4.8
}
}
]
}
}Как читать результат
Отдельно проверьте total, три упорядоченных ID и точные ключи source. Их сочетание определяет контракт страницы; изменение size или from меняет запрошенную страницу.
Самопроверка
Сначала ответьте самостоятельно, затем раскройте объяснение.
1. Почему глубокий from дорогой?
Нужно собрать и отсортировать кандидатов включая пропущенные результаты.
2. Что передаёт search_after?
Sort-значения последнего документа предыдущей страницы.
3. Зачем PIT?
Чтобы сохранять согласованный поисковый снимок во время обхода.
4. Почему нужен tie-breaker?
Для однозначного порядка документов с одинаковыми значениями сортировки.
5. Collapse меняет total на число групп?
Нет, число групп нужно получать отдельно.
Краткая памятка
- Нужно собрать и отсортировать кандидатов включая пропущенные результаты.
- Sort-значения последнего документа предыдущей страницы.
- Чтобы сохранять согласованный поисковый снимок во время обхода.
- Для однозначного порядка документов с одинаковыми значениями сортировки.
- Нет, число групп нужно получать отдельно.
Официальная документация
Готовы применить знания?
Подтвердите прочтение, чтобы сохранить своё место. Чтение и решение заданий учитываются отдельно в этом браузере с помощью гостевой cookie.