Выводить результаты поиска по страницам
Elasticsearch использует пагинацию для разбиения больших наборов результатов на управляемые страницы для эффективного извлечения и обработки.
Elasticsearch поддерживает три техники пагинации:
- Пагинация с параметрами from и size: Идеально подходит для создания списка страниц, по которым пользователи могут перемещаться.
- Search after: Поддерживает бесконечную прокрутку или позволяет загружать дополнительные результаты с помощью кнопки "следующая страница".
- Скроллинг: Исторически использовался для извлечения всех соответствующих документов. Метод search after с API точек во времени теперь рекомендуется для лучшей эффективности и надежности.
Пагинация с параметрами from и size
По умолчанию запросы возвращают 10 самых релевантных результатов. Чтобы перелистывать более большой набор результатов, можно использовать параметры API поиска from и size. Параметр from определяет количество результатов для пропуска, по умолчанию значение равно 0. Параметр size — максимальное количество результатов, которые необходимо вернуть. Вместе эти два параметра определяют страницу результатов.
resp = client.search(
from_=5,
size=20,
query={
"match": {
"user.id": "kimchy"
}
},
)
print(resp) response = client.search(
body: {
from: 5,
size: 20,
query: {
match: {
'user.id' => 'kimchy'
}
}
}
)
puts response res, err := es.Search(
es.Search.WithBody(strings.NewReader(`{
"from": 5,
"size": 20,
"query": {
"match": {
"user.id": "kimchy"
}
}
}`)),
es.Search.WithPretty(),
)
fmt.Println(res, err) const response = await client.search({
from: 5,
size: 20,
query: {
match: {
"user.id": "kimchy",
},
},
});
console.log(response); GET /_search
{
"from": 5,
"size": 20,
"query": {
"match": {
"user.id": "kimchy"
}
}
} Избегайте использования параметров from и size для слишком глубокого перелистывания или запроса слишком большого количества результатов сразу. Запросы поиска обычно охватывают несколько фрагментов. Каждый фрагмент должен загрузить запрошенные результаты и результаты любых предыдущих страниц в память. Для глубоких страниц или больших наборов результатов эти операции могут значительно увеличить использование памяти и ЦП. Если эти операции не управляются должным образом, это может привести к ухудшению производительности или сбоям узла.
По умолчанию нельзя использовать параметры from и size для перелистывания более чем 10 000 результатов. Это ограничение задано настройкой индекса index.max_result_window. Если необходимо перелистывать более 10 000 результатов, используйте параметр search_after вместо него.
Пагинация является бессостоятельной, поэтому порядок результатов поиска может меняться при переходе между страницами. Чтобы сохранить последовательный порядок, используйте API точек во времени для состоятельной пагинации.
Elasticsearch использует внутренние идентификаторы документов Lucene в качестве разделителей. Эти внутренние идентификаторы документов могут значительно отличаться на репликах одних и тех же данных. При перелистывании результатов поиска иногда может наблюдаться, что документы с одинаковыми значениями сортировки не упорядочиваются последовательно.
Search after
Вы можете использовать параметр search_after для получения следующей страницы результатов, используя набор значений сортировки с предыдущей страницы. Этот подход идеально подходит для сценариев, где пользователи нажимают кнопку "следующая страница" или "загрузить больше", а не выбирают конкретную страницу.
Использование search_after требует нескольких запросов поиска с одинаковыми значениями параметров query и sort. Первый шаг — выполнить начальный запрос. В следующем примере результаты сортируются по двум полям (date и tie_breaker_id):
resp = client.search(
index="twitter",
query={
"match": {
"title": "elasticsearch"
}
},
sort=[
{
"date": "asc"
},
{
"tie_breaker_id": "asc"
}
],
)
print(resp) response = client.search(
index: 'twitter',
body: {
query: {
match: {
title: 'elasticsearch'
}
},
sort: [
{
date: 'asc'
},
{
tie_breaker_id: 'asc'
}
]
}
)
puts response const response = await client.search({
index: "twitter",
query: {
match: {
title: "elasticsearch",
},
},
sort: [
{
date: "asc",
},
{
tie_breaker_id: "asc",
},
],
});
console.log(response); GET twitter/_search
{
"query": {
"match": {
"title": "elasticsearch"
}
},
"sort": [
{"date": "asc"},
{"tie_breaker_id": "asc"}
]
} | Копия поля |
Результат поиска содержит массив значений sort для каждого результата:
{
"took" : 17,
"timed_out" : false,
"_shards" : ...,
"hits" : {
"total" : ...,
"max_score" : null,
"hits" : [
...
{
"_index" : "twitter",
"_id" : "654322",
"_score" : null,
"_source" : ...,
"sort" : [
1463538855,
"654322"
]
},
{
"_index" : "twitter",
"_id" : "654323",
"_score" : null,
"_source" : ...,
"sort" : [
1463538857,
"654323"
]
}
]
}
} | Значения сортировки для последнего возвращенного результата. |
Чтобы получить следующую страницу результатов, повторите запрос, возьмите значения sort из последнего результата и вставьте их в массив search_after:
resp = client.search(
index="twitter",
query={
"match": {
"title": "elasticsearch"
}
},
search_after=[
1463538857,
"654323"
],
sort=[
{
"date": "asc"
},
{
"tie_breaker_id": "asc"
}
],
)
print(resp) response = client.search(
index: 'twitter',
body: {
query: {
match: {
title: 'elasticsearch'
}
},
search_after: [
1_463_538_857,
'654323'
],
sort: [
{
date: 'asc'
},
{
tie_breaker_id: 'asc'
}
]
}
)
puts response const response = await client.search({
index: "twitter",
query: {
match: {
title: "elasticsearch",
},
},
search_after: [1463538857, "654323"],
sort: [
{
date: "asc",
},
{
tie_breaker_id: "asc",
},
],
});
console.log(response); GET twitter/_search
{
"query": {
"match": {
"title": "elasticsearch"
}
},
"search_after": [1463538857, "654323"],
"sort": [
{"date": "asc"},
{"tie_breaker_id": "asc"}
]
} Повторяйте этот процесс, обновляя массив search_after каждый раз, когда получаете новую страницу результатов. Если происходит обновление поиска в реальном времени между этими запросами, порядок ваших результатов может измениться, что приведет к несогласованным результатам на разных страницах. Чтобы этого избежать, можно создать точку во времени (PIT), чтобы сохранить текущее состояние индекса во время ваших запросов.
resp = client.open_point_in_time(
index="my-index-000001",
keep_alive="1m",
)
print(resp) response = client.open_point_in_time( index: 'my-index-000001', keep_alive: '1m' ) puts response
const response = await client.openPointInTime({
index: "my-index-000001",
keep_alive: "1m",
});
console.log(response); POST /my-index-000001/_pit?keep_alive=1m
API возвращает идентификатор PIT.
{
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"_shards": ...
} Чтобы получить первую страницу результатов, отправьте запрос поиска с аргументом sort. Если используется PIT, укажите идентификатор PIT в параметре pit.id и исключите целевой поток данных или индекс из пути запроса.
Все запросы поиска PIT добавляют неявное поле разделителя сортировки, называемое _shard_doc, которое также можно указать явно. Если вы не можете использовать PIT, мы рекомендуем включить поле разделителя в ваши sort. Это поле разделителя должно содержать уникальное значение для каждого документа. Если вы не включите поле разделителя, ваши результаты пагинации могут пропустить или дублировать результаты.
Запросы search after имеют оптимизации, которые делают их быстрее, когда порядок сортировки является _shard_doc, и общее количество результатов не отслеживается. Если вы хотите перебрать все документы независимо от порядка, это самый эффективный вариант.
Если поле sort является date в некоторых целевых потоках данных или индексах, но является полем date_nanos в других, используйте параметр numeric_type для преобразования значений в одно разрешение и параметр format, чтобы указать формат даты для поля sort. В противном случае Elasticsearch не будет правильно интерпретировать параметр search after в каждом запросе.
resp = client.search(
size=10000,
query={
"match": {
"user.id": "elkbee"
}
},
pit={
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
sort=[
{
"@timestamp": {
"order": "asc",
"format": "strict_date_optional_time_nanos",
"numeric_type": "date_nanos"
}
}
],
)
print(resp) const response = await client.search({
size: 10000,
query: {
match: {
"user.id": "elkbee",
},
},
pit: {
id: "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
keep_alive: "1m",
},
sort: [
{
"@timestamp": {
order: "asc",
format: "strict_date_optional_time_nanos",
numeric_type: "date_nanos",
},
},
],
});
console.log(response); GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos", "numeric_type" : "date_nanos" }}
]
} | Идентификатор PIT для поиска. | |
| Сортирует результаты поиска с неявным разделителем на |
Результат поиска содержит массив значений sort для каждого результата. Если вы использовали PIT, разделитель включен в качестве последних значений sort для каждого результата. Этот разделитель, называемый _shard_doc, автоматически добавляется ко всем запросам поиска, использующим PIT. Значение _shard_doc представляет собой комбинацию индекса фрагмента в PIT и внутреннего идентификатора документа Lucene, оно уникально для каждого документа и постоянно в рамках PIT. Вы также можете явно добавить разделитель в запрос поиска для настройки порядка:
resp = client.search(
size=10000,
query={
"match": {
"user.id": "elkbee"
}
},
pit={
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
sort=[
{
"@timestamp": {
"order": "asc",
"format": "strict_date_optional_time_nanos"
}
},
{
"_shard_doc": "desc"
}
],
)
print(resp) const response = await client.search({
size: 10000,
query: {
match: {
"user.id": "elkbee",
},
},
pit: {
id: "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
keep_alive: "1m",
},
sort: [
{
"@timestamp": {
order: "asc",
format: "strict_date_optional_time_nanos",
},
},
{
_shard_doc: "desc",
},
],
});
console.log(response); GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos"}},
{"_shard_doc": "desc"}
]
} | Идентификатор PIT для поиска. | |
| Сортирует результаты поиска с явным разделителем на |
{
"pit_id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"took" : 17,
"timed_out" : false,
"_shards" : ...,
"hits" : {
"total" : ...,
"max_score" : null,
"hits" : [
...
{
"_index" : "my-index-000001",
"_id" : "FaslK3QBySSL_rrj9zM5",
"_score" : null,
"_source" : ...,
"sort" : [
"2021-05-20T05:30:04.832Z",
4294967298
]
}
]
}
} | Обновленное значение | |
| Значения сортировки для последнего возвращенного результата. | |
| Значение разделителя, уникальное для каждого документа в пределах |
Чтобы получить следующую страницу результатов, повторно запустите предыдущий поиск, используя значения сортировки последнего результата (включая разделитель) в качестве аргумента search_after. Если используется PIT, используйте самый последний идентификатор PIT в параметре pit.id. Аргументы запроса query и sort должны оставаться неизменными. Если указан аргумент from, он должен иметь значение 0 (по умолчанию) или -1.
resp = client.search(
size=10000,
query={
"match": {
"user.id": "elkbee"
}
},
pit={
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
sort=[
{
"@timestamp": {
"order": "asc",
"format": "strict_date_optional_time_nanos"
}
}
],
search_after=[
"2021-05-20T05:30:04.832Z",
4294967298
],
track_total_hits=False,
)
print(resp) const response = await client.search({
size: 10000,
query: {
match: {
"user.id": "elkbee",
},
},
pit: {
id: "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
keep_alive: "1m",
},
sort: [
{
"@timestamp": {
order: "asc",
format: "strict_date_optional_time_nanos",
},
},
],
search_after: ["2021-05-20T05:30:04.832Z", 4294967298],
track_total_hits: false,
});
console.log(response); GET /_search
{
"size": 10000,
"query": {
"match" : {
"user.id" : "elkbee"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
},
"sort": [
{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos"}}
],
"search_after": [
"2021-05-20T05:30:04.832Z",
4294967298
],
"track_total_hits": false
} | Идентификатор PIT, возвращённый предыдущим запросом. | |
| Значения сортировки из последнего результата предыдущего запроса. | |
| Отключить отслеживание общего количества результатов для ускорения постраничной навигации. |
Вы можете повторить этот процесс, чтобы получить дополнительные страницы результатов. Если используете PIT, вы можете продлить срок хранения PIT, используя параметр keep_alive каждого запроса.
После завершения работы необходимо удалить PIT.
resp = client.close_point_in_time(
id="46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
)
print(resp) response = client.close_point_in_time(
body: {
id: '46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=='
}
)
puts response const response = await client.closePointInTime({
id: "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
});
console.log(response); DELETE /_pit
{
"id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
} Прокрутка результатов поиска
Мы больше не рекомендуем использовать API прокрутки для глубокой постраничной навигации. Если вам необходимо сохранить состояние индекса при просмотре более 10 000 результатов, используйте параметр search_after с точкой во времени (PIT).
Хотя запрос search возвращает одну “страницу” результатов, API scroll можно использовать для получения большого количества результатов (или даже всех результатов) из одного запроса, так же, как вы бы использовали курсор в традиционной базе данных.
Прокрутка не предназначена для запросов в режиме реального времени от пользователя, а скорее для обработки больших объёмов данных, например, для повторной индексации содержимого одного потока данных или индексации в новый поток данных или индекс с другой конфигурацией.
Результаты, возвращаемые запросом на прокрутку, отражают состояние потока данных или индекса на момент выполнения первоначального запроса search, как моментальный снимок. Последующие изменения документов (индексации, обновления или удаления) повлияют только на последующие запросы.
Для использования прокрутки первоначальный запрос должен указать параметр scroll в строке запроса, который сообщает Elasticsearch, как долго он должен поддерживать “контекст поиска” (см. Поддержание контекста поиска), например, ?scroll=1m.
resp = client.search(
index="my-index-000001",
scroll="1m",
size=100,
query={
"match": {
"message": "foo"
}
},
)
print(resp) response = client.search(
index: 'my-index-000001',
scroll: '1m',
body: {
size: 100,
query: {
match: {
message: 'foo'
}
}
}
)
puts response const response = await client.search({
index: "my-index-000001",
scroll: "1m",
size: 100,
query: {
match: {
message: "foo",
},
},
});
console.log(response); POST /my-index-000001/_search?scroll=1m
{
"size": 100,
"query": {
"match": {
"message": "foo"
}
}
} Результат вышеуказанного запроса включает _scroll_id, который должен быть передан API scroll для получения следующей партии результатов.
resp = client.scroll(
scroll="1m",
scroll_id="DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
)
print(resp) res, err := es.Scroll(
es.Scroll.WithBody(strings.NewReader(`{
"scroll": "1m",
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}`)),
es.Scroll.WithPretty(),
)
fmt.Println(res, err) const response = await client.scroll({
scroll: "1m",
scroll_id: "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
});
console.log(response); POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
} | Можно использовать | |
| Параметр | |
| Параметр |
Параметр size позволяет настроить максимальное количество возвращаемых результатов в каждой порции. Каждый вызов API scroll возвращает следующую порцию результатов, пока не закончатся результаты, т.е. массив hits пуст.
Изначальный запрос и каждый последующий запрос прокрутки возвращают _scroll_id. Хотя _scroll_id может меняться между запросами, это не всегда происходит — в любом случае, следует использовать только наиболее недавно полученный _scroll_id.
Если запрос указывает агрегации, только начальный ответ поиска будет содержать результаты агрегаций.
Запросы прокрутки имеют оптимизации, которые делают их быстрее, когда порядок сортировки _doc. Если вы хотите проитерировать по всем документам независимо от порядка, это наиболее эффективный вариант:
$params = [
'body' => [
'sort' => [
'_doc',
],
],
];
$response = $client->search($params); resp = client.search(
scroll="1m",
sort=[
"_doc"
],
)
print(resp) response = client.search(
scroll: '1m',
body: {
sort: [
'_doc'
]
}
)
puts response res, err := es.Search(
es.Search.WithBody(strings.NewReader(`{
"sort": [
"_doc"
]
}`)),
es.Search.WithScroll(time.Duration(60000000000)),
es.Search.WithPretty(),
)
fmt.Println(res, err) const response = await client.search({
scroll: "1m",
sort: ["_doc"],
});
console.log(response); GET /_search?scroll=1m
{
"sort": [
"_doc"
]
} Поддержание контекста поиска
Прокрутка возвращает все документы, которые соответствовали поиску на момент первоначального запроса. Она игнорирует любые последующие изменения этих документов. scroll_id идентифицирует контекст поиска, который отслеживает всё, что Elasticsearch необходимо для возврата корректных документов. Контекст поиска создаётся первоначальным запросом и поддерживается последующими запросами.
Параметр scroll (передаваемый в запрос search и в каждый запрос scroll) сообщает Elasticsearch, как долго он должен поддерживать контекст поиска открытым. Его значение (например, 1m, см. Единицы измерения времени) не обязательно должно быть достаточно большим для обработки всех данных — оно просто должно быть достаточно большим для обработки предыдущей порции результатов. Каждый запрос scroll (с параметром scroll) устанавливает новое время истечения срока действия. Если запрос scroll не передаёт параметр scroll, то контекст поиска будет освобождён в рамках этого запроса scroll.
Обычно процесс фоновой слияния оптимизирует индекс, объединяя меньшие сегменты в новые, большие сегменты. После того, как меньшие сегменты больше не нужны, они удаляются. Этот процесс продолжается во время прокрутки, но открытый контекст поиска препятствует удалению старых сегментов, поскольку они всё ещё используются.
Поддержание старых сегментов активными означает, что требуется больше места на диске и дескрипторов файлов. Убедитесь, что вы настроили узлы с достаточным количеством свободных дескрипторов файлов. См. Дескрипторы файлов.
Кроме того, если сегмент содержит удалённые или обновлённые документы, контекст поиска должен отслеживать, был ли каждый документ активен на момент первоначального запроса. Убедитесь, что ваши узлы имеют достаточный объём кучи, если у вас много открытых прокруток в индексе, который подвергается постоянным удалениям или обновлениям.
Чтобы предотвратить проблемы, связанные с открытием слишком большого количества прокруток, пользователю не разрешается открывать прокрутки сверх определённого лимита. По умолчанию максимальное количество открытых прокруток составляет 500. Этот лимит можно обновить с помощью настройки кластера search.max_open_scroll_context.
Вы можете проверить количество открытых контекстов поиска с помощью API статистики узлов:
$params = [
'metric' => 'indices',
'index_metric' => 'search',
];
$response = $client->nodes()->stats($params); resp = client.nodes.stats(
metric="indices",
index_metric="search",
)
print(resp) response = client.nodes.stats( metric: 'indices', index_metric: 'search' ) puts response
res, err := es.Nodes.Stats(
es.Nodes.Stats.WithMetric([]string{"indices"}...),
es.Nodes.Stats.WithIndexMetric([]string{"search"}...),
)
fmt.Println(res, err) const response = await client.nodes.stats({
metric: "indices",
index_metric: "search",
});
console.log(response); GET /_nodes/stats/indices/search
Очистка прокрутки
Контексты поиска автоматически удаляются, когда истекает время ожидания scroll. Однако открытые прокрутки имеют свою стоимость, как обсуждалось в предыдущем разделе, поэтому прокрутки следует явно очищать, как только они больше не используются, используя API clear-scroll:
resp = client.clear_scroll(
scroll_id="DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
)
print(resp) response = client.clear_scroll(
body: {
scroll_id: 'DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=='
}
)
puts response res, err := es.ClearScroll(
es.ClearScroll.WithBody(strings.NewReader(`{
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}`)),
)
fmt.Println(res, err) const response = await client.clearScroll({
scroll_id: "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
});
console.log(response); DELETE /_search/scroll
{
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
} Несколько идентификаторов прокрутки можно передать как массив:
resp = client.clear_scroll(
scroll_id=[
"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB"
],
)
print(resp) response = client.clear_scroll(
body: {
scroll_id: [
'DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==',
'DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB'
]
}
)
puts response res, err := es.ClearScroll(
es.ClearScroll.WithBody(strings.NewReader(`{
"scroll_id": [
"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB"
]
}`)),
)
fmt.Println(res, err) const response = await client.clearScroll({
scroll_id: [
"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB",
],
});
console.log(response); DELETE /_search/scroll
{
"scroll_id" : [
"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB"
]
} Все контексты поиска можно очистить с помощью параметра _all:
$params = [
'scroll_id' => '_all',
];
$response = $client->clearScroll($params); resp = client.clear_scroll(
scroll_id="_all",
)
print(resp) response = client.clear_scroll( scroll_id: '_all' ) puts response
res, err := es.ClearScroll(
es.ClearScroll.WithScrollID("_all"),
)
fmt.Println(res, err) const response = await client.clearScroll({
scroll_id: "_all",
});
console.log(response); DELETE /_search/scroll/_all
Также scroll_id можно передать как параметр запроса или в теле запроса. Несколько идентификаторов прокрутки можно передать через запятую:
$params = [
'scroll_id' => 'DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==,DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB',
];
$response = $client->clearScroll($params); resp = client.clear_scroll(
scroll_id="DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==,DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB",
)
print(resp) response = client.clear_scroll( scroll_id: 'DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==,DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB' ) puts response
res, err := es.ClearScroll(
es.ClearScroll.WithScrollID("DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==", "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB"),
)
fmt.Println(res, err) const response = await client.clearScroll({
scroll_id:
"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==,DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB",
});
console.log(response); DELETE /_search/scroll/DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==,DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB
Разделенная прокрутка
При просмотре большого количества документов полезно разделить поиск на несколько фрагментов, чтобы потреблять их независимо:
resp = client.search(
index="my-index-000001",
scroll="1m",
slice={
"id": 0,
"max": 2
},
query={
"match": {
"message": "foo"
}
},
)
print(resp)
resp1 = client.search(
index="my-index-000001",
scroll="1m",
slice={
"id": 1,
"max": 2
},
query={
"match": {
"message": "foo"
}
},
)
print(resp1) response = client.search(
index: 'my-index-000001',
scroll: '1m',
body: {
slice: {
id: 0,
max: 2
},
query: {
match: {
message: 'foo'
}
}
}
)
puts response
response = client.search(
index: 'my-index-000001',
scroll: '1m',
body: {
slice: {
id: 1,
max: 2
},
query: {
match: {
message: 'foo'
}
}
}
)
puts response const response = await client.search({
index: "my-index-000001",
scroll: "1m",
slice: {
id: 0,
max: 2,
},
query: {
match: {
message: "foo",
},
},
});
console.log(response);
const response1 = await client.search({
index: "my-index-000001",
scroll: "1m",
slice: {
id: 1,
max: 2,
},
query: {
match: {
message: "foo",
},
},
});
console.log(response1); GET /my-index-000001/_search?scroll=1m
{
"slice": {
"id": 0,
"max": 2
},
"query": {
"match": {
"message": "foo"
}
}
}
GET /my-index-000001/_search?scroll=1m
{
"slice": {
"id": 1,
"max": 2
},
"query": {
"match": {
"message": "foo"
}
}
} | Идентификатор фрагмента | |
| Максимальное количество фрагментов |
Результат первого запроса возвращает документы, относящиеся к первому фрагменту (id: 0), а результат второго запроса возвращает документы, относящиеся ко второму фрагменту. Поскольку максимальное количество фрагментов установлено в 2, объединение результатов двух запросов эквивалентно результатам запроса прокрутки без разделения. По умолчанию разделение выполняется сначала по фрагментам, а затем локально в каждом фрагменте, используя поле _id. Локальное разделение следует формуле slice(doc) = floorMod(hashCode(doc._id), max)).
Каждая прокрутка независима и может быть обработана параллельно, как любой запрос прокрутки.
Если количество фрагментов больше, чем количество фрагментов, фильтр фрагментов очень медленный при первых вызовах, он имеет сложность O(N) и стоимость памяти равна N битам на фрагмент, где N — общее количество документов в фрагменте. После нескольких вызовов фильтр должен быть кэширован, и последующие вызовы должны быть быстрее, но вы должны ограничить количество выполняемых параллельно запросов с фрагментацией, чтобы избежать взрыва памяти.
API точке во времени поддерживает более эффективную стратегию разбиения и не страдает от этой проблемы. Если это возможно, рекомендуется использовать поиск в точку во времени с фрагментацией вместо прокрутки.
Другой способ избежать этих высоких затрат — использовать doc_values другого поля для фрагментации. Поле должно обладать следующими свойствами:
- Поле является числовым.
-
doc_valuesвключены для этого поля. - Каждый документ должен содержать единственное значение. Если документ содержит несколько значений для указанного поля, используется первое значение.
- Значение для каждого документа должно быть установлено один раз при создании документа и не должно обновляться. Это гарантирует, что каждый фрагмент получает детерминированные результаты.
- Мощность поля должна быть высокой. Это гарантирует, что каждый фрагмент получает приблизительно одинаковое количество документов.
resp = client.search(
index="my-index-000001",
scroll="1m",
slice={
"field": "@timestamp",
"id": 0,
"max": 10
},
query={
"match": {
"message": "foo"
}
},
)
print(resp) response = client.search(
index: 'my-index-000001',
scroll: '1m',
body: {
slice: {
field: '@timestamp',
id: 0,
max: 10
},
query: {
match: {
message: 'foo'
}
}
}
)
puts response const response = await client.search({
index: "my-index-000001",
scroll: "1m",
slice: {
field: "@timestamp",
id: 0,
max: 10,
},
query: {
match: {
message: "foo",
},
},
});
console.log(response); GET /my-index-000001/_search?scroll=1m
{
"slice": {
"field": "@timestamp",
"id": 0,
"max": 10
},
"query": {
"match": {
"message": "foo"
}
}
} Для индексов с добавлением только по времени поле timestamp можно безопасно использовать.
© 2023-2025 Elasticsearch
As of September 2024, Elasticsearch is available under a choice of three licenses: the Server Side Public License (SSPL), the Elastic License, or the AGPLv3 (OSI approved).
Elasticsearch and the Elasticsearch logo are trademarks of Elasticsearch B.V., registered in the U.S. and in other countries.
https://www.elastic.co/guide/en/elasticsearch/reference/8.17/paginate-search-results.html