分页搜索结果编辑

默认情况下,搜索会返回前 10 个匹配的命中结果。要浏览更大的结果集,可以使用 搜索 APIfromsize 参数。 from 参数定义要跳过的命中结果数量,默认为 0size 参数是要返回的命中结果的最大数量。这两个参数一起定义了一页结果。

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)
GET /_search
{
  "from": 5,
  "size": 20,
  "query": {
    "match": {
      "user.id": "kimchy"
    }
  }
}

避免使用 fromsize 对过深的结果集进行分页或一次请求过多的结果。搜索请求通常跨越多个分片。每个分片必须将请求的命中结果以及任何先前页面的命中结果加载到内存中。对于深层页面或大型结果集,这些操作会显着增加内存和 CPU 使用量,从而导致性能下降或节点故障。

默认情况下,您不能使用 fromsize 对超过 10,000 个命中结果进行分页。此限制是 index.max_result_window 索引设置设置的安全措施。如果您需要对超过 10,000 个命中结果进行分页,请改用 search_after 参数。

Elasticsearch 使用 Lucene 的内部文档 ID 作为 tie-breaker。这些内部文档 ID 在同一数据的副本之间可能完全不同。在对搜索命中结果进行分页时,您可能会偶尔看到具有相同排序值的文档排序不一致。

搜索后编辑

您可以使用 search_after 参数使用上一页的一组 排序值 检索下一页命中结果。

使用 search_after 需要使用相同的 querysort 值执行多个搜索请求。第一步是运行初始请求。以下示例按两个字段 (datetie_breaker_id) 对结果进行排序

response = client.search(
  index: 'twitter',
  body: {
    query: {
      match: {
        title: 'elasticsearch'
      }
    },
    sort: [
      {
        date: 'asc'
      },
      {
        tie_breaker_id: 'asc'
      }
    ]
  }
)
puts response
GET twitter/_search
{
    "query": {
        "match": {
            "title": "elasticsearch"
        }
    },
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}      
    ]
}

启用了 doc_values_id 字段的副本

搜索响应包含每个命中结果的 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 数组中

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
GET twitter/_search
{
    "query": {
        "match": {
            "title": "elasticsearch"
        }
    },
    "search_after": [1463538857, "654323"],
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}
    ]
}

通过在每次检索新结果页时更新 search_after 数组来重复此过程。如果在这些请求之间发生 刷新,则结果的顺序可能会发生变化,导致页面间结果不一致。为了防止这种情况,您可以创建一个 时间点 (PIT) 以在您的搜索中保留当前索引状态。

response = client.open_point_in_time(
  index: 'my-index-000001',
  keep_alive: '1m'
)
puts response
POST /my-index-000001/_pit?keep_alive=1m

API 返回一个 PIT ID。

{
  "id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}

要获取第一页结果,请提交一个带有 sort 参数的搜索请求。如果使用 PIT,请在 pit.id 参数中指定 PIT ID,并从请求路径中省略目标数据流或索引。

所有 PIT 搜索请求都会添加一个名为 _shard_doc 的隐式排序 tiebreaker 字段,该字段也可以显式提供。如果您不能使用 PIT,我们建议您在 sort 中包含一个 tiebreaker 字段。此 tiebreaker 字段应包含每个文档的唯一值。如果您不包含 tiebreaker 字段,则您的分页结果可能会丢失或重复命中结果。

搜索后请求具有优化功能,当排序顺序为 _shard_doc 且未跟踪总命中结果时,这些优化功能可以使搜索后请求更快。如果您想按任何顺序迭代所有文档,这是最有效的方法。

如果 sort 字段在某些目标数据流或索引中是 date,但在其他目标中是 date_nanos 字段,请使用 numeric_type 参数将值转换为单个分辨率,并使用 format 参数为 sort 字段指定 日期格式。否则,Elasticsearch 不会在每个请求中正确解释搜索后参数。

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 ID。

_shard_doc 升序对搜索命中结果进行排序。

搜索响应包含每个命中结果的 sort 值数组。如果您使用了 PIT,则 tiebreaker 将作为每个命中结果的最后一个 sort 值包含在内。此名为 _shard_doc 的 tiebreaker 会在使用 PIT 的每个搜索请求中自动添加。 _shard_doc 值是 PIT 中分片索引与 Lucene 的内部文档 ID 的组合,它在每个文档中都是唯一的,并且在 PIT 内是恒定的。您也可以在搜索请求中显式添加 tiebreaker 以自定义顺序

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 ID。

_shard_doc 降序对搜索命中结果进行排序。

{
  "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                              
        ]
      }
    ]
  }
}

时间点的更新 id

最后一个返回的命中结果的排序值。

tiebreaker 值,在 pit_id 内每个文档都是唯一的。

要获取下一页结果,请使用最后一个命中结果的排序值(包括 tiebreaker)作为 search_after 参数重新运行之前的搜索。如果使用 PIT,请在 pit.id 参数中使用最新的 PIT ID。搜索的 querysort 参数必须保持不变。如果提供,则 from 参数必须为 0(默认)或 -1

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 ID。

来自之前搜索的最后一个命中结果的排序值。

禁用总命中结果的跟踪以加快分页速度。

您可以重复此过程以获取更多结果页。如果使用 PIT,您可以使用每个搜索请求的 keep_alive 参数来延长 PIT 的保留期。

完成后,您应该删除您的 PIT。

