Другое

Исправление пагинации MongoDB $search: searchBefore возвращает одинаковые результаты

Решение проблемы пагинации MongoDB $search, когда searchBefore и searchAfter возвращают идентичные результаты. Узнайте о правильном использовании токенов, настройке сортировки и лучших практиках для пагинации Atlas Search.

Проблема с пагинацией в MongoDB $search: searchBefore и searchAfter возвращают идентичные результаты

Я столкнулся с неожиданным поведением функциональности пагинации MongoDB $search. При использовании следующего агрегационного запроса:

json
[
  {
    $search: {
      index: "search-index",
      text: { query: "20", path: "name" },
      sort: { score: { order: -1 }, _id: 1 },
      searchBefore: "<paginationToken>"
    }
  },
  {
    $project: {      
      name: "name",
      paginationToken: {$meta: "searchSequenceToken"},
      score: { $meta: "searchScore" }
    }
  }
]

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

Я использую правильные токены:

  • Токен первого результата для searchBefore
  • Токен последнего результата для searchAfter

Проблема также воспроизводится простым заменой searchAfter на searchBefore при намерении перейти к следующей странице.

Что может вызывать такое поведение, и что я, возможно, делаю неправильно в своей реализации?

Проблема, с которой вы сталкиваетесь при пагинации MongoDB $search, когда searchBefore и searchAfter возвращают идентичные результаты, обычно вызвана неправильной обработкой токенов или непониманием того, как работают токены пагинации. searchSequenceToken, генерируемый MongoDB, имеет направление - токены с конца набора результатов следует использовать с searchAfter для навигации вперед, а токены с начала - с searchBefore для навигации назад. Чаще всего это происходит, когда пользователи пытаются использовать один и тот же токен для обоих направлений или когда конфигурация сортировки создает неоднозначность в упорядочивании.

Содержание

Основы пагинации поиска MongoDB

Пагинация в MongoDB Atlas Search работает иначе, чем традиционная пагинация MongoDB. Вместо использования skip() и limit(), которые могут быть неэффективны при работе с большими наборами данных, Atlas Search использует пагинацию по набору ключей (keyset pagination) с опциями searchAfter и searchBefore.

Основные компоненты:

  • searchSequenceToken: Уникальный токен, генерируемый для каждого документа, который представляет его позицию в отсортированном наборе результатов
  • searchAfter: Используется для получения результатов после определенного токена (следующая страница)
  • searchBefore: Используется для получения результатов перед определенным токеном (предыдущая страница)

Как объясняется в документации MongoDB, “Чтобы искать перед контрольной точкой, вы должны указать контрольную точку в вашем запросе $search с помощью опции searchBefore и токена, сгенерированного searchSequenceToken.”

Основные причины проблемы

На основе результатов исследований и обсуждений сообщества, вот наиболее распространенные причины, по которым searchBefore и searchAfter возвращают идентичные результаты:

1. Неправильное использование направления токенов

Наиболее частая ошибка - использование токенов в неправильном направлении. Когда вы получаете страницу результатов:

  • Для searchAfter (следующая страница): Используйте последний токен с вашей текущей страницы
  • Для searchBefore (предыдущая страница): Используйте первый токен с вашей текущей страницы

Если вы используете один и тот же токен (например, первый токен) как для searchAfter, так и для searchBefore, вы получите пересекающиеся или идентичные результаты.

2. Неоднозначная конфигурация сортировки

Ваша текущая конфигурация сортировки:

json
sort: { score: { order: -1 }, _id: 1 }

Это может вызвать проблемы, потому что:

  • Несколько документов могут иметь одинаковые оценки
  • Когда оценки одинаковы, MongoDB переходит к сортировке по _id: 1
  • Это создает неуникальное упорядочивание, делая токены пагинации неоднозначными

Как отмечается в обсуждениях Stack Overflow, “у всего этого одинаковый ‘textScore’, но это порядок, в котором MongoDB вернет эти документы.”

3. Отсутствующая или неправильная конфигурация отслеживания

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

json
{
  $search: {
    index: "search-index",
    text: { query: "20", path: "name" },
    sort: { score: { order: -1 }, _id: 1 },
    tracking: {
      searchSequenceToken: {}
    }
  }
}

Правильное использование токенов и реализация

Правильный поток пагинации

Вот как правильно реализовать пагинацию:

Первый запрос (без пагинации)

json
[
  {
    $search: {
      index: "search-index",
      text: { query: "20", path: "name" },
      sort: { score: { order: -1 }, _id: 1 },
      tracking: {
        searchSequenceToken: {}
      }
    }
  },
  {
    $project: {      
      name: 1,
      paginationToken: {$meta: "searchSequenceToken"},
      score: { $meta: "searchScore" }
    }
  },
  {
    $limit: 10
  }
]

Запрос следующей страницы

Используйте последний токен с предыдущей страницы:

json
[
  {
    $search: {
      index: "search-index",
      text: { query: "20", path: "name" },
      sort: { score: { order: -1 }, _id: 1 },
      searchAfter: "<последний_токен_с_предыдущей_страницы>",
      tracking: {
        searchSequenceToken: {}
      }
    }
  },
  {
    $project: {      
      name: 1,
      paginationToken: {$meta: "searchSequenceToken"},
      score: { $meta: "searchScore" }
    }
  },
  {
    $limit: 10
  }
]

Запрос предыдущей страницы

Используйте первый токен с текущей страницы:

