本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws。
本文参考和学习资料 《ES权威指南》
当年在学校的随笔 Lucene概念入门: http://www.cnblogs.com/tdws/p/4263425.html
一.基本概念
存储数据到ES中的行为叫做索引,每个索引可以包含多个类型,这些不同的类型存储着多个文档,每个文档有多个属性。
索引 index/indexes相当于传统关系数据库中的数据库,是存储关系型文档的地方。
索引在ES做动词的时候,索引一个文档就是存储一个文档到索引(名词)中,以便被检阅和查询到。类似于insert。
默认下,一个文档中的每个属性都是被索引的。没被索引的属性是不能搜索到的。
二.ES集群 主分片 副分片 健康状态
垂直的硬件扩容是有极限的,真正的扩容能力来自于水平扩容(为集群内增加更多节点),集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
主节点负责管理集群范围内所有的变更,增加删除索引和增加删除节点等。主节点不涉及文档级别的变更和搜索等操作。任何一个节点都可接受请求,任何一个节点都知道任意文档所处位置。
curl -XGET 'localhost:9200/_cluster/health?pretty' 获取集群健康状态
status 字段指示着当前集群在总体上是否工作正常。它的三种颜色含义如下:
green 所有的主分片和副本分片都正常运行。
yellow 所有的主分片都正常运行,但不是所有的副本分片都正常运行。
red 有主分片没能正常运行。
分片是一个底层的工作单元,是数据的容器,一个分片是一个Lucene实例。它本身就是一个完整的搜索引擎,到本文最后的分页,相信你能更加理解。应用不会与分片交互。一个分片可以使主分片或者副本分片。副本分片只是主分片的一个拷贝。副本分片用于从灾难中保护数据并未搜索读取操作提供服务,注意只是读操作。
默认情况下创建一个索引,会被分配5个分片。当然我们也可以指定分片数量还有副本数量,比如:
curl -XPUT 'localhost:9200/blogs?pretty' -H 'Content-Type: application/json' -d'
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
假设我们目前集群中只有一个节点,那么刚才三个分片的存储是这样的。即使我们设置了1个副本分片也没用,因为相同数据的主分片和副本分片在一个节点上没有任何意义。
如果此时,我们为集群增加一个节点,主分片和副本分片是下图这样的。副本分片得以创建,并分配在新节点上。现在我们的容灾能力就可以允许失去一台节点的数据,数据读取服务节点就是两个。
接下来,集群中再加入节点,情况如下图所示。为了提高更强的服务能力和容灾能力,均匀分布了主分片和副本分片,现在我们三个node,失去任意一台,都能保障数据和服务。
最后,关闭node1,使集群失去MASTER, 而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点: Node 2 。在失去Master的同时,也失去了主分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为 red :不是所有主分片都在正常工作。幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片, 此时集群的状态将会为 yellow 。 这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。
如果现在恢复node1,如果 Node 1 依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。
三.基本的操作
一个文档的 _index 、 _type 和 _id 唯一标识一个文档。
索引仅仅是一个逻辑上的命名空间,这个命名空间由一个或者多个分片组合在一起。对于应用程序而言,无需关注分片,只需要知道一个文档位于一个索引内。
索引index名称必须小写,不能以下划线开头,不能有逗号。类型type名称可以大写或者小写,但是不能以下划线或者句号开头,不应该包含逗号并且长度在265字符及以内。
当你创建一个新文档的时候,要么主动给id,要么让ES自动生成。所以,确保创建一个新文档的最简单办法是,使用索引请求的 POST 形式让 Elasticsearch 自动生成唯一 _id,比如:
POST /website/blog/ { ... }
以下两种方式为等效操作:
PUT /website/blog/123?op_type=create { ... }
PUT /website/blog/123/_create { ... }
在获取文档的时候,_source字段是用于筛选我们只想要的目标字段,比如
GET /website/blog/123?_source=title,text
如果想要得到http响应头部,可以通过传递 -i 比如
curl -i -XGET http://localhost:9200/website/blog/124?pretty
如果只想得到source字段,不想得到任何其他元数据,使用方式如下:
GET /website/blog/123/_source
如果查询文档是否存在,使用Http HEAD,比如:
curl -i -XHEAD http://localhost:9200/website/blog/123 //存在则返回200 OK,不存在则是404Not Found
在 Elasticsearch 中文档是 不可改变 的,不能修改它们。 相反,如果想要更新现有的文档,需要 重建索引或者进行替换, 我们可以使用相同的 index API 进行实现。和使用创建时PUT是一样的,ES内部都做了。部分更新,也是如此。使用DELETE删除文档,删除文档不会立即将文档从磁盘中删除,只是将文档标记为已删除状态,随着你不断的索引更多的数据,Elasticsearch 将会在后台清理标记为已删除的文档。
四.并发冲突的解决
假设上图操作的是库存数量stock_count ,web_1 对 stock_count 所做的更改已经丢失,因为 web_2 不知道它的 stock_count 的拷贝已经过期。变更越频繁,读数据和更新数据的间隙越长,也就越可能丢失变更。在数据库领域中,有悲观和乐观两种方法通常被用来确保并发更新时变更不会丢失:
悲观并发控制
悲观并发控制一般锁住当前被操作的资源。
乐观并发控制
Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。Elasticsearch 是分布式的,当创建和更新文档的时候,如果你是多个node,并且还有replication备份数据,这中间就会有时间差存在,并且还会存在检索-修改-重建索引间隔。或者说并发更新发生的时候,所获取并在处理中的数据,可能已经被其他请求更新掉,这就导致数据的过期,结果就是覆盖更新,过期数据覆盖新数据。在ES中 我们解决此问题的方式就是利用更新中指定_version版本号来确保相互冲突的更新不会丢失。所有文档的更新或删除 API,都可以接受 version 参数,比如:
PUT /website/blog/1?version=1
过指定了version,如果请求到达ES后,该条数据version已经不是1了,这时就会更新失败并返回409 Conflict响应码。之后应该怎么处理,就取决于你的场景了,正如上面所说的可以重试更新、使用新的数据、或者将相关情况报告给用户。另外一种方式是使用外部版本号,而非ES内置自增的版本号。外部的版本号源于你自己的系统控制,比如更新一条ES文档,指定使用外部版本号为5:
PUT /website/blog/2?version=5&version_type=external
为了并发控制,下一次你的请求给到了version为10,如果你的请求version小于等于上面的5则会更新失败,像前面提到的409一样,只有新version大于ES文档已有version,更新才会成功。
PUT /website/blog/2?version=10&version_type=external
ES文档是不可变的,即使是在部分更新情况下。比如:
POST /website/blog/1/_update { "doc" : { "tags" : [ "testing" ], "views": 0 } } //其将会覆盖现有字段,增加新字段。被称为Update API
部分更新的时候,如果被更新的文档还不存在,这时应该使用upsert参数
POST /website/pageviews/1/_update { "script" : "ctx._source.views+=1", "upsert": { "views": 1 } }
如果你某个场景中经常会出现所更新文档不存在的情况下,那么使用它是明智之选,该方式会在文档存在的时候,直接执行更新脚本将值应用,不存在的时候,则执行upsert.
ES update API还提供了retry_on_conflict参数,指示了返回失败之前,要重试多少次。
POST /website/pageviews/1/_update?retry_on_conflict=5
update API和前面index API所提到的乐观并发控制一样,也支持制定version参数来控制冲突。
ES提供了mget API (multi-get),可以减少网络传输时间等,比如:
GET /_mget { "docs" : [ { "_index" : "website", "_type" : "blog", "_id" : 2 }, { "_index" : "website", "_type" : "pageviews", "_id" : 1, "_source": "views" } ] }
可以看到这段代码中index和type 实在docs数组的item项中。所得到的响应数组中,每个item和使用单个get拿到的结果是一样的。
如果想检索的数据都在相同的 _index 中(甚至相同的 _type 中),则可以在 URL 中指定默认的 /_index或者默认的 /_index/_type 。
你仍然可以通过单独请求覆盖这些值:
GET /website/blog/_mget { "docs" : [ { "_id" : 2 }, { "_type" : "pageviews", "_id" : 1 } ] }
事实上,如果所有文档的 _index 和 _type 都是相同的,你可以只传一个 ids 数组,而不是整个 docs 数组:
GET /website/blog/_mget { "ids" : [ "2", "1" ] }
如果数组中某个寻找目标未找到结果,则响应数组中,其他值正常,未找到的found字段则为false "found" : false,并不妨碍其他文档。所以在应用中,应该检查的不是200还是404,而是found字段标记。
既然提供了mget, 同时bulk API也提供了单个步骤进行多次create index, update和delete请求。
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" : 3} } { "doc" : {"title" : "My updated blog post"} }
delete是没有请求体的,而create,update 请求体是必须的 如上所示。如果其中哪个操作失败了,则会在响应中的一个对应的item中给出相应的error. 而成功的则给出200.比如
{ "took": 3, "errors": true, "items": [
{ "create": { "_index": "website", "_type": "blog", "_id": "123", "status": 409, "error": "DocumentAlreadyExistsException [[website][4] [blog][123]: document already exists]" }},
{ "index": { "_index": "website", "_type": "blog", "_id": "123", "_version": 5, "status": 200 }} ] }
bulk 不是原子性的,不能用它来实现事务控制。每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求。为什么是下面这种格式的写法,为什么是每个请求单独处理,这是因为不同的文档数据,他们主分片可能不同,每个文档可能被分配给不同节点,所以ES这种设计的方式是最明智的。如果使用json数组,看起来简便了,实则导致需要ES解析大量数据,占用更多内存,让JVM来花大量时间进行回收。相反ES现有的方式,则可以直接将原始请求转发到正确的分片上,用最小的内存处理。
也许你正在批量索引日志数据到相同的 index 和 type 中。 但为每一个文档指定相同的元数据是一种浪费。相反,可以像 mget API 一样,在 bulk 请求的 URL 中接收默认的 /_index 或者 /_index/_type ,比如:
POST /website/_bulk { "index": { "_type": "log" }} { "event": "User logged in" }
POST /website/log/_bulk { "index": {}} { "event": "User logged in" } { "index": { "_type": "blog" }} { "title": "Overriding the default type" }
五.分布式文档存储
当索引一个文档的时候,文档将被存储到ES的一个主分片当中。ES索引文档到分片遵从公式:shard = hash(routing) % number_of_primary_shards 。
routing是一个可变值,默认你为文档_id。 如上公式hash出的结果是 0 到 number_of_primary_shards-1 之间的余数,将来我们寻找数据所在分片,就是这样找到的。所以在创建索引的时候就要确定好主分片数量,并且永远不改变primary_shards ,因为一旦改变了,之前hash路由的值将全部无效,文档也就不能被正确找到。当然如果你想扩容,可以有其他的奇技淫巧,后面将会提到。
比如我们有三个节点,两个主分片,每个主分片有两个副本。我们依然可以将请求指向任意节点,每个节点都知道任一文档的位置,并转发请求到目标节点。为了扩展负载,更好的做法是轮询所有节点,以单节点免压力过大。我想,ES知道任一文档的位置,通过上一段给出的公式即可。
向node1发送存储数据请求,路由到node3主分片上,复制给node1和node2,最后返回结果给node1
向node1 发送更新数据请求,路由到node3主分片,更改_source,重新索引数据,成功后复制到node1和node2。如果是部分更新的时候,通知副本节点更新的时候,不是转发部分更新的内容,而是转发完整文档的新版本。
六.搜索
上面,我们可以简单的把ES当作NOSQL风格的分布式文档存储系统。我们可以将文档扔到ES里,然后根据ID检索。但其真正的强大之处并不在于此,而是从无规律的数据找出有意义的信息,从大数据到大信息。
搜索(search) 可以做到:
1.在类似于 gender 或者 age 这样的字段 上使用结构化查询,join_date 这样的字段上使用排序,就像SQL的结构化查询一样。
2.全文检索,找出所有匹配关键字的文档并按照相关性(relevance) 排序后返回结果。
3.以上二者兼而有之。
下面分析一则查询:
GET /_search (curl -XGET 'localhost:9200/_search?pretty')
{ "hits" : { "total" : 14, "hits" :
[ { "_index": "us", "_type": "tweet", "_id": "7", "_score": 1,
"_source": { "date": "2014-09-17", "name": "John Smith", "tweet": "The Query DSL is really powerful and flexible", "user_id": 2 } },
... 9 RESULTS REMOVED ... ], "max_score" : 1 },
"took" : 4,
"_shards" : { "failed" : 0, "successful" : 10, "total" : 10 }, "timed_out" : false }
解释一下查询结果:
hits是返回结果最重要的部分,其中total代表匹配总数,每个文档都有元数据_index库,_type类型,_id编号,_source值字段,_score代表相关性,文档按照相关性倒序返回。
_took字段代表搜索花了多少ms .
_shards告诉我们参与分片的总数和成功与失败多少个。正常情况下不会出现失败,如果遇到灾难故障比如同一分片原始数据和副本都丢失了,那么对这个分片将没有可用副本做响应,ES则会报告有失败的分片,但正确返回剩余分片结果。
time_out指示了是否查询超时,在查询的时候,你也可以设置超时时间 GET /_search?timeout=10ms 。关于超时需要特别注意的是,超时并不是停止执行查询,而是告知正在协调节点,在所设置的时间内,返回目前已查询收集到的的结果。在后台,即使结果已经返回给我们了,但是查询仍然在执行。为什么说是返回已收集到的文档,是因为在集群内搜索的时候,协调节点转发请求到其他节点的主分片或者副分片上,汇集结果。
不记得什么是协调节点了吗?Elasticsearch集群中作为客户端接入的节点叫协调节点。协调节点会将客户端请求路由到集群中合适的分片上。对于读请求来说,协调节点每次会选择不同的分片处理请求,以实现负载均衡。
一些在一个或多个特殊的索引并且在一个或者多个特殊的类型中进行搜索查询的示范:
/_search
在所有的索引中搜索所有的类型
/gb/_search
在 gb 索引中搜索所有的类型
/gb,us/_search
在 gb 和 us 索引中搜索所有的文档
/g*,u*/_search
在任何以 g 或者 u 开头的索引中搜索所有的类型
/gb/user/_search
在 gb 索引中搜索 user 类型
/gb,us/user,tweet/_search
在 gb 和 us 索引中搜索 user 和 tweet 类型
/_all/user,tweet/_search
在所有的索引中搜索 user 和 tweet 类型
分页搜索,如果每页展示 5 条结果,可以用下面方式请求得到 1 到 3 页的结果:
GET /_search?size=5 GET /_search?size=5&from=5 GET /_search?size=5&from=10
ES返回分页数据的思路是 : 比如在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。
这也说明在深度分页中,所将付出的代价,比如获取第1000页,那么每个节点查询出10010条,最后到协调节点中,从50050条抛弃50040条取10条. 补充一句 为什么每个分片可以做我们想要的查询,想要的数量和结果?是因为每个分片都是一个Lucene实例,是一个完整的搜索引擎。