ES允许基于字段值折叠搜索结果,ES仅对排序后文档的顶部文档执行成折叠操作,比如从每个推特用户获取它们最好的推文并通过其他用户的点赞数进行排序(升序):
// 创建索引,这里一定要将user字段的类型设置为keyword或numeric
curl -X PUT "localhost:9200/twitter" -H 'Content-Type: application/json' -d'
{
"mappings": {
"_doc": {
"properties": {
"user": {"type": "keyword"}
}
}
}
}
'
// 模拟数据4个,分别是1-10,2-9,3-8,4-12(_id-likes)
curl -X POST "localhost:9200/twitter/_doc/1" -H 'Content-Type: application/json' -d'
{
"user": "kimchy1",
"likes": 10,
// 使用postman自带的时间戳变量进行赋值
"post_date": {{$timestamp}},
"message" : "trying out Elasticsearch"
}
'
// 字段折叠搜索
curl -X GET "localhost:9200/twitter/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"message": "elasticsearch"
}
},
// 使用user字段折叠结果,注意这个字段的类型为keyword
"collapse" : {
"field" : "user"
},
// 选出点赞数的顶部文档
"sort": ["likes"],
// 定义第一个折叠结果的偏移量
"from": 1
}
'
响应中的总命中数表示没有折叠的匹配文档的数量,不同组的总数是未知的。用于折叠的字段必须是激活doc_values
的keyword
或numeric
字段(这点一定要注意,我在测试的时候就是在这里遭坑了)。最终的结果为:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 4,
"max_score": null,
// 命中4个结果,但偏移量为1,显示后面3个,即第一个命中的likes为8的缺省了
"hits": [
{
"_index": "twitter",
"_type": "_doc",
"_id": "2",
"_score": null,
"_source": {
"user": "kimchy2",
"likes": 9,
"post_date": 1542197883,
"message": "trying out Elasticsearch"
},
// 将命中的结果折叠为user字段
"fields": {
"user": [
"kimchy2"
]
},
"sort": [
9
]
},
{
"_index": "twitter",
"_type": "_doc",
"_id": "1",
"_score": null,
"_source": {
"user": "kimchy1",
"likes": 10,
"post_date": 1542197868,
"message": "trying out Elasticsearch"
},
"fields": {
"user": [
"kimchy1"
]
},
"sort": [
10
]
},
{
"_index": "twitter",
"_type": "_doc",
"_id": "4",
"_score": null,
"_source": {
"user": "kimchy4",
"likes": 12,
"post_date": 1542197912,
"message": "trying out Elasticsearch"
},
"fields": {
"user": [
"kimchy4"
]
},
"sort": [
12
]
}
]
}
}
折叠仅适用于顶部匹配,不会影响聚合。
展开折叠的结果
上述是折叠命中的结果,同时也可以使用inner_hits
参数展开每个折叠的顶部匹配,比如:
curl -X GET "localhost:9200/twitter/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"message": "elasticsearch"
}
},
"collapse": {
// 使用user字段折叠结果集
"field": "user",
"inner_hits": {
// 用于响应中内部命中部分的名称
"name": "kimchy4",
// 每个折叠检索的inner_hits数量
"size": 1,
// 指定每组文档的排序方式
"sort": [{"post_date": "asc"}]
},
// 指定允许每组检索 inner_hits 的并发请求数量
"max_concurrent_group_searches": 2
},
"sort": ["likes"]
}
'
size
参数含义未知(待处理)
ES也支持在每个折叠命中请求多个内部命中inner_hits
,这个功能可以在想要获取折叠命中中多个表示时使用:
curl -X GET "localhost:9200/twitter/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"message": "elasticsearch"
}
},
"collapse" : {
// 使用user字段折叠命中结果
"field" : "user",
"inner_hits": [
// 返回3个点赞数最多的推文
{
"name": "most_liked",
"size": 3,
"sort": ["likes"]
},
// 返回3个最近的推文
{
"name": "most_recent",
"size": 3,
"sort": [{ "date": "asc" }]
}
]
},
"sort": ["likes"]
}
'
通过为响应中返回的每个折叠命中的每个inner_hit
请求发送附加查询来完成组的扩展,如果有太多组的逻辑与或inner_hit
请求,这可能会导致速度明显降低。max_concurrent_group_searches
请求参数可以用来控制在这个阶段允许的最大并发搜索请求数量,允许最大并发请求数默认是根据数据节点和默认搜索线程池的大小决定的。
二级折叠
字段折叠也支持二级折叠、应用于内部命中inner_hits
。下面的栗子是一个查找每个国家中最高分的推文,每个国家为每个用户查找最高分的推文:
GET /twitter/_search
{
"query": {
"match": {
"message": "elasticsearch"
}
},
"collapse" : {
"field" : "country",
"inner_hits" : {
"name": "by_location",
"collapse" : {"field" : "user"},
// 参数含义未知
"size": 3
}
}
}
// 结果
{
...
"hits": [
{
"_index": "twitter",
"_type": "_doc",
"_id": "9",
"_score": ...,
"_source": {...},
"fields": {"country": ["UK"]},
"inner_hits":{
"by_location": {
"hits": {
...,
"hits": [
{
...
"fields": {"user" : ["user124"]}
},
{
...
"fields": {"user" : ["user589"]}
},
{
...
"fields": {"user" : ["user001"]}
}
]
}
}
}
},
{
"_index": "twitter",
"_type": "_doc",
"_id": "1",
"_score": ..,
"_source": {...},
"fields": {"country": ["Canada"]},
"inner_hits":{
"by_location": {
"hits": {
...,
"hits": [
{
...
"fields": {"user" : ["user444"]}
},
{
...
"fields": {"user" : ["user1111"]}
},
{
...
"fields": {"user" : ["user999"]}
}
]
}
}
}
},
....
]
}
搜索可以通过使用from
和to
参数进行分页,但是在页码较大时系统开销会比较大,index.max_result_window
默认是10000,搜索请求使用堆的内存和时间和from
+size
的大小成比例的,它们的和越大,系统资源消耗的越多。滚动Scroll接口更加适合用来作深层次的滚动(相对高效一点),但是滚动上下文也是有消耗的,所以也不推荐使用滚动Scroll来作面对用户的实时查询。search_after
参数通过提供一个活动给光标(live cursor)绕过上述的问题,这个想法是使用前一页的结果来帮助下一个页面的检索。假设获取第一页的搜索结果:
curl -X GET "localhost:9200/twitter/_search" -H 'Content-Type: application/json' -d'
{
"size": 10,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"sort": [
{"date": "asc"},
// _id字段启用了doc_values的副本
{"tie_breaker_id": "asc"}
]
}
'
每个文档中值唯一的字段可以被用作排序说明的重要元素(tiebreaker,暂先理解为重要元素),否则具有相同排序元素值的文档排序就会成为未定义甚至可能导致丢失或出现重复的结果。每个文档都有一个唯一的_id
字段值,但并不推荐使用_id
值直接作为tiebreaker。文档值(doc value)是禁止用来作为这个字段的值,因为以文档值作为排序的标准将会在内存中加载大量的数据。取而代之的是,建议复制(客户端或一组摄取处理器)_id
字段的值到另一个字段中(文档值启用了这个字段并使用这个新字段作为排序的决胜局,即tiebreaker)。
上述请求的结果包含了一个数组(包含每个文档的排序值的数组sort values
),sort values
可以用来和search_after
参数结合在任何结果文档集之后开始返回结果,比如可以使用最后一个文档排序值sort values
并将它传递给search_after
去获取下一页的结果:
curl -X GET "localhost:9200/twitter/_search" -H 'Content-Type: application/json' -d'
{
"size": 10,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"search_after": [1463538857, "654323"],
"sort": [
{"date": "asc"},
{"tie_breaker_id": "asc"}
]
}
'
注:当使用search_after
参数时,from
参数必须设置为0或-1。
search_after
不是解决自由跳到一个随机页面而是为了滚动许多并行查询操作,这和scroll
很相似但又不同,search_after
参数是无状态的,它总是对最新版本的搜索器来解决,因此排序的顺序可能会在走的过程(during a walk)中改变(这取决于索引的更新和删除)。