二、ElasticSearch 学习 ELK 进阶

(1)文档局部更新

我们也说过文档是不可变的——它们不能被更改,只能被替换。 update API必须遵循相同的规则。表面看来,我们似乎是局部更新了文档的位置,内部却是像我们之前说的一样简单的使用 update API处理相同的检索*-修改-*重建索引流程,我们也减少了其他进程可能导致冲突的修改。

最简单的 update 请求表单接受一个局部文档参数 doc ,它会合并到现有文档中——对象合并在一起,存在的标量字段被覆盖,新字段被添加。举个例子,我们可以使用以下请求为博客添加一个 tags 字段和一个 views 字段:

POST /website/blog/1/_update{"doc" : {"tags" : [ "testing" ],"views": 0}}

返回:

{"_index" : "website","_id" : "1","_type" : "blog","_version" : 3}

查询当前的文档信息:

GET /website/blog/1{"_index": "website","_type": "blog","_id": "1","_version": 3,"found": true,"_source": {"title": "My first blog entry","text": "Starting to get the hang of this...","tags": [ "testing" ], "views": 0}}

(2)使用脚本局部更新

使用Groovy脚本这时候当API不能满足要求时,Elasticsearch允许你使用脚本实现自己的逻辑。脚本支持非常多的API,例如搜索、排序、聚合和文档更新。脚本可以通过请求的一部分、检索特殊的 .scripts 索引或者从磁盘加载方式执行。默认的脚本语言是Groovy,一个快速且功能丰富的脚本语言,语法类似于Javascript。它在一个沙盒(sandbox)中运行,以防止恶意用户毁坏Elasticsearch或攻击服务器。

1、硬编码更新:

脚本能够使用 update API改变 _source 字段的内容,它在脚本内部以 ctx._source 表示。例如,我们可以使用脚本增加博客的 views 数量:

POST /website/blog/1/_update{"script" : "ctx._source.views+=1"}

2、软编码更新:

我们还可以使用脚本增加一个新标签到 tags 数组中。在这个例子中,我们定义了一个新标签
做为参数而不是硬编码在脚本里。这允许Elasticsearch未来可以重复利用脚本,而不是在想
要增加新标签时必须每次编译新脚本:

#添加新的tag到文档中POST /website/blog/1/_update{"script": {"source": "ctx._source.tags.addAll(params.new_tag)","params": {"new_tag": ["search"]}}}

查询结果:

GET /website/blog/1{"_index" : "website","_type" : "blog","_id" : "1","_version" : 10,"_seq_no" : 11,"_primary_term" : 1,"found" : true,"_source" : {"title" : "My first blog entry","text" : "Just trying this out...3","views" : 2,"tags" : ["testing","search"]}}

3、更新可能不存在的文档

比如:

​ 我们要在Elasticsearch中存储浏览量计数器。每当有用户访问页面,我们增加这个页面的浏览量。但如果这是个新页面,我们并不确定这个计数器存在与否。当我们试图更新一个不存在的文档,更新将失败。

​ 在这种情况下,我们可以使用 upsert 参数定义文档来使其不存在时被创建,将自动创建该索引信息。

POST /website_new/pageviews/1/_update{"script": {"source": "ctx._source.views+=1"},"upsert": {"views": 1}}

查询自动创建的信息:

GET /website_new/pageviews/_search{}//返回值{"took" : 472,"timed_out" : false,"_shards" : {"total" : 1,"successful" : 1,"skipped" : 0,"failed" : 0},"hits" : {"total" : {"value" : 1,"relation" : "eq"},"max_score" : 1.0,"hits" : [{"_index" : "website_new","_type" : "pageviews","_id" : "1","_score" : 1.0,"_source" : {"views" : 3}}]}}

4、更新和冲突

​ 为了避免丢失数据, update API在检索**(retrieve)阶段检索文档的当前 _version ,然后在重建索引(reindex)阶段通过 index 请求提交。如果其他进程在检索(retrieve)和重建索引(reindex)**阶段修改了文档, _version 将不能被匹配,然后更新失败。

对于多用户的局部更新,文档被修改了并不要紧。例如,两个进程都要增加页面浏览量,增加的顺序我们并不关心——如果冲突发生,我们唯一要做的仅仅是重新尝试更新既可

这些可以通过 retry_on_conflict 参数设置重试次数来自动完成,这样 update 操作将会在发生错误前进行重试,这个值默认为0。

POST /website/pageviews/1/_update?retry_on_conflict=5{"script" : "ctx._source.views+=1","upsert": {"views": 0}}

配置参数retry_on_conflict=5代表:在错误发生前重试更新5次。这种重试方式更适合于count这种与顺序无关的操作。

5、检索多个文档

使用ElasticSearch,检索多个文档可以使用合并多个请求来避免每个请求单独的网络开销。

如果你需要从Elasticsearch中检索多个文档,相对于一个一个的检索,更快的方式是在一个请求中使用multi-get或者 mget API。

