
4.2 分页与排序
在查询大量数据时必须要做分页,一方面是便于用户浏览,但更重要的是防止一次加载数据量过大而导致内存溢出。_search接口提供了一组参数可用于检索结果分页,但它们有各自不同的应用场景,需要区别对待。
4.2.1 from/size参数
_search接口提供的from和size两个参数可以实现分页,其中from参数代表检索文档的起始位置,默认值为0;而size参数则代表每次检索文档的总量,默认值为10。form和size即可以在URI参数中使用,也可以在请求体中使用。例如示例4-5中的两个请求都是从第100条文档开始,一共取20条文档:

示例4-5 from/size参数
from与size的和不能超过index.max_result_window这个索引配置项设置的值。默认情况下这个配置项的值为10000,所以如果要查询10000条以后的文档,就必须要增加这个配置值。例如,要检索第10000条开始的200条数据,这个参数的值必须要大于10200,否则将会抛出类似“Result window is too large”的异常。由此可见,Elasticsearch在使用from和size处理分页问题时会将所有数据全部取出来,然后再截取用户指定范围的数据返回。所以在查询非常靠后的数据时,即使使用了from和size定义的分页机制依然有内存溢出的可能,而index.max_result_window设置的10000条则是对Elasticsearch的一种保护机制。
那么Elasticsearch为什么要这么设计呢?首先,在互联网时代的数据检索应该通过相似度算法,提高检索结果与用户期望的附和度,而不应该让用户在检索结果中自己挑选满意的数据。以互联网搜索为例,用户在浏览搜索结果时很少会看到第3页以后的内容。假如用户在翻到第10000条数据时还没有找到需要的结果,那么他对这个搜索引擎一定会非常失望。其次,如果真的需要遍历所有数据,不能单纯使用from和size,应该结合scroll接口使用。
4.2.2 scroll参数
scroll即是_search接口的参数也是接口,它提供了一种类似数据库游标的文档遍历机制,一般用于非实时性的海量文档处理需求。例如,将一个索引中的文档导入到另一个索引中,或者将索引中的文档导入到MySQL中。使用scroll机制有两个步骤,第一步是创建游标,第二步则是对游标遍历。这两个步骤基于_search接口执行,例如:


示例4-6 创建游标
其中,scroll参数只能在URI中使用,而不能出现在请求体中。它定义了检索生成的游标需要保留多长时间,比如2m代表2分钟,1h代表1小时。scroll保留时长不是处理完所有数据所需要的时长,而是处理单次遍历所需要的时间。从性能角度来看,保留时间越短,空间利用率就越高,所以应该根据单次处理能力设置这个值。size参数可以放在请求体中,也可以挂在地址后面,代表了每次遍历时返回的文档数量。size只能在初始查询时指定在遍历时不能更改,请求体中还可以包含其他_search接口的合法参数。在添加了scroll参数后,返回的结果中将包含一个名为_scroll_id的字段,它惟一地代表了一个scroll查询的结果。接下来,根据这个_scroll_id就可以对结果进行遍历了。例如:

示例4-7 遍历游标
在遍历游标时,不需要指明索引或映射类型,反复调用_search/scroll接口就可实现对结果的遍历了。请求体中的scroll参数相当于延长了游标的存活时长,而scroll_id则是在初始查询时返回的_scroll_id值。在遍历过程中将根据初始查询时设置的size值返回相应数量的文档,但在遍历过程中不能重新修改size值。每次调用scroll都会自动向后遍历,直到所有文档全部遍历结束。在遍历过程中,每次返回的结果中还是会包含_scroll_id字段,通常来说它的值会保持不变。
scroll在超时后将自动删除,但Elasticsearch也为用户提供了主动删除scroll的接口。可以通过请求体发送要删除的游标,例如:

示例4-8 删除游标
对于海量文档的遍历,Elasticsearch还支持对scroll再做片段分割,每一个分割后的片段又可以被独立使用。例如:

