Пагинация в ElasticSearch
Один из наших клиентов в своей системе поиска тендеров использует пагинацию. После того, как пользователь выполнил поиск в веб-интерфейсе и отобразились страницы с постраничными результатами, они заранее загружают следующую страницу. То есть, при нахождении на первой странице с результатами, при переходе на вторую страницу, она отображается мгновенно. Когда пользователь загружает вторую страницу, сразу же подгружается третья и так далее. Такой подход весьма улучшает UX. Осталось выбрать правильный тип пагинации. В этом посте рассмотрим все имеющиеся три вида пагинации (pagination, search-after и scroll) и определимся с предназначением каждого типа.
Это механизм, используемый по умолчанию, для получения большого количества документов в результате поиска в ElasticSearch. После отправки поискового запроса, по умолчанию, вернутся первые, наиболее релевантные, 10 документов. Для показа следующей страницы (10 хитов) нужно будет изменить параметр «from» в следующем запросе на 10 и так далее.
# Первый запрос
GET catalog/_search
{
"query": {
"match": {
"name": "колбаса"
}
},
"size": 10,
"from": 0
}
# Следующий запрос
GET catalog/_search
{
"query": {
"match": {
"name": "колбаса"
}
},
"size": 10,
"from": 10
}
Верхняя граница по количеству результатов — 10000. Тут ваша рука может непроизвольно потянуться изменить параметр index.max_result_window на нечто большее этого значения, но не торопитесь этого делать. Поисковый запрос делится на две фазы: запрос и ответ. Ответ будет храниться в памяти координирующей ноды. Если результатов будет много, представьте себе сколько памяти будет под это резервироваться, а если у вас еще нет выделенной координирующей ноды… Глубокая пагинация — главная убийца производительности кластера. Не стоит предоставлять пользователям доступ ко всем результатам поискового запроса. А если на вас будут давить — скажите, что даже Гугл даёт только 50 страниц с результатами. Должно помочь.
Так что же делать? Правильный ответ — использовать фильтры.
Если не нужен свободный доступ к страницам (например, переход с первой страницы на пятую и т.д.) и кнопка «Далее» — это ок (или когда используеся бесконечная прокрутка), то параметр search_after может даже и подойдет вам.
# Первый запрос
GET catalog/_search
{
"query": {
"match": {
"name": "колбаса"
}
},
"size": 10,
"sort": [
{
"_score": "desc"
},
{
"id.keyword": "asc"
}
]
}
# Следующий запрос
GET catalog/_search
{
"query": {
"match": {
"name": "колбаса"
}
},
"size": 10,
"sort": [
{
"_score": "desc"
},
{
"id.keyword": "asc"
}
],
"search_after": [
0.1853153,
"1"
]
}
С помощью search_after можно указать ElasticSearch последнее просмотренное совпадение, чтобы все предыдущие совпадения можно было игнорировать. Вместо того чтобы хранить в памяти весь список результатов поискового запроса и сортировать его, чтобы выдать нужную страницу результатов, search_after будет использовать закладку из последних результатов предыдущего поискового запроса. Это работает эффективнее pagination, в случае когда нужно показать много результатов. При необходимости, можно использовать search_after для показа более 10000 совпадений и ничего вам за это не будет.
Иногда случается, что документы обновляются после отображения поисковой выдачи и, после перехода между страницами с результатами, порядок документов изменится. Пользователю это может не понравиться. Для решения такой проблемы в ElasticSearch предусмотрен Point in Time API (PIT) в качестве расширения возможностей поиска (для pagination и search_after). Ниже пример для search_after.
# Создание PIT для индекса
POST catalog/_pit?keep_alive=5m
# Первая страница
GET catalog/_search
{
"size": 10,
"query": {
"match": {
"name": "колбаса"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive" : "5m"
},
"sort": [
{
"_score": "desc"
},
{
"_shard_doc": "asc"
}
]
}
# Следующая страница
GET catalog/_search
{
"size": 10,
"query": {
"match": {
"name": "колбаса"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive" : "5m"
},
"sort": [
{
"_score": "desc"
},
{
"_shard_doc": "asc"
}
],
"search_after": [
0.1853153,
0
]
}
Scroll можно использовать для итерационного просмотра большого количества документов, соответствующих запросу или всех документов, соответствующих запросу. В последних версиях документации Elastic не рекомендует использовать этот API, вместо этого советуют search_after + PIT. Причину не объясняют, видимо, scroll недостаточно оптимален по производительности.
В отличие от pagination и search-after (без PIT, конечно), scroll игнорирует обновления индекса. Чтобы достичь этого, хранится и поддерживается моментальный снимок поисковой выдачи в течение заданного времени. Сохранение такого объёма данных имеет высокую стоимость по производительности для активно обновляемых индексов.
# Первый запрос
GET catalog/_search?scroll=5m
{
"size": 10,
"query": {
"match": {
"name": "колбаса"
}
}
}
# Следующий запрос
GET catalog/_search/scroll
{
"scroll": "5m",
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
Заключение
Когда использовать pagination?
Когда нужен доступ к постраничной выдаче и не планируется глубокая пагинация.
Когда использовать search-after?
Когда вам подходит кнопка «далее» и вы хотите предоставить эффективный доступ ко многим страницам.
Когда использовать Point in Time (PIT)?
Когда вам нужен неизменный порядок на страницах результатов поиска.
Когда использовать прокрутку?
Когда использовать Scroll?
Scroll можно использовать когда версия ElasticSearch не поддерживает Point in Time.
Хорошего вам поиска!