起因是一月份需求中有一个功能,需要遍历ES索引中的内容进行处理,原本之前常用的的是ES的scroll方法来处理的,但是对比以前其他同事开发的功能发现以前某个已离职的同事做的一个类似的功能的时候用的是search_after方法。所以在这里把3中方式都列下,对比说明下。题外话不多说,进入正题

我们在进行ES数据查询的时候,对于翻页需求的处理可以使用的方法有3种,如题中所述分别是from&size参数,scroll滚动处理,search_after参数

首先是基础环境

请求

GET /

结果

{
    "name": "olap05-sit",
    "cluster_name": "common",
    "cluster_uuid": "xgg8HEt3SzyI7-Jyg4-J4A",
    "version": {
        "number": "5.4.2",
        "build_hash": "Unknown",
        "build_date": "Unknown",
        "build_snapshot": true,
        "lucene_version": "6.5.1"
    },
    "tagline": "You Know, for Search"
}
  • from&size分页查询

请求

GET /your_es_index/your_es_index/_search
{
  "size": 20,
  "from": 0
}

结果

{
  "took": 10,
  "timed_out": false,
  "_shards": {
    "total": 30,
    "successful": 30,
    "failed": 0
  },
  "hits": {
    "total": 8645477,
    "max_score": 1.0,
    "hits": [
      {
        "_index": "your_es_index",
        "_type": "your_es_index",
        "_id": "6073958667689227072",
        "_score": 1.0,
        "_source": {
          "brand_name": "iPad",
          "bu_name": "冰洗",
          "end_time": "2022-05-27 12:14:56",
          "item_id": "000882022439",
          "oid": "6073958667689227072",
          "pay_time": "2022-05-15 05:11:43",
          "purch_title": "无印良品日式简约全棉被套单件纯棉被罩冬新款150x200x230单人87"
        }
      },
      ......
    ]
  }
}

返回结果太长,不全贴出来了。简单说明下,size参数表示的是每页返回结果条数,from表示从某个位置开始查询,即相当于偏移量的意思,不是表示的当前页码,如果在页面上需要做分页显示当前页码需要自行计算转换下。自己在代码中对请求参数的page、pageSize转换为es查询中的from和size参数。

默认不传的情况下size的值是10,即每页返回10条数据。from的参数的默认值为0。

以上是就是from&size的基本用法,接下来说下这个方法的限制条件,我们修改下参数内容,向es发出如下请求

GET /your_es_index/your_es_index/_search
{
  "size": 1,
  "from": 10000
}

可以得到如下的异常返回结果,其中因为篇幅,我省略了部分信息

{
  "error": {
    "root_cause": [
      {
        "type": "query_phase_execution_exception",
        "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
      }
    ],
    "type": "search_phase_execution_exception",
    "reason": "all shards failed",
    "phase": "query",
    "grouped": true,
    "failed_shards": [
      {
        "shard": 0,
        "index": "your_es_index",
        "node": "ld7BoK2kR1CGSbSek6iTwA",
        "reason": {
          "type": "query_phase_execution_exception",
          "reason": "......"
        }
      }
    ]
  },
  "status": 500
}

从这个结果我们可以很明白的看到,es直接报错了,返回给我们一个from + size must be less than or equal to: [10000]的信息。es是限制的from&size的深分页查询数量的,并给出了一个配置参数index.max_result_window,我们可以修改这个参数来扩大支持数量。

通常来说,如果是页面查询,即时在一个页面展示100条数据的情况下,也要往后翻100页才能达到这个限制,所以一般的页面浏览的情况下,这种方式是完全足够的。

在我们的请求参数直接请求从from=500的情况下查询,实际上es在服务端也是需要把前500条数据捞出来并计算出来的,这样,在分布式节点数量增大,总数据量增大的情况下,越往后翻页,需要耗费的计算资源就越发巨大。

  • search_after查询

直接先看查询语句

GET /your_es_index/your_es_index/_search
{
    "size": 1000,
    "search_after": [0],
    "sort":"_doc"
}

返回结果,内容过多进行了删减展示