示例4-9 游标分段
其中,max定义了分割片段的总量为2,而id则定义了当前请求返回哪一个片段。所以,上面的请求将会把游标分为两个片段,当前请求返回第一个片段。id值从0开始,所以它的值应该小于max。在返回的结果中,同样也会包含_scroll_id字段。每一个游标片段都是独立的,可以使用多线程并发处理。从物理角度来看,Elasticsearch会让游标片段分配到不同的索引分片上以提升遍历速度。所以,游标片段数量不应该大于索引分片数量,否则游标分段的性能将受到影响。正因如此,游标片段数量也有上限,默认为1024,由index.max_slices_per_scroll参数设置。
4.2.3 search_after参数
在前面介绍分页时提到了两种机制,一种是使用from/size,一种是使用scroll。这两种机制都会将数据整体加载进来,不同的是from/size机制下每一次请求都会加载,而scroll则只在初始时加载。所以,scroll实际上比较适合对同一结果集做多次迭代,但在数据量比较大时依然对性能有影响。为此,Elasticsearch提供了另外一种机制search after,它使用search_after参数定义检索应该在文档某些字段的值之后查询其他文档,所以需要预先以这些字段排序。例如:

示例4-10 search_after
在上面的请求中,kibana_sample_data_flights将按DestCountry和FlightNum字段排序,但只返回DestCountry为AE并且FlightNum为AR9OTDM之后的10条文档(size默认为10)。所以这种机制本质上是通过匹配字段,动态决定第一条文档是哪一个,所以在这种情况下from必须设置为0或-1。不仅如此,参与排序的字段值需要保证惟一。虽然这种惟一性保证并非必须,但如果不惟一则在查询时将导致歧义,有可能返回不正确的结果。
需要特别注意的是,这种机制在匹配字段时并非使用精确匹配,而是只要部分满足即可。在上面的例子中,如果含有一个FlightNum为AR9OTDMXXX,也是满足匹配条件的。但由于AR9OTDMXXX会排在AR9OTDM之后,所以还是不会出现问题的。
排序后的检索结果中,都会在最后附带一个排序字段的值,例如示例4-10检索结果最后会包含如下内容:

示例4-11 排序结果
这个内容正好与当前检索结果中的DestCountry和FlightNum字段值相同,可以为下一次search after使用。讲到这里就涉及到检索的另外一个重要内容-排序。
4.2.4 sort参数
排序是文档检索中另一个重要的话题,在很多应用中排序都是一个必不可少的功能。例如,按商品售价、销量排序以搜索出物美价廉的商品。例如在示例4-10中,由于使用search after机制已经使用到了排序。Elasticsearch提供的排序可以依照文档一个或多个字段排序,包括两个虚拟字段_score和_doc。按_score排序就是按文档相似度得分排序,而_doc则是按索引次序排序。例如:

示例4-12 排序
在示例4-12中给出了几种排序方法,将会依次按AvgTicketPrice、FlightDelayMin字段升序排列,再按DistanceKilometers、_score字段降序排列。排序执行的顺序与它们在sort数组中的次序一致。与SQL语言类似,asc代表升序,desc代表降序。默认情况下,除_score按降序排列,其余字段都按升序排列。Elasticsearch支持使用数组类型或多值类型字段做排序,但需要定义如何使用数组中的数据。这包括min、max、avg、sum、median等几种情况,分别代表取最小值、最大值、平均值、总和或中值参与排序,可通过参数mode来定义。例如在下面的示例中,将按products.base_price字段的最大值做降序排列:

示例4-13 数组排序
默认情况下,查询结果会按_score字段降序排列,_score字段是文档与查询条件的相似度得分。也就是说,越是靠前的结果与查询条件的相似度越高,这与人们的使用习惯相符。相似度问题在全文检索中是一个非常重要的话题,将在本书第6章中专门讨论。
此外,由于排序算法需要知道所有参与排序的值才可能做运算,所以参与排序字段在文档中的值都需要加载到内存中来。这一方面对节点分配的内存提出了更高要求,另一方面也要求参与排序的字段必须支持文档值(Doc Value)或fielddata机制。这是因为倒排索引保存的是词项到文档的对应关系,适用于通过词项检索文档。但在排序时需要的是通过文档找到字段值参与排序,所以必须保证能够通过文档找到字段值。在默认情况下,文档值机制对于非text类型的字段都是开启的,而text类型则只能通过开启fielddata机制才可能支持排序。这两种机制在本书第2章2.2.2节有过介绍,如果不记得了可以翻回去查看。