json
[
  {
    $search: {
      index: "search-index",
      text: { query: "20", path: "name" },
      sort: { score: { order: -1 }, _id: 1 },
      searchBefore: "<первый_токен_с_текущей_страницы>",
      tracking: {
        searchSequenceToken: {}
      }
    }
  },
  {
    $project: {      
      name: 1,
      paginationToken: {$meta: "searchSequenceToken"},
      score: { $meta: "searchScore" }
    }
  },
  {
    $limit: 10
  }
]

Улучшенная сортировка для лучшей пагинации

Чтобы избежать неоднозначности в сортировке, рассмотрите возможность использования более конкретных критериев сортировки:

json
sort: { 
  score: { order: -1 }, 
  _id: 1,
  name: 1,
  createdAt: -1
}

Это создает более детерминированное упорядочивание, которое снижает вероятность связей.

Шаги по устранению неполадок

Шаг 1: Проверьте извлечение токенов

Убедитесь, что вы извлекаете правильные токены:

  • Для searchAfter: Используйте токен последнего документа в вашем текущем наборе результатов
  • Для searchBefore: Используйте токен первого документа в вашем текущем наборе результатов

Шаг 2: Проверьте последовательность сортировки

Добавьте отладку для проверки, что ваша сортировка работает как ожидается:

json
[
  {
    $search: {
      index: "search-index",
      text: { query: "20", path: "name" },
      sort: { score: { order: -1 }, _id: 1 },
      tracking: {
        searchSequenceToken: {}
      }
    }
  },
  {
    $project: {      
      name: 1,
      paginationToken: {$meta: "searchSequenceToken"},
      score: { $meta: "searchScore" },
      debugSort: { $concat: [ { $toString: "$score" }, "_", { $toString: "$_id" } ] }
    }
  },
  {
    $limit: 10
  }
]

Шаг 3: Протестируйте с детерминированной сортировкой

Временно используйте более детерминированную сортировку для изоляции проблемы:

json
sort: { 
  score: { order: -1 }, 
  name: 1,
  _id: 1
}

Шаг 4: Проверьте формат токена

Убедитесь, что токены правильно хранятся и извлекаются. Токены представляют собой строки в кодировке base64 и должны обрабатываться как таковые в коде вашего приложения.

Лучшие практики для пагинации поиска

1. Всегда включайте отслеживание

Всегда включайте конфигурацию tracking с searchSequenceToken:

json
tracking: {
  searchSequenceToken: {}
}

2. Используйте детерминированную сортировку

Проектируйте свои критерии сортировки для минимизации связей:

json
sort: {
  score: { order: -1 },
  relevanceField: -1,
  _id: 1,
  timestamp: -1
}

3. Обрабатывайте крайние случаи

  • Корректно обрабатывайте пустые наборы результатов
  • Кэшируйте токены для избежания проблем согласованности
  • Реализуйте правильную обработку ошибок для недействительных токенов

4. Учитывайте производительность

Для больших наборов данных имейте в виду, что “пагинация по набору ключей работает значительно хуже, чем skip-limit” в определенных сценариях, как отмечено в результатах исследований.

Продвинутые техники пагинации

Двунаправленная пагинация с отслеживанием курсора

Для более надежной реализации отслеживайте как первый, так и последний токены каждой страницы:

javascript
// В вашей логике приложения
const currentPage = {
  items: results,
  firstToken: results[0]?.paginationToken,
  lastToken: results[results.length - 1]?.paginationToken,
  hasNext: results.length === limit,
  hasPrevious: currentPageNumber > 1
};

Гибридный подход к пагинации

Для очень больших наборов данных рассмотрите возможность объединения пагинации поиска с традиционной пагинацией MongoDB:

json
[
  {
    $search: {
      index: "search-index",
      text: { query: "20", path: "name" },
      sort: { score: { order: -1 }, _id: 1 },
      searchAfter: "<токен>",
      tracking: {
        searchSequenceToken: {}
      }
    }
  },
  {
    $skip: 0
  },
  {
    $limit: 20
  }
]

Заключение

Проблема с пагинацией MongoDB $search, когда searchBefore и searchAfter возвращают идентичные результаты, обычно возникает из-за неправильного использования токенов или неоднозначных конфигураций сортировки. Следуя этим ключевым практикам, вы можете решить проблему:

  1. Используйте правильные токены: Последний токен для searchAfter, первый токен для searchBefore
  2. Реализуйте детерминированную сортировку: Добавьте несколько критериев сортировки для минимизации связей
  3. Включайте конфигурацию отслеживания: Всегда используйте tracking: { searchSequenceToken: {} }
  4. Проверяйте вашу реализацию: Тестируйте с упрощенной сортировкой сначала, затем постепенно улучшайте

Наиболее частая ошибка - попытка использовать один и тот же токен пагинации для навигации как вперед, так и назад. Помните, что пагинация по набору ключей в MongoDB relies на упорядочивание, установленное вашей конфигурацией сортировки, и токены по своей природе имеют направление.

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

Источники

  1. Пагинация результатов - Документация MongoDB Atlas
  2. Atlas search - searchBefore возвращает те же документы, что и searchAfter - Форумы сообщества MongoDB
  3. Как пагинировать результаты запроса - Учебное пособие MongoDB Atlas
  4. Как реализовать searchAfter и searchBefore в MongoDb? - Stack Overflow
  5. Документация этапа $search поиска MongoDB
  6. Пагинация в MongoDB: правильный способ VS распространенные ошибки - Форумы сообщества MongoDB
Авторы
Проверено модерацией
Модерация
Исправление пагинации MongoDB $search: searchBefore возвращает одинаковые результаты