{
  "took": 18,
  "timed_out": false,
  "_shards": {
    "total": 30,
    "successful": 30,
    "failed": 0
  },
  "hits": {
    "total": 8645477,
    "max_score": null,
    "hits": [
      {
        "_index": "your_es_index",
        "_type": "your_es_index",
        "_id": "4760525852067779828",
        "_score": null,
        "_source": {
          "brand_name": "希乐(Cille)",
          "bu_name": "通讯",
          "end_time": "2022-05-08 20:35:03",
          "item_id": "001684745858",
          "oid": "4760525852067779828",
          "pay_time": "2022-05-04 00:44:28",
          "purch_title": "Muscletech肌肉科技精英分离乳清蛋白营养增肌粉2.65磅男女健身",
          "sell_diff": 0.01,
          "sn_supplier_user_code": "000815703279",
          "sn_supplier_user_name": "安康市莱仕电力有限公司",
          "step_trade_status": "",
          "supplier_user_name": "小天籁居家日用旗舰店供应商141"
        },
        "sort": [
          1
        ]
      },
      ......
    ]
  }
}

在查询结果中,每一条数据都有个sort字段,遍历结果,得到一个当前查询的最后的sort值,并带到下一次查询的请求参数中的search_after字段中。如上面的结果,假定我们遍历了结果之后,最后的sort值为1001,则下次请求的时候我们请求的参数应当是

GET /your_es_index/your_es_index/_search
{
    "size": 1000,
    "search_after": [1001],
    "sort":"_doc"
}

但是这里有一个问题,在查询的时候,使用的sort字段应当具有全局唯一性,如上代码中我使用_doc字段来进行sort排序其实是有问题的,因为我创建索引中有30个shard。一般说法是[index + type + 文档_id]三个字段在一个ES实例/集群中是全局唯一的,事实上应当是[index + type + 分片标识 + 文档_id]这样的组合。

那么,在上面查询的结果中,我们看到的内容则应当是有30条sort值为1的数据,30条sort值为2的数据,这样的情况。又因为我们是按照1000条数据进行的分页查询,那么查询结果列表最后是只有10条sort等于34的数据的,还有20条因为分页限制被截断了。如果我们这时候拿34作为下次查询的search_after的值的话,则会丢失掉这20条数据,这就是为什么sort字段需要全局唯一的原因。关于这部分更深入的内容可以参见Elasticsearch 7 : 文档唯一性ElasticSearch——路由(_routing)机制这两票文章。

在这里我们可以使用自己定义的唯一字段来oid来查询,如果没有一个字段是全局唯一的话可以使用组合字段的排序方式处理,即查询请求如下

GET /your_es_index/your_es_index/_search
{
    "size": 1000,
    "search_after": [0,0],
    "sort":[{"oid":"asc"},{"_doc":"asc"}]
}

得到返回结果

{
  "took": 25,
  "timed_out": false,
  "_shards": {
    "total": 30,
    "successful": 30,
    "failed": 0
  },
  "hits": {
    "total": 8645477,
    "max_score": null,
    "hits": [
      {
        "_index": "your_es_index",
        "_type": "your_es_index",
        "_id": "1000000368132804023",
        "_score": null,
        "_source": {
          "brand_name": "retrosun",
          "bu_name": "小家电",
          "end_time": "2022-05-30 14:59:05",
          "item_id": "001815388825",
          "oid": "1000000368132804023",
          "pay_time": "2022-05-28 04:17:24",
          "purch_title": "乐扣乐扣吸管水杯儿童卡通可爱Tritan便携带刻度幼儿园小学生男女",
          "sell_diff": 0.01,
          "sn_supplier_user_code": "002024773355",
          "sn_supplier_user_name": "湛江市进正文化传播有限公司",
          "step_trade_status": "FRONT_PAID_FINAL_PAID",
          "supplier_user_name": "yaloo雅鹿雁城专卖店供应商101"
        },
        "sort": [
          "1000000368132804023",
          85907
        ]
      },
      ......
    ]
  }
}

对应的拿到最后的一组sort值,在下一页查询的时候代入search_after字段中。

相对于from&size形式的分页查询,search_after的这种方式可以支持深分页,但是需要保证自己查询的时候选择的sort字段是唯一的,否则查询的时候有漏数据的风险。另外,如果选择的sort字段是无序或者非连续的,则无法完成跳页功能,因为search_after的值不能像form的值一样被主观的计算出来。

  • scroll查询

