Перейти к содержанию

Фильтры и пагинация

Найденные документы уже имеют устойчивый порядок. Теперь посетитель должен уметь ограничить выдачу разделом и просмотреть длинный список частями. Эти операции кажутся простыми, но ошибка в их последовательности приводит к пропавшим материалам и неверному счётчику.

Продолжим использовать индекс предыдущего урока. Сначала получим всех подходящих кандидатов, применим фильтры, а затем выделим текущую страницу. Результатом станет единый формат ответа с карточками, общим числом совпадений и сведениями о пагинации.

Фильтр относится к документу

В нашем формате у материала есть section и topic. Это структурированные значения, а не слова, которые следует искать подстрокой в тексте. Фильтр по разделу сравнивает идентификатор раздела; запрос продолжает описывать содержание.

Допустим, XML упомянут в двух материалах. Один относится к backend, другой — к frontend. Без фильтра показываются оба. После выбора backend должен остаться профильный урок, если именно такую рубрику ему назначили в данных.

Названия разделов для человека можно переводить и менять независимо от внутренних идентификаторов. Значение backend в запросе не должно случайно превратиться в текст «Серверная разработка». Для отображения используйте отдельный словарь подписей.

Фильтр не является правом доступа. В нашей учебной выдаче находятся только публичные документы. Если система обслуживает закрытые материалы, проверка прав должна происходить на сервере до передачи текста, а не через выбранный пункт формы.

Порядок операций

Правильная последовательность для этой версии — поиск, фильтрация и разбивка. Представьте, что общая выдача содержит двадцать карточек, а первые десять относятся к другому разделу. Если сначала взять первую страницу и потом фильтровать, пользователь увидит пустоту, хотя ниже есть нужные материалы.

Общий счётчик относится к отфильтрованному набору до выделения страницы. Число карточек на странице может быть меньше. Обе величины полезны, но обозначают разные вещи.

Ранжирование пока не зависит от выбранного раздела: фильтр просто удаляет неподходящие записи из уже упорядоченного массива. Для некоторых библиотек потребуется ранжирование внутри раздела, но это отдельная гипотеза, которую следует сравнивать с контрольными запросами.

Ответ с пагинацией

Текущую searchIndex из предыдущего урока переименуйте в findAll и уберите перед ней слово export. Её тело остаётся прежним. Затем добавьте новую экспортируемую функцию:

export function searchIndex(index, query, options = {}) {
  const section = options.section || "";
  const topic = options.topic || "";
  const requestedPage = Number(options.page ?? 1);
  const pageSize = Number(options.pageSize ?? 10);
  if (!Number.isInteger(requestedPage) || requestedPage < 1 ||
      !Number.isInteger(pageSize) || pageSize < 1 || pageSize > 50) {
    throw new Error("Неверная пагинация");
  }
  const filtered = findAll(index, query).hits.filter(doc =>
    (!section || doc.section === section) && (!topic || doc.topic === topic)
  );
  const total = filtered.length;
  const totalPages = Math.ceil(total / pageSize);
  const page = Math.min(requestedPage, Math.max(1, totalPages));
  const start = (page - 1) * pageSize;
  return {
    hits: filtered.slice(start, start + pageSize),
    total, page, pageSize, totalPages
  };
}

Здесь page начинается с единицы, а позиция в массиве — с нуля. Выражение (page - 1) * pageSize соединяет эти две системы отсчёта. Для первой страницы начало равно нулю.

Параметры превращаются в числа и проверяются как положительные целые. Размер страницы дополнительно ограничен 50. Это предел учебного интерфейса; менять его можно после оценки объёма ответа и удобства.

Если после фильтрации запрошенная страница выходит за пределы, возвращается последняя допустимая. Для пустого набора сохраняется page=1, но totalPages=0. Интерфейс должен учитывать это различие и не показывать подпись «страница 1 из 0».

hits содержит только текущую часть, а total — полный размер отфильтрованной выдачи. ID документов и их постоянные URL не меняются от выбора страницы результатов.

Разбор маленькой выдачи

Для удобного наблюдения используйте запрос XML и размер страницы 1. Если учебные тексты содержат оба ожидаемых совпадения, первая страница показывает XML-урок, а вторая — материал о Fetch с упоминанием формата.

Пример вызова относится к уже созданному индексу:

const first = searchIndex(index, "XML", { page: 1, pageSize: 1 });
const second = searchIndex(index, "XML", { page: 2, pageSize: 1 });
console.log(first.total, first.hits.map(doc => doc.id));
console.log(second.total, second.hits.map(doc => doc.id));

Ожидаемый общий счётчик одинаков у обеих страниц. Карточки различаются. Если count меняется только из-за номера страницы, вероятно, в него попала длина текущего массива вместо длины полного результата.

Добавьте section: "backend" и посмотрите, исчез ли frontend-документ. После изменения фильтра интерфейс должен начинать с первой страницы. Не нужно сохранять третью страницу старой выдачи, если новый раздел содержит один результат.

Состояние формы

Форма может хранить запрос, раздел и номер страницы в URL. Эта часть появится в следующем уроке. Сейчас важно определить, какие действия сбрасывают пагинацию: новый запрос, новый раздел и новая тема.

Кнопки перехода меняют только страницу при сохранении остальных параметров. Если при нажатии «Далее» забыть раздел, человек внезапно попадёт в общую выдачу. Это выглядит как ошибка поиска, хотя причина находится в состоянии интерфейса.

Пункт «Все разделы» представляется пустой строкой. Она означает отсутствие ограничения. Не используйте строку all одновременно как служебное значение и реальную рубрику: такой договор усложняет сравнение.

Темы могут зависеть от выбранного раздела. Если посетитель сменил раздел, прежняя тема иногда перестаёт быть допустимой. Простая версия должна сбросить её или объяснить несовместимость, а не молча оставить пустой результат.

Ограничения текущего подхода

Браузерная реализация получает всех кандидатов перед выделением страницы. На небольшом корпусе это понятно и удобно, но для большого набора не всегда экономно. Серверный движок способен выполнять запрос с собственными ограничениями выдачи.

Сервер также может сообщать оценочное, а не точное общее число совпадений. При переходе нельзя назвать оценку точным счётчиком только для сохранения старой подписи. Контракт должен передавать смысл значения.

Пагинация зависит от версии индекса. Если между двумя запросами появился новый документ, порядок и состав страниц могут измениться. Устойчивый вторичный порядок уменьшает случайность, но не делает разные версии корпуса одним снимком.

Не создавайте отдельные копии статей для каждой страницы результатов. Карточка всегда ведёт к собственному URL материала. Адрес поиска описывает запрос и позицию выдачи, а не новый экземпляр документа.

Для самостоятельного разбора измените порядок операций на «страница, затем фильтр» и объясните пример, при котором нужный документ потеряется. Достаточно ручного списка из нескольких ID: он показывает ошибку без большой базы.

Теперь выдача имеет чёткие границы: общий отфильтрованный набор, его устойчивый порядок и текущая часть. Следующий урок соединит эти значения со страницей поиска и сохранит её состояние в адресной строке.