
3.4 操作文档
索引是Elasticsearch中REST接口访问的最基本资源,其次就是文档。前面三个小节介绍的接口基本都是针对索引的操作,本节就来看看针对文档的接口。尽管映射类型在Elasticsearch版本7中已经被废止,但在操作文档时仍需要指明映射类型。由于在版本7中映射类型只能是_doc,所以可以不再把它当成是映射类型,而是将它看成是类似于_alias、_settings、_mapping等一样的接口名称。
对于文档来说,由于_id字段是文档的惟一标识,所以在访问某一文档资源时需要在路径中标识其_id值。Elasticsearch支持通过GET、HEAD、DELETE、PUT和POST方法请求由_id标识的文档,分别用于查看、存在性校验、删除、更新或添加文档。例如在示例3-25中的请求都是合法的:

示例3-28 操作文档
POST和PUT在对文档操作时,它们的作用都是创建或更新文档。但正如前文所述,POST在REST中是非幂等操作,更多是对映射类型访问以添加新文档。所以在示例3-28中,POST请求可以不带文档_id,这样会自动生成文档ID并添加到索引中。这些操作都可以使用refresh参数,这将在操作文档的同时刷新索引。refresh参数有三个可选值,true、false和wait_for,其中wait_for的含义是在刷新索引未完成时不返回操作文档的响应。
3.4.1 索引文档
索引文档是Elasticsearch官方的一种叫法,这里的索引是个动词,意思是将文档分析处理后编入索引以使文档可检索。这听上去比较晦涩,与汉语的习惯与不尽相同。初学者可以将索引文档理解为添加或更新文档。索引文档可以通过PUT或POST请求实现,在索引文档时可以无差别使用。在示例3-28中就分别通过PUT和POST方法创建或更新了_id为1的文档,请求路径中最后的1实际上就是文档_id字段的值。由于_id是元字段,所以不能在请求体中以“<字段名>:<字段值>”的形式设置。如果指定编号的文档已经存在,多次发送PUT或POST请求则相当于更新文档。这时,文档编号字段_id不会发生变化,而文档版本字段_version则会随之增加。如果没有在路径中指明_id值,Elasticsearch会自动生成文档_id值,这相当于对映射类型操作,只能通过POST方法请求。
还要特别注意的是,在这里使用PUT请求更新文档时,不能只更新某一字段,要更新就必须更新整个文档。如果更新文档时只发送了要变更的字段及其值,那么在更新后的文档中将只包含这一字段,其他字段将会被全部删除。Elasticsearch支持通过_update接口更新文档单个字段,详细请参见3.4.4节。
文档版本实际上提供了一种控制并发更新的锁机制,这就是人们常说的乐观锁机制。如果要使用这种机制,需要在请求中添加version参数,例如调用“PUT test/doc/1?version=1”只在文档版本为1时才会更新成功,否则将报版本冲突异常。
在默认情况下文档版本号使用内部文档版本机制实现,版本号从1开始,并在文档更新时加1。除了内部版本机制外,还可以通过设置version_type参数使用外部版本号。外部版本号通过version参数传入,只有当version参数值大于或等于当前文档版本值,version参数的值才会被读取出来,并设置为文档当前的版本号。version_type可选值为
● internal:内部版本机制,类似于自增;
● external或external_gt:外部版本机制,并在version参数大于当前版本号时有效;
● external_gte:外部版本机制,并在version参数大于等于当前版本号时有效。
如果不想让PUT请求在文档存在时更新文档,可以通过设置操作类型来禁止更新,这样在文档存在时也会报版本冲突异常:

示例3-29 版本冲突
从Elasticsearch实现机制上来说,旧版本的文档只会标识为已删除而不会立即做物理上的删除。被标识为已删除的文档,用户就不能再访问了。这些被标识删除的旧版本文档将在后台统一删除,所以这种机制的主要目的是为了提升性能。
3.4.2 获取文档
Elasticsearch支持以GET、HEAD方法通过_id值获取单个文档,也支持通过_mget根据多个_id值获取多个文档。前者_id值将出现在请求的路径中,而后者则会将_id值通过请求体参数传递给_mget。
1.获取单个文档
通过GET方法或HEAD方法请求文档资源时,可查看_id字段标识的文档信息。HEAD方法用于存在性校验,返回结果通过状态码200和404表示文档是否存在。GET方法返回结果包含了文档基本信息,例如调用“GET /test/_doc/1”返回结果基本格式如下:


示例3-30 GET请求文档
其中,_index、_type、_id和_version是文档的元字段,分别代表文档的索引、映射类型、ID和版本;found代表ID标识的文档是否存在,而_source则是原始存入索引的文档。如果没有找到ID标识的文档,found为false,而_source字段也不会出现在结果中。如果不想看到元字段而只对源文档感兴趣,可以在路径后添加_source参数或_source路径,这时将只返回源文档。例如:

示例3-31 使用_source过滤结果
在示例3-31中,第1种过滤方式实际上是在版本7中才引入的,它是在映射类型被废除的背景下用于取代第2种过滤方式的新接口,所以以后在使用中应该尽可能使用第1种方式。除了_source参数,还有_source_include和_source_exclude参数可以精确指定要包含和要排除的字段,可以在这些参数中使用星号“*”通配符。这三个参数还可以与前面提到的_source路径放在一起使用,例如第5种请求方式就会只返回源文档中的gender字段。
最后要强调的是,根据文档ID查看文档时接口满足实时性要求。如果文档已经更新但未被编入索引,该接口在执行查询前会先刷新。如果不希望这个接口做这种实时性刷新,可通过参数realtime设置为false,从而禁止实时刷新。
2.获取多个文档
前述路径中包含的_id值,不支持设置多个_id或使用星号“*”通配符。如果想要根据一组_id值查看多个文档,可以使用_mget接口来实现。_mget接口根据索引名称和文档_id获取多个文档,可以使用GET或POST方法请求该接口。在请求地址中可以指定一个或多个索引,也可以不包含索引。例如示例3-32中_mget请求地址中就没有包含索引,而是在请求体中通过docs参数指明了索引和_id:


示例3-32 _mget接口
如果在请求地址中指明了索引,则在请求体的docs参数中就可以不用再指定索引。但如果在docs指定了_indexe参数,并且与路径中的索引不一致,那么在检索时将覆盖路径中的索引。类似地,_mget接口可以使用_source参数指定获取哪些字段,但_source不是在请求路径中使用,而是在docs参数中与_index、_id等参数一起使用。 除此之外还有stored_fields、routing等参数可以使用,分别用于查询存储字段和指定路由规则。
本小节讨论的获取文档,无论是获取单个文档还是多个文档都必须先要知道文档的_id,这在实际应用中并不是常见的文档检索方法。有关文档检索的更多内容,请参考第4、5章。
3.4.3 删除文档
Elasticsearch提供了两种删除文档的接口:一种是根据文档_id从索引中将文档删除;另一种则是根据查询条件找到满足条件的文档并删除。根据文档_id删除文档,使用DELETE方法发送请求;而根据查询条件删除文档,则需要使用POST或GET方法发送请求。例如:

示例3-33 删除文档
在示例3-33中,前者将users索引中_id为1的文档删除,属于根据_id删除文档;而后者则是将name与tom词项匹配的所有文档都删除,属于根据条件删除文档。
如前所述,所有编入索引的文档都有版本信息。在删除文档时,也可以跟更新文档类似指定版本号,以确保要删除的文档在删除时没有更改。不仅如此,删除操作也会导致文档版本号增加。已删除文档的版本号在删除后短时间内仍然可用以控制并发,可用时长由索引的配置项index.gc_delete索引设置,默认值为60s。
3.4.4 更新文档
尽管使用PUT方法请求文档可以更新文档,但它不能只更新文档中的某一字段值,而且必须要知道文档的_id字段值。如果想要只更新某个字段,或是根据_id字段以外的条件更新文档,使用PUT方法就无法实现了。为了解决这两个问题,Elasticsearch提供了_update接口和_update_by_query两个接口。
1._update接口
_update接口主要用于解决更新文档单个字段的问题,可以使用doc参数指明要更新哪些字段。例如在示例3-34中,如果_id为1的文档包含gender字段,则gender字段将被更新为M;如果文档中不存在gender字段,则将会在文档中添加gender字段并更新值为M:

