Оглавление курса

Выдача, сортировка и пагинация

Спроектируем стабильную страницу результатов и выберем способ последовательного чтения.

Разделов: 8Elasticsearch 9.4.3Ещё не прочитан

Чему вы научитесь

  • Порядок выдачи
  • Глубина страницы
  • Контракт результата

Спроектируем стабильную страницу результатов и выберем способ последовательного чтения.

Примеры course-products используют учебную схему и товар из главы 5 «Mapping и типы полей». Разборы упражнений ниже используют отдельный публичный набор тренажёра.

Логика главы
  • Порядок выдачи
  • Глубина страницы
  • Контракт результата

Спроектируем стабильную страницу результатов и выберем способ последовательного чтения.

Курс ориентирован на Elasticsearch 9.4.3. Разборы упражнений используют публичный набор products; остальные запросы показывают отдельные учебные индексы и могут требовать подготовки или прав, недоступных в тренажёре. Ответы сокращены, score может меняться. Для расширенных функций проверяйте лицензию и вариант развёртывания.

01 / 08

Порядок выдачи

Сортировка по полю меняет роль score: если нужен текстовый порядок, задайте его явно. Для многозначного поля sort mode выбирает одно значение, missing определяет позицию пропусков. При равных значениях нужен уникальный tie-breaker с doc values, например отдельный product_id типа keyword. _id не стоит использовать как обычное поле сортировки.

Самопроверка
02 / 08

Глубина страницы

from/size удобны для первых страниц, но шарды должны собрать кандидатов и для пропущенной части. search_after передаёт sort-значения последнего результата и подходит для последовательного чтения. PIT фиксирует поисковое представление между запросами, уменьшая пропуски и повторы при обновлениях. Курсор привязан к запросу и порядку сортировки; PIT нужно продлевать и закрывать.

Самопроверка
03 / 08

Контракт результата

_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 и затем закрывают.

Самопроверка
04 / 08

Разбор на примере каталога

Если два товара стоят 99, сортировка только по price не задаёт однозначного порядка между ними. Добавьте product_id, сохраните массив sort последнего hit и передайте его как search_after. При использовании PIT запрос отправляют с pit.id и согласованной сортировкой, а не открывают новый PIT на каждой странице. Не превращайте курсор в обещание произвольного перехода на страницу 100: последовательная пагинация и прямой номер страницы — разные интерфейсы.

Продолжение по ключам сортировки

POST /course-products/_search
{
  "size": 10,
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": "asc"
    },
    {
      "product_id": "asc"
    }
  ],
  "search_after": [
    99,
    "sku-1"
  ]
}

Как читать результат

Значения 99 и sku-1 — учебный курсор. В реальном ответе берут весь массив sort последнего hit. Этот пример показывает только курсор; для согласованного обхода меняющегося индекса добавляют PIT и используют его id в последующих запросах.

Самопроверка
05 / 08

Сортировка по цене

sort — упорядоченный список ключей. Elasticsearch сравнивает первый, а второй использует только при равенстве первого. Пример запрашивает самые дорогие товары с id по возрастанию для равных цен. Направления задаются как asc и desc.

Сортируйте по полям с подходящими индексированными значениями: price, rating, id, brand.keyword. Анализируемое name здесь не подходит как keyword-ключ. При явной сортировке не по оценке _score может быть null: вычисление релевантности не понадобилось. null не означает неудачный поиск.

Разберём другой сценарий

POST /products/_search
{
  "query": {
    "match_all": {}
  },
  "size": 2,
  "_source": [
    "name",
    "price"
  ],
  "sort": [
    {
      "price": "desc"
    },
    {
      "id": "asc"
    }
  ]
}
Ответ Elasticsearch
Сокращённый ответ · JSON
{
  "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. Второй ключ не пересортировывает весь результат, а разрешает равенство первого.

Закрепить в задании 16
06 / 08

Вторая страница

from — смещение с нуля, а не номер страницы. Для страницы p длиной n используется from = (p − 1) × n и size = n. Пример запрашивает четвёртую страницу по три элемента: from равен 9. Фильтрация и сортировка происходят до применения смещения.

Используйте одну детерминированную сортировку на всех страницах. Иначе равные значения могут приводить к пересечениям или пропускам. Тренажёр ограничивает выдачу 100 документами и from + size значением 500: это небольшие учебные страницы, а не глубокая промышленная пагинация.

Разберём другой сценарий

POST /products/_search
{
  "query": {
    "match_all": {}
  },
  "size": 3,
  "_source": [
    "name",
    "price"
  ],
  "sort": [
    {
      "id": "asc"
    }
  ],
  "from": 9
}
Ответ Elasticsearch
Сокращённый ответ · JSON
{
  "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 совпадений. Длина страницы и размер отбора отвечают на разные вопросы.

Закрепить в задании 17
07 / 08

Поля ответа

Фильтрация _source выбирает исходные JSON-свойства для каждого результата. Она не меняет запрос, оценку и доступные поля сортировки. Пример возвращает только name и category, но сортирует по id: ключ сортировки не обязан присутствовать в ответе.

_id и _score — метаданные результата вне _source. Сокращение исходных полей поэтому не удаляет идентификатор. Указывайте исходные пути полей: подполе mapping вроде brand.keyword не означает отдельное свойство _source.

Разберём другой сценарий

POST /products/_search
{
  "query": {
    "match_all": {}
  },
  "size": 2,
  "_source": [
    "name",
    "category"
  ],
  "sort": [
    {
      "id": "asc"
    }
  ]
}
Ответ Elasticsearch
Сокращённый ответ · JSON
{
  "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.

Закрепить в задании 18
08 / 08

Страница каталога для API

Контракт API может одновременно задавать отбор, порядок, страницу и проекцию. Сначала ограничьте наличие и индексированный рейтинг, исключите preorder. Затем отсортируйте rating по убыванию, price и id по возрастанию. Наконец выберите вторую страницу по три элемента и только name, price, rating в _source. Каждый шаг имеет самостоятельный смысл.

exists по rating до сортировки удаляет отсутствующие и null-значения из набора: не нужно полагаться на размещение пропусков по умолчанию. Уникальный id последним ключом делает равенство рейтинга и цены воспроизводимым. Даже правильные ID нарушают контракт, если возвращается лишнее исходное поле.

Разберём другой сценарий

POST /products/_search
{
  "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
Сокращённый ответ · JSON
{
  "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 меняет запрошенную страницу.

Закрепить в задании 28

Самопроверка

Сначала ответьте самостоятельно, затем раскройте объяснение.

1. Почему глубокий from дорогой?

Нужно собрать и отсортировать кандидатов включая пропущенные результаты.

2. Что передаёт search_after?

Sort-значения последнего документа предыдущей страницы.

3. Зачем PIT?

Чтобы сохранять согласованный поисковый снимок во время обхода.

4. Почему нужен tie-breaker?

Для однозначного порядка документов с одинаковыми значениями сортировки.

5. Collapse меняет total на число групп?

Нет, число групп нужно получать отдельно.

Краткая памятка

  • Нужно собрать и отсортировать кандидатов включая пропущенные результаты.
  • Sort-значения последнего документа предыдущей страницы.
  • Чтобы сохранять согласованный поисковый снимок во время обхода.
  • Для однозначного порядка документов с одинаковыми значениями сортировки.
  • Нет, число групп нужно получать отдельно.

Официальная документация

Готовы применить знания?

Подтвердите прочтение, чтобы сохранить своё место. Чтение и решение заданий учитываются отдельно в этом браузере с помощью гостевой cookie.