不同于上面的from&size和search_after的查询方法,先看请求参数和返回结果

请求参数

GET /your_es_index/your_es_index/_search?scroll=1m
{
    "query": {
    }
}

响应结果

{
  "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoHg......ZWs2aVR3QQ==",
  "took": 13,
  "timed_out": false,
  "_shards": {
    "total": 30,
    "successful": 30,
    "failed": 0
  },
  "hits": {
    "total": 8645477,
    "max_score": 1.0,
    "hits": [
      {
        "_index": "your_es_index",
        "_type": "your_es_index",
        "_id": "6073958667689227072",
        "_score": 1.0,
        "_source": {
          "brand_name": "iPad",
          "bu_name": "冰洗",
          "end_time": "2022-05-27 12:14:56",
          "item_id": "000882022439",
          "oid": "6073958667689227072",
          "pay_time": "2022-05-15 05:11:43",
          "purch_title": "无印良品日式简约全棉被套单件纯棉被罩冬新款150x200x230单人87",
          "sn_supplier_user_code": "001372016913",
          "sn_supplier_user_name": "保定市瑞纳集成电路有限公司",
          "step_trade_status": "FRONT_PAID_FINAL_NOPAID",
          "supplier_user_name": "孜恋旗舰店供应商82"
        }
      },
      ......
    ]
  }
}

scroll滚动查询并不是实时的查询索引中的数据,第一个 scroll 请求发出的时候,就像是es会生成一个快照,请求参数里有一个scroll=1m字段,表示这个快照的存活时间,而在返回的结果中也有一个"_scroll_id": "DnF1ZXJ5......R3QQ=="字段,之后的每次查询则根据上一次的_scroll_id进行在这个快照中查询。

GET /_search/scroll
{
  "scroll" : "1m",
  "scroll_id": "DnF1ZXJ5VGhlbkZldGNoHg......ZWs2aVR3QQ=="
}

所以在滚动查询过程中,对索引的内容进行改动都不会对已经开始的scroll滚动查询的结果有影响。scroll滚动查询到的结果只代表当前查询快照生成的时候的情况。

每次滚动返回结果中的_scroll_id可能会变化,所以每次滚动需要拿到最新的_scroll_id,不能一直使用第一次拿到的_scroll_id。如果超过了预设的快照存活时间,则会得到一个错误提示

{"type":"search_context_missing_exception","reason":"No search context found for id [7369602]"}

这样一页一页的查询就类似于游标一样,每次滚动查询往后移动游标地址返回查询得到的结果,没有深分页问题,但是不同于from&size和search_after,scroll不能自定义起始位置,必须从头开始一页页查询,直到查询到最后的返回结果中hits数组中的结果为空。

一般情况下,scroll是在非实时的处理大批量数据的场景下使用的,比如索引数据迁移reindex操作的时候。

到这里来看似乎和search_after的查询方式差不多没什么太大区别,不过scroll滚动查询支持Sliced Scroll 方式的查询,举个栗子

GET /your_es_index/your_es_index/_search?scroll=1m 
{
    "slice": {
        "id": 0,
        "max": 2
    },
    "query": {
    }
}
GET /your_es_index/your_es_index/_search?scroll=1m 
{
    "slice": {
        "id": 1,
        "max": 2
    },
    "query": {
    }
}

对scroll滚动查询进行切片处理,对同样的query请求参数在自己的应用程序中同时启动2个线程,并分别使用不同的切片参数。如下

"slice": {
    "id": 切片的id,
    "max": 最大切片数
},

在各自独立的滚动请求线程中,每个滚动都是独立的,可以像任何滚动请求一样并行处理。第一个请求的结果返回属于第一个slice(id:0)的文档,第二个请求的结果返回属于第二个slice的文档。由于最大切片数设置为2,因此两个请求的结果的并集等效于没有切片的滚动查询的结果。elasticsearch 深入 —— Scroll滚动查询

  • 小结一下
  1. from&size支持跳页查询,但是有深分页问题,用法简单,方便快捷
  2. search_after支持深分页,需指定全局唯一字段,不然可能会丢数据,一定程度上可以支持跳页查询,支持实时查询结果
  3. scroll支持深分页,不支持实时查询,不支持跳页,但是可以切片使用多线程加速滚动查询