示例3-34 _update接口
事实上,还可以使用“POST /students/_doc/1/_update”的形式更新文档,只是这种访问形式是版本7之前的方法,已经被废止。_update接口在文档不存在时提示错误,如果希望在文档不存在时创建文档,则可以在请求中添加upsert参数或doc_as_upsert参数,例如:


示例3-35 upsert参数
upsert参数定义了创建新文档使用的文档内容,而doc_as_upsert参数的含义是直接使用doc参数中的内容作为创建文档时使用的文档内容。
_update接口除了可以使用doc参数指定要更新的字段以外,还可以使用script参数设置更新脚本。script参数包含三个子参数,即source、lang和params。source参数设置实际用于更新文档的脚本片段;lang则指定了要使用的脚本语言;params定义了一个Map,包含在脚本片段中所需要的参数。在脚本更新中,一般使用Painless脚本语言,这在本书第2章2.5.2节有过一些介绍。Painless在不同上下文中应用时,会有一些不同的上下文变量可以使用。在_update接口中可以使用的变量见表3-5。
表3-5 _update接口脚本上下文变量

利用表3-5中的ctx['_op']和ctx['_source']可实现对源文档的更新。例如,在示例3-36中就是使用了Painless脚本,将users索引中_id为1的文档中age字段加1:


示例3-36 使用脚本更新文档
在上面的请求中,ctx._source即ctx['_source']的另一种访问形式。由于ctx['_source']的类型是Map,所以可以使用“ctx._source.<字段名>”访问或修改源文档字段;如果要访问的字段并不存在,则会将这个字段添加到文档中;如果想要删除字段,则可以调用Map的remove方法删除。示例3-36中的第二个请求就是将文档_id为3的gender字段删除了。
使用ctx['_op']变量可以设置更新时的行为,可选值包括index、delete和none。例如在示例3-37中,当_id为1的文档age字段大于24时删除文档,否则不做任何操作。

示例3-37 使用ctx[' op']变量
当执行的操作为none时,在返回结果中会包含noop;而执行删除时,在返回结果中将包含delete。
与_update接口类似,如果请求文档不存在,Elasticsearch会返回文档不存在的错误提示。如果希望在文档不存在时自动将文档插入,可以将scripted_upsert参数设置为true,或者使用upsert参数中加入要插入的文档内容。
2._update_by_query接口
前面的更新文档都是通过_id找到文档,然后再对文档进行更新。但在很多场景下,用户并不知道文档_id,这时就要用到根据查询条件进行更新的_update_by_query接口了。该接口使用POST方法请求,并通过query参数接收查询条件。例如,在示例3-38中的更新请求,只更新包含有age字段的文档,然后将它们的age字段加1:

示例3-38 _update_by_query接口
query字段中使用的是Elasticsearch中专门使用的查询语言称为DSL,有关这种查询语言的详细介绍请参见本书第4章。script使用了与_update接口类似的脚本,上下文变量除了ctx['_now']不可使用以外,其余在表3-5中提及的变量均可使用。
3.4.5 批量操作
如果需要批量地对Elasticsearch中的文档进行操作,可以使用_bulk接口执行以提升效率和性能。_bulk接口一组请求体,请求体一般每两个一组,对应一种对文档的操作;第一个请求体代表操作文档的类型,而第二个请求体则代表操作文档所需要的参数。但对于某些不需要参数的文档操作来说,则可能只有一个请求体。
操作类型包括index、create、delete、update等,其中index和create都代表创建文档,区别在于当要创建的文档存在时,create会失败而index则可以变为更新;delete和update则分别代表删除和更新文档。例如:

示例3-39 _bulk接口