response = client.close_point_in_time(
  body: {
    id: '46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=='
  }
)
puts response
DELETE /_pit
{
    "id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}

滚动搜索结果编辑

我们不再建议使用滚动 API 进行深度分页。如果您需要在对超过 10,000 个命中结果进行分页时保留索引状态,请使用带有时间点 (PIT) 的 search_after 参数。

虽然 search 请求返回单个“页面”的结果,但 scroll API 可用于从单个搜索请求中检索大量结果(甚至所有结果),这与您在传统数据库上使用游标的方式非常相似。

滚动不适用于实时用户请求,而是用于处理大量数据,例如为了将一个数据流或索引的内容重新索引到具有不同配置的新数据流或索引中。

从滚动请求返回的结果反映了在发出初始 search 请求时数据流或索引的状态,就像时间快照一样。对文档的后续更改(索引、更新或删除)只会影响以后的搜索请求。

为了使用滚动,初始搜索请求应该在查询字符串中指定 scroll 参数,该参数告诉 Elasticsearch 它应该保持“搜索上下文”存活多长时间(参见 保持搜索上下文存活),例如 ?scroll=1m

response = client.search(
  index: 'my-index-000001',
  scroll: '1m',
  body: {
    size: 100,
    query: {
      match: {
        message: 'foo'
      }
    }
  }
)
puts response
POST /my-index-000001/_search?scroll=1m
{
  "size": 100,
  "query": {
    "match": {
      "message": "foo"
    }
  }
}

上述请求的结果包含一个 _scroll_id,该 _scroll_id 应该传递给 scroll API 以检索下一批结果。

res, err := es.Scroll(
	es.Scroll.WithBody(strings.NewReader(`{
	  "scroll": "1m",
	  "scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
	}`)),
	es.Scroll.WithPretty(),
)
fmt.Println(res, err)
POST /_search/scroll                                                               
{
  "scroll" : "1m",                                                                 
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}

GETPOST 可以使用,并且 URL 不应包含 index 名称——这在原始 search 请求中指定。

scroll 参数告诉 Elasticsearch 将搜索上下文保持打开状态另外 1m

scroll_id 参数

size 参数允许您配置每批结果要返回的最大命中结果数量。每次调用 scroll API 都会返回下一批结果,直到没有更多结果可以返回,即 hits 数组为空。

初始搜索请求和每个后续滚动请求都会返回一个 _scroll_id。虽然 _scroll_id 可能会在请求之间发生变化,但它并不总是变化——无论如何,只应使用最近收到的 _scroll_id

如果请求指定了聚合,则只有初始搜索响应将包含聚合结果。

滚动请求具有优化功能,当排序顺序为 _doc 时,这些优化功能可以使滚动请求更快。如果您想按任何顺序迭代所有文档,这是最有效的方法

$params = [
    'body' => [
        'sort' => [
            '_doc',
        ],
    ],
];
$response = $client->search($params);
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',
  body: {
    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);
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 超时时,搜索上下文会自动删除。但是,正如 上一节 中所讨论的,保持滚动打开会产生成本,因此应在滚动不再使用时使用 clear-scroll API 显式清除滚动。

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)
DELETE /_search/scroll
{
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

可以将多个滚动 ID 作为数组传递。

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)
DELETE /_search/scroll
{
  "scroll_id" : [
    "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
    "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB"
  ]
}

可以使用 _all 参数清除所有搜索上下文。

$params = [
    'scroll_id' => '_all',
];
$response = $client->clearScroll($params);
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 也可以作为查询字符串参数或在请求主体中传递。可以将多个滚动 ID 作为逗号分隔的值传递。

$params = [
    'scroll_id' => 'DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==,DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZrUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB',
];
$response = $client->clearScroll($params);
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

切片滚动编辑

当分页浏览大量文档时,将搜索拆分为多个切片以独立使用它们可能会有所帮助。

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
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。

切片的最大数量。

第一个请求的结果返回属于第一个切片(ID:0)的文档,第二个请求的结果返回属于第二个切片的文档。由于切片的最大数量设置为 2,因此两个请求结果的并集等效于没有切片的滚动查询的结果。默认情况下,拆分首先在分片上进行,然后使用 _id 字段在每个分片上本地进行。本地拆分遵循公式 slice(doc) = floorMod(hashCode(doc._id), max))

每个滚动都是独立的,可以像任何滚动请求一样并行处理。

如果切片的数量大于分片的数量,则切片过滤器在第一次调用时非常慢,它的复杂度为 O(N),内存成本等于每个切片的 N 位,其中 N 是分片中文档的总数。在几次调用之后,过滤器应该被缓存,后续调用应该更快,但您应该限制并行执行的切片查询的数量,以避免内存爆炸。

时间点 API 支持更有效的划分策略,并且不会受到此问题的困扰。如果可能,建议使用带有切片的时间点搜索而不是滚动。

避免这种高成本的另一种方法是使用另一个字段的 doc_values 来进行切片。该字段必须具有以下属性

  • 该字段是数字。
  • doc_values 在该字段上启用。
  • 每个文档都应该包含一个值。如果文档在指定字段中具有多个值,则使用第一个值。
  • 每个文档的值应在创建文档时设置一次,并且永远不会更新。这确保每个切片都获得确定性的结果。
  • 该字段的基数应该很高。这确保每个切片都获得大约相同数量的文档。
response = client.search(
  index: 'my-index-000001',
  scroll: '1m',
  body: {
    slice: {
      field: '@timestamp',
      id: 0,
      max: 10
    },
    query: {
      match: {
        message: 'foo'
      }
    }
  }
)
puts response
GET /my-index-000001/_search?scroll=1m
{
  "slice": {
    "field": "@timestamp",
    "id": 0,
    "max": 10
  },
  "query": {
    "match": {
      "message": "foo"
    }
  }
}

对于仅追加的基于时间的索引,可以使用 timestamp 字段。