mget:

  • mget API参数是一个 docs 数组,数组的每个节点定义一个文档的 _index 、 _type 、 _id 元数据。但是如果你只想检索一个或者几个确定的字段,也可以使用 _source参数进行设置;
//比如已下操作则是查询两个index的信息整合在一起返回POST /_mget{"docs": [{"_index": "website","_type": "blog","_id": 2},{"_index": "website_new","_type": "pageviews","_id": 1,"_source": "views"}]}//返回结果是:{"docs" : [{"_index" : "website","_type" : "blog","_id" : "2","_version" : 10,"_seq_no" : 5,"_primary_term" : 1,"found" : true,"_source" : {"title" : "My first external blog entry","text" : "This is a piece of cake..."}},{"_index" : "website_new","_type" : "pageviews","_id" : "1","_version" : 3,"_seq_no" : 2,"_primary_term" : 1,"found" : true,"_source" : {"views" : 3}}]}//也会通过docs组成数组的方式返回

如果想检索的在同一个_index中,可以直接使用ids的方式进行检索:

POST /website/blog/_mget{"ids":["2","1"]}//返回{"docs" : [{"_index" : "website","_type" : "blog","_id" : "2","_version" : 10,"_seq_no" : 5,"_primary_term" : 1,"found" : true,"_source" : {"title" : "My first external blog entry","text" : "This is a piece of cake..."}},{"_index" : "website","_type" : "blog","_id" : "1","_version" : 11,"_seq_no" : 12,"_primary_term" : 1,"found" : true,"_source" : {"title" : "My first blog entry","text" : "Just trying this out...3","views" : 3,"tags" : ["testing","search"]}}]}

我们还可以使用设置_type的方式进行检索:

POST /website_new/bolg/_mget{"docs":[{"_id":2},{"_type":"pageviews","_id":1}]}//返回{"docs" : [{"_index" : "website_new","_type" : "bolg","_id" : "2","found" : false},{"_index" : "website_new","_type" : "pageviews","_id" : "1","_version" : 3,"_seq_no" : 2,"_primary_term" : 1,"found" : true,"_source" : {"views" : 3}}]}

可以发现,在website_new index的bolg 类型下没有id为2的数据,所以返回值found为false,所以再使用_mget的时候需要检查found是否为false。

6、批量操作

就像 mget 允许我们一次性检索多个文档一样, bulk API允许我们使用单一请求来实现多个文档的 create 、 index 、 update 或 delete 。

也就是说,我们可以铜bulk API的方式使用一个请求完成create、index、update、delete一系列的操作。这对索引类似于日志活动这样的数据流非常有用,它们可以 以成百上千的数据为一个批次按序进行索引。

bulk 请求体如下:

{ action: { metadata }}\n{ request body }\n{ action: { metadata }}\n{ request body }\n...

这种格式类似于用 “\n” 符号连接起来的一行一行的JSON文档流**(stream)**。两个重要的点需注意:

  • 每行必须以 “\n” 符号结尾,包括最后一行。这些都是作为每行有效的分离而做的标记。
  • 每一行的数据不能包含未被转义的换行符,它们会干扰分析——这意味着JSON不能被美化打印。

将这些放在一起, bulk 请求表单是这样的:

POST /_bulk{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}{ "title": "My first blog post" }{ "index": { "_index": "website", "_type": "blog" }}{ "title": "My second blog post" }{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" :{ "doc" : {"title" : "My updated blog post"} }

通过bulk api操作,Elasticsearch响应包含一个 items 数组,它罗列了每一个请求的结果,结果的顺序与我们请求的顺序相同:

{"took": 4,"errors": false,"items": [{"delete": {"_index": "website","_type": "blog","_id": "123","_version": 2,"status": 200,"found": true}},{"create": {"_index": "website","_type": "blog","_id": "123","_version": 3,"status": 201}},{"create": {"_index": "website","_type": "blog","_id": "EiwfApScQiiy7TIKFxRCTw","_version": 1,"status": 201}},{"update": {"_index": "website","_type": "blog","_id": "123","_version": 4,"status": 200}}]}

但是在使用批量请求的时候,需要注意一点的是:

  • 整个批量请求需要被加载到我们请求节点的内存里面,请求越大,那么给其他请求使用的内存空间就越小。

  • 有一个最佳的 bulk 请求大小。超过这个大小,性能不再提升而且可能降低,这个最佳的大小与当前的硬件、网络环境、和搜索的复杂度有关。

  • 通常可以试着使用线性增长的方式进行寻找这个平衡点,弱随着大小的增长,当性能开始降低,说明你每个批次的大小太大了。开始的数量可以在1000 ~ 5000个文档之间,如果你的文档非常大,可以使用较小的批次,通常着眼于你请求批次的物理大小是非常有用的。一千个1kB的文档和一千个1MB的文档大不相同。一个好的批次最好保持在5 ~15MB大小间。