4. 检索模版
4.1 检索模版基础知识
1. 什么是检索模版 我们知道什么是索引模板:便于跨索引统一建模;适合数据量巨大、索引字段类似的业务系统;灵活便捷。
检索模板不如索引模板的使用范围广。在实战业务场景中,每次业务请求都要构造DSL,DSL可能有些微差别(比如这次查title、下次查content),除此之外的部分都一样,但要实现两次请求,后端代码就要有相应的修改和适配。有没有不修改、拼接DSL使用检索的方案?这就需要检索模板了。
检索模板与关系数据库中的存储过程在概念上有很多相似之处。它们都允许预定义并存储一些常用的操作,然后在需要时引用它们的标识符(例如ID)来调用这些操作。
关系数据库能够存储常见的数据库操作流程,以便在多个场合下重复使用这些操作,而es的检索模板也允许预定义一些常用的查询模板,通过模板ID来引用。可以简化查询操作的构造过程,提高查询效率。好处是,可以抽象和封装复杂的查询逻辑,使得应用程序更易于编写和维护,同时减少了手动编写复杂的查询逻辑而导致的错误。此外,使用查询模板,可以将查询逻辑从应用程序代码中解耦出来,使得对查询逻辑的修改和管理更为方便
检索模板支持在运行时指定参数。检索模板存储在服务器端,可以在不更改客户端代码的情况下对其进行修改。检索模板利用Mustache模板引擎。是一种“无逻辑”的模板语法。模板本身只负责表示和描绘数据的呈现格式及结构,而不关心数据是如何生成和控制的。Mustache能够清晰简洁地表示模板,还可以方便地接受参数来动态地生成内容。在es中,这种机制使得我们能够创建一些动态的、可以在运行时接受参数的查询模板,从而极大地提高查询的灵活性和可复用性 2. 检索模版示例
PUT _scripts/cur_search_template
{
"script":{
"lang":"mustache",
"source":{
"query":{
"match":{
"{{cur_field}}":"{{cur_value}}"
}
},
"size":"{{cur_size}}"
}
}
}
# 还是那个索引
POST lwy_index/_search/template
{
"id":"cur_search_template",
"params": {
"cur_field": "title",
"cur_value": "乌兰",
"cur_size": 3
}
}
定义了一个名为cur_search_template的检索模板。使用Mustache模板引擎,并在模板中定义了3个参数,包括cur_field、cur_value和cur_size。
检索模板被定义并存储在服务器端,就可以发送一个特定的查询请求来使用这个模板进行检索。
好处在于用户可以动态地设定搜索字段和搜索参数,使得检索过程具有更高的灵活性和可配置性。实际上,通过使用检索模板,可以在服务端预定义复杂的查询结构,仅需在客户端传递不同的参数,即可实现对不同字段或不同条件的检索,从而实现检索逻辑与请求参数的有效分离。
5. 深度解读es分页查询
常使用的es分页查询方式有如下3种。
- from+size查询
- search_after查询
- scroll查询
关于分页查询的常见问题如下。
- 若要一次性获取索引上的某个字段的所有值(100万左右),除了把max_result_window调大以外,还有没有其他方法?
- 关于分页设置,若要每次将20条结果展示在前台,点击“下一页”则可以查询后面20条数据,那么应该怎么实现?
- from+size、scroll、search_after查询方式的本质区别和应用场景分别是什么?
下面就对这3种方式的联系与区别、优缺点、适用场景等展开进行解读。
5.1 from+size 查询
es允许查询、分析和搜索大量数据。分页查询是其中一项重要功能,可以将数据分成多页,以避免一次性检索大量数据。
from参数指定从结果集中的第几条数据开始返回,而size参数指定返回数据的数量。以Kibana自带的飞行样例数据集为例,若希望从结果集中的第11条数据开始返回5条数据,并基于飞行时间降序排序,则可以使用以下命令进行查询。
GET kibana_sample_data_flights/_search
{
"from": 10,
"size": 5,
"query": {
"match": {
"DestWeather": "Sunny"
}
},
"sort": [
{
"FlightTimeHour": {
"order": "desc"
}
}
]
}
from+size分页查询的优缺点如下。
- 优点:支持随机翻页。
- 缺点:限于max_result_window设置,不能无限制翻页;存在深度翻页问题,越往后翻页越慢。
from+size查询适用场景如下。
- 非常适合小型数据集或者从大数据集中返回Top N(N≤10000)结果集的业务场景。
- 主流PC搜索引擎中支持随机跳转分页的业务场景
深度分页不推荐使用from+size
es会限制最大分页数,避免因大数据量的召回导致系统性能低下。max_result_window默认值是10000,意味着每页有10条数据,会最大翻页至1000页。主流搜索引擎实际都翻不了那么多页。例如,在百度中搜索“上海”,在搜索结果中翻到第76页,就无法再往下翻页了,
PUT kibana_sample_data_flights/_settings
{
"index.max_result_window":50000
}
官方建议避免使用from+size来过度分页或一次请求太多结果。
搜索请求通常会跨多个分片,每个分片必须将其请求的命中内容以及先前页面的命中内容加载到内存中。
对于分页较多的页面或大量结果,这样操作会显著增加内存和CPU使用率,导致性能下降,甚至导致节点故障。
GET kibana_sample_data_flights/_search
{
"from": 10000,
"size": 10
}
该样例的结果是有共10条数据加载到内存吗?不是的,其实是有共10011条数据加载到内存,只是经过后台处理后返回了10条符合条件的数据。这也就意味着,越往后翻页(即深度分页),需要加载的数据量越大,越耗费CPU和内存资源,响应就会越慢。
5.2 search_after查询
基本工作原理是以前一页结果的排序值作为参照点,进而检索与这个参照点相邻的下一页的匹配数据。这种方法在处理大规模数据分页时更为高效且实用。
使用该查询的前置条件是要求后续的多个请求返回与第一次查询相同的排序结果序列。也就是说,在后续翻页的过程中,即便有新数据写入等操作,也不会对原有结果集构成影响。
那么,如何实现呢?
可以创建一个时间点—PIT(Point In Time)来保障在搜索过程中能保留特定事件点的索引状态。
PIT是7.10版本之后才有的新特性,实际上是存储索引数据状态的轻量级视图。
如下示例能很好地解释PIT的内涵,其中keep_alive参数代表保持时间长度。
# 创建PIT
POST kibana_sample_data_flights/_pit?keep_alive=1m
# 获取数据量13059
POST kibana_sample_data_flights/_count
# 新增一条数据
POST kibana_sample_data_flights/_doc/13060
{
"test":"test"
}
# 获取数据量13060
POST kibana_sample_data_flights/_count
# 查询PIT,数据依然是13059
POST /_search
{
"track_total_hits": true,
"query": {
"match_all": {}
},
"pit":{
"id":"w62xAwEaa2liYW5hX3NhbXBsZV9kYXRhX2ZsaWdodHMWZllSd0VJTUxROE9BT1hOX1Q1TXN0dwAWWk51MjBzakpUZHU2eTN0M3RpRm9OZwAAAAAABBG1nhZpeTV6VUNuM1IwcUhvOGFHdEQxVFp3ARZmWVJ3RUlNTFE4T0FPWE5fVDVNc3R3AAA="
}
}
search_after的后续查询都是基于PIT视图进行的,能有效保障数据的一致性。
查询过程可以简单概括为如下几个步骤。
- 创建PIT视图,这是必要的前置条件
POST kibana_sample_data_flights/_pit?keep_alive=5m
keep_alive=1m是一个类似于scroll的参数,表示滚动视图的保留时间是1min,超过1min会清除这个滚动视图
- 创建基础查询语句,主要是设置分页的条件
POST /_search
{
"size": 10,
"query": {
"match_all": {}
},
"pit": {
"id": "w62xAwEaa2liYW5hX3NhbXBsZV9kYXRhX2ZsaWdodHMWZllSd0VJTUxROE9BT1hOX1Q1TXN0dwAWWk51MjBzakpUZHU2eTN0M3RpRm9OZwAAAAAABBK87hZpeTV6VUNuM1IwcUhvOGFHdEQxVFp3ARZmWVJ3RUlNTFE4T0FPWE5fVDVNc3R3AAA",
"keep_alive": "5m"
},
"sort": [
{
"Dest.keyword": {
"order": "asc"
}
}
]
}
代码中设置了PIT,因此检索时候就不需要再指定索引。id是基于第一步返回的id值。排序sort指的是按照哪个关键字排序。
在每个返回文档的最后会有一个sort
就是我们指定的排序方式Dest
- 后续翻页
POST /_search
{
"size": 10,
"query": {
"match_all": {}
},
"pit": {
"id": "w62xAwEaa2liYW5hX3NhbXBsZV9kYXRhX2ZsaWdodHMWZllSd0VJTUxROE9BT1hOX1Q1TXN0dwAWWk51MjBzakpUZHU2eTN0M3RpRm9OZwAAAAAABBK87hZpeTV6VUNuM1IwcUhvOGFHdEQxVFp3ARZmWVJ3RUlNTFE4T0FPWE5fVDVNc3R3AAA",
"keep_alive": "5m"
},
"sort": [
{
"Dest.keyword": {
"order": "asc"
}
}
],
"search_after": [
"Albuquerque International Sunport Airport"
]
}
后续翻页都需要借助search_after来指定前一页中最后一个文档的sort字段值
search_after查询的优缺点如下。
- 优点:不严格受制于max_result_window,可以无限地往后翻页。“不严格”是指单次请求值不能超过max_result_window,但总翻页结果集可以超过。
- 缺点:只支持向后翻页,不支持随机翻页。更适合在手机端应用的场景中使用,类似今日头条等产品的分页搜索。
5.3 scroll 查询
相比于from+size不支持分页和search_after向后翻页的实现,scroll API可从单个搜索请求中检索大量结果(甚至所有结果),这种方式与传统数据库中的游标类似。
如果把from+size和search_after两种请求看作近实时的请求处理方式,那么scroll滚动遍历查询显然是非实时的。数据量大的时候,响应时间可能会比较长。
scroll查询的核心执行步骤如下。
- 指定检索语句的同时设置scroll上下文保留时间。
实际上,scroll已默认包含了search_after的PIT的视图或快照功能。
scroll请求返回的结果反映了发出初始搜索请求时索引的状态,就像在那一个时刻做了快照,随后对文档的更改(写入、更新或删除)只会影响以后的搜索请求。
POST kibana_sample_data_flights/_search?scroll=3m
{
"size":100,
"query": {
"match_all": {}
}
}
- 向后翻页
POST _search/scroll
{
"scroll":"3m",
"scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFjZfdHp2RkZ2UWhXOXMyX19TZnJNQlEAAAAAA9Y8mxYtcTY5ZGx4VVNnQ3Q3Ync3TDJlNy13"
}
scroll查询的优缺点如下。
- 优点:支持全量遍历,是检索大量文档的重要方法,但单次遍历的size值不能超过max_result_window的大小。
- 缺点:响应是非实时的;保留上下文需要具有足够的堆内存空间;需要通过更多的网络请求才能获取所有结果。
scroll查询的适用场景如下。
大量文档检索:当要检索的文档数量很大,甚至需要全量召回数据时,scroll查询是一个很好的选择。
大量文档的数据处理:滚动API适合对大量文档进行数据处理,例如索引迁移或将数据导入其他技术栈。
注意:
- 现在不建议使用scroll API进行深度分页。
- 如果要分页检索并获得超过10000条结果时,则推荐使用PIT+search_af ter。
对上述查询方式进行简要总结,如下。
- from+size:随机跳转不同分页(类似主流搜索引擎),适用于10000条结果数据之内的分页显示场景。
- search_after:仅支持向后翻页,适用于超过10000条结果数据的分页场景。
- scroll:需要遍历全量数据的场景。
另外,调大max_result_window值“治标不治本”,不建议将该值调得过大。PIT本质上是视图