ES集群内原理&相关性以及分数score计算
1. 集群相关
一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
当一个节点被选举成为主节点时,它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
作为用户,我们可以将请求发送到 集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。
1. 集群健康
Elasticsearch 的集群监控信息中包含了许多的统计数据,其中最为重要的一项就是 集群健康 , 它在 status 字段中展示为 green 、 yellow 或者 red 。
例如启动一个默认的es服务器之后查看:
$ curl http://127.0.0.1:9200/_cluster/health?pretty % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 467 100 467 0 0 29187 0 --:--:-- --:--:-- --:--:-- 29187{ "cluster_name" : "my-application", "status" : "green", "timed_out" : false, "number_of_nodes" : 1, "number_of_data_nodes" : 1, "active_primary_shards" : 0, "active_shards" : 0, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 0, "delayed_unassigned_shards" : 0, "number_of_pending_tasks" : 0, "number_of_in_flight_fetch" : 0, "task_max_waiting_in_queue_millis" : 0, "active_shards_percent_as_number" : 100.0 }
解释: status 字段指示着当前集群在总体上是否工作正常。它的三种颜色含义如下:
green:所有的主分片和副本分片都正常运行。
yellow:所有的主分片都正常运行,但不是所有的副本分片都正常运行。
red:有主分片没能正常运行
2. 添加索引
索引实际上是指向一个或者多个物理 分片 的 逻辑命名空间 。
1. 分片:
一个 分片 是一个底层的工作单元 ,它仅保存了全部数据中的一部分。一个分片是一个 Lucene 的实例,它本身就是一个完整的搜索引擎。我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。
Elasticsearch 是利用分片将数据分发到集群内各处的。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。
一个分片可以是 主 分片或者 副本 分片。 索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。一个索引必须创建主分片,副本分片可以没有。
分片和主流关系型数据库的表分区的概念有点类似。
2. 副本:
一个副本分片只是一个主分片的拷贝。副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。
如果主分片有3个,那么一个副本replica就对应有1X3=3个replica shard副本分片。副本分片数量计算公式 = 副本数量repilca num X 主分片数量primary shard num
在索引建立的时候就已经确定了主分片数,但是副本分片数可以随时修改。也就是说分片数量不允许修改,副本数量可以修改。
3. 分片与副本的关系:
每个主分片(primary shard)不会和副本分片(replica shard)存在于同一个节点中,有效的保证es的数据高可用性。
例如1:比如一个索引有3个分片和1副本,那么一共就有3*2=6个分片,3个是主分片,3个是副本分片,每个主分片都会对应一个副本分片。
例如2:只有2个节点,但是有3个分片和2个副本,这样的情况就会导致分片无法完全分配,因为主分片和副本分片不能存在于同一个节点中。
4. 测试:
(1)在包含一个节点的集群内创建名为 blogs 的索引。分配3个主分片和一份副本(每个主分片拥有一个副本分片):
再次查看集群健康信息:
{ "cluster_name": "my-application", "status": "yellow", "timed_out": false, "number_of_nodes": 1, "number_of_data_nodes": 1, "active_primary_shards": 3, "active_shards": 3, "relocating_shards": 0, "initializing_shards": 0, "unassigned_shards": 3, "delayed_unassigned_shards": 0, "number_of_pending_tasks": 0, "number_of_in_flight_fetch": 0, "task_max_waiting_in_queue_millis": 0, "active_shards_percent_as_number": 50.0 }
集群的健康状况为 yellow 则表示全部 主 分片都正常运行(集群可以正常服务所有请求),但是 副本 分片没有全部处在正常状态。 实际上,所有3个副本分片都是 unassigned —— 它们都没有被分配到任何节点。 在同一个节点上既保存原始数据又保存副本是没有意义的,因为一旦失去了那个节点,我们也将丢失该节点上的所有副本数据。
当前我们的集群是正常运行的,但是在硬件故障时有丢失数据的风险。
上面的集群图可以表示为:
(2)再次启动一个节点Node2加入到节点后再次查看健康信息:
当第二个节点加入到集群后,3个 副本分片 将会分配到这个节点上——每个主分片对应一个副本分片。 这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。
所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们既可以从主分片又可以从副本分片上获得文档。
{ "cluster_name": "my-application", "status": "green", "timed_out": false, "number_of_nodes": 2, "number_of_data_nodes": 2, "active_primary_shards": 3, "active_shards": 6, "relocating_shards": 0, "initializing_shards": 0, "unassigned_shards": 0, "delayed_unassigned_shards": 0, "number_of_pending_tasks": 0, "number_of_in_flight_fetch": 0, "task_max_waiting_in_queue_millis": 0, "active_shards_percent_as_number": 100.0 }
status变为green。这表示所有6个分片(包括3个主分片和3个副本分片)都在正常运行。
上面的集群图可以表示为:
(3) 水平扩容: 我们再次启动第三个节点 Node3
Node 1 和 Node 2 上各有一个分片被迁移到了新的 Node 3 节点,现在每个节点上都拥有2个分片,而不是之前的3个。 这表示每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片的性能将会得到提升。
分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有6个分片(3个主分片和3个副本分片)的索引可以最大扩容到6个节点,每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。
(4)增加副本数量
查看集群信息:
{ "cluster_name": "my-application", "status": "green", "timed_out": false, "number_of_nodes": 3, "number_of_data_nodes": 3, "active_primary_shards": 3, "active_shards": 9, "relocating_shards": 0, "initializing_shards": 0, "unassigned_shards": 0, "delayed_unassigned_shards": 0, "number_of_pending_tasks": 0, "number_of_in_flight_fetch": 0, "task_max_waiting_in_queue_millis": 0, "active_shards_percent_as_number": 100.0 }
其集群状态图如下:
(5) 关闭主节点:
我们尝试将主节点关闭,集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点: Node 2。
在我们关闭 Node 1 的同时也失去了主分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为 red :不是所有主分片都在正常工作。
幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片, 此时集群的状态将会为 yellow 。 这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。
为什么我们集群状态是 yellow 而不是 green 呢? 虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应2份副本分片,而此时只存在一份副本分片。 所以集群不能为 green 的状态,不过我们不必过于担心:如果我们同样关闭了 Node 2 ,我们的程序 依然 可以保持在不丢任何数据的情况下运行,因为 Node 3 为每一个分片都保留着一份副本。
集群健康信息如下:
{ "cluster_name": "my-application", "status": "yellow", "timed_out": false, "number_of_nodes": 2, "number_of_data_nodes": 2, "active_primary_shards": 3, "active_shards": 6, "relocating_shards": 0, "initializing_shards": 0, "unassigned_shards": 3, "delayed_unassigned_shards": 0, "number_of_pending_tasks": 0, "number_of_in_flight_fetch": 0, "task_max_waiting_in_queue_millis": 0, "active_shards_percent_as_number": 66.66666666666666 }
状态图如下:
2. 相关性
1.什么是相关性
返回结果是按相关性_score倒序排列的(如果我们指定了排序字段就会按照排序字段进行排序)。 但是什么是相关性? 相关性如何计算?
每个文档都有相关性评分,用一个正浮点数字段 _score
来表示 。 _score
的评分越高,相关性越高。
查询语句会为每个文档生成一个 _score 字段。评分的计算方式取决于查询类型不同的查询语句用于不同的目的: fuzzy 查询会计算与关键词的拼写相似程度,terms 查询会计算 找到的内容与关键词组成部分匹配的百分比,但是通常我们说的 relevance 是我们用来计算全文本字段的值相对于全文本检索词相似程度的算法。
Elasticsearch 的相似度算法被定义为检索词频率/反向文档频率, TF/IDF (Term Frequency&Inverse Document Frequency),包括以下内容:
(1)检索词频率
检索词在该字段出现的频率?出现频率越高,相关性也越高。 字段中出现过 5 次要比只出现过 1 次的相关性高。
(2)反向文档频率
每个检索词在索引中出现的频率?频率越高,相关性越低。检索词出现在多数文档中会比出现在少数文档中的权重更低。
(3)字段长度准则
字段的长度是多少?长度越长,相关性越低。 检索词出现在一个短的 title 要比同样的词出现在一个长的 content 字段权重更大。
单个查询可以联合使用 TF/IDF 和其他方式,比如短语查询中检索词的距离或模糊查询里的检索词相似度。
相关性并不只是全文本检索的专利。也适用于 yes|no 的子句,匹配的子句越多,相关性评分越高。
如果多条查询子句被合并为一条复合查询语句,比如 bool 查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。
例如:
(1) explain分析返回的score:
GET /_search?explain { "query" : { "match" : { "content" : "java 和 JS" }} }
结果: 可看到是按照score分数降序排序。
{ "took" : 53, "timed_out" : false, "_shards" : { "total" : 16, "successful" : 16, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 5, "relation" : "eq" }, "max_score" : 1.3862942, "hits" : [ { "_index" : "news", "_type" : "_doc", "_id" : "NQILB3QBfZgDx-b1yeNE", "_score" : 1.3862942, "_source" : { "creator" : "creator3", "createTime" : "2020-08-19T14:07:27.034Z", "type" : "js", "title" : "js记录", "content" : "这里是js记录", "amount" : 6 } }, { "_index" : "news", "_type" : "_doc", "_id" : "_J4LB3QBhQI7S8XqzLjT", "_score" : 0.87546873, "_source" : { "creator" : "creator6", "createTime" : "2020-08-19T14:07:27.946Z", "type" : "java", "title" : "java记录", "content" : "这里是java记录", "amount" : 7 } }, { "_index" : "news", "_type" : "_doc", "_id" : "NAILB3QBfZgDx-b1xOMr", "_score" : 0.87546873, "_source" : { "creator" : "creator1", "createTime" : "2020-08-19T14:07:25.469Z", "type" : "java", "title" : "java记录", "content" : "这里是java记录", "amount" : 5 } }, { "_index" : "news", "_type" : "_doc", "_id" : "_Z4LB3QBhQI7S8Xqzrhk", "_score" : 0.6931471, "_source" : { "creator" : "creator8", "createTime" : "2020-08-19T14:07:28.346Z", "type" : "js", "title" : "js记录", "content" : "这里是js记录", "amount" : 2 } }, { "_index" : "news", "_type" : "_doc", "_id" : "-54LB3QBhQI7S8Xqyrjs", "_score" : 0.6931471, "_source" : { "creator" : "creator4", "createTime" : "2020-08-19T14:07:27.459Z", "type" : "es", "title" : "js记录", "content" : "这里是js记录", "amount" : 3 } } ] } }
(2) explain=true 可以返回来自于哪个节点哪个分片上的信息,另外词频率和文档频率是在每个分片中计算出来的,而不是每个索引中。
GET /_search?explain=true { "query" : { "match" : { "content" : "面试" }} }
结果:
{ "took" : 1043, "timed_out" : false, "_shards" : { "total" : 16, "successful" : 16, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 1, "relation" : "eq" }, "max_score" : 1.1220688, "hits" : [ { "_shard" : "[news][0]", "_node" : "cu2o5KFkT0iE5bB2IegMOg", "_index" : "news", "_type" : "_doc", "_id" : "2VQSMHQBKZKMrWbM2NPS", "_score" : 1.1220688, "_source" : { "creator" : "creator0", "createTime" : "2020-08-27T13:19:33.988Z", "type" : "面试笔记", "title" : "2018年面试记录", "content" : "面试心酸啊" }, "_explanation" : { ① "value" : 1.1220688, "description" : "weight(content:面试 in 0) [PerFieldSimilarity], result of:", "details" : [ { ② "value" : 1.1220688, "description" : "score(freq=1.0), computed as boost * idf * tf from:", "details" : [ { "value" : 2.2, "description" : "boost", "details" : [ ] }, { ③ "value" : 0.98082924, "description" : "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:", "details" : [ { "value" : 1, "description" : "n, number of documents containing term", "details" : [ ] }, { "value" : 3, "description" : "N, total number of documents with field", "details" : [ ] } ] }, { ④ "value" : 0.52000004, "description" : "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:", "details" : [ { "value" : 1.0, "description" : "freq, occurrences of term within document", "details" : [ ] }, { "value" : 1.2, "description" : "k1, term saturation parameter", "details" : [ ] }, { "value" : 0.75, "description" : "b, length normalization parameter", "details" : [ ] }, { "value" : 3.0, "description" : "dl, length of field", "details" : [ ] }, { "value" : 4.3333335, "description" : "avgdl, average length of field", "details" : [ ] } ] } ] } ] } } ] } }
解释:它提供了 _explanation ,每个入口都包含一个 description 、 value 、 details 字段,它分别告诉你计算的类型、计算结果和任何我们需要的计算细节。
① 关于计算的总结
②检索词频率:检索词 `面试` 在这个文档的 `content` 字段中的出现次数。
③反向文档频率:检索词 `面试` 在索引上所有文档的 `content` 字段中出现的次数。
④字段长度准则:在这个文档中, `面试` 字段内容的长度 -- 内容越长,值越小。
2. 相关度控制:
1. 查询提升权重
bool 查询可以组合任意其他的查询,以及其他 bool 查询。假设想要查询关于 “full-text search(全文搜索)” 的文档,但我们希望为提及 “Elasticsearch” 或 “Lucene” 的文档给予更高的 权重 ,这里 更高权重 是指如果文档中出现 “Elasticsearch” 或 “Lucene” ,它们会比没有的出现这些词的文档获得更高的相关度评分 _score ,也就是说,它们会出现在结果集的更上面。
GET /_search { "query": { "bool": { "must": { "match": { "content": { "query": "full text search", "operator": "and" } } }, "should": [ { "match": { "content": "Elasticsearch" }}, { "match": { "content": "Lucene" }} ] } } }
content 字段必须包含 full 、 text 和 search 所有三个词。
如果 content 字段也包含 Elasticsearch 或 Lucene ,文档会获得更高的评分 _score 。
但是如果我们想让包含 Lucene 的有更高的权重,并且包含 Elasticsearch 的语句比 Lucene 的权重更高,可以通过指定 boost 来控制任何查询语句的相对的权重, boost 的默认值为 1 ,大于 1 会提升一个语句的相对权重。所以下面重写之前的查询:
GET /_search { "query": { "bool": { "must": { "match": { "content": { "query": "full text search", "operator": "and" } } }, "should": [ { "match": { "content": { "query": "Elasticsearch", "boost": 3 } }}, { "match": { "content": { "query": "Lucene", "boost": 2 } }} ] } } }
boost 参数被用来提升一个语句的相对权重( boost 值大于 1 )或降低相对权重( boost 值处于 0 到 1 之间),但是这种提升或降低并不是线性的,换句话说,如果一个 boost 值为 2 ,并不能获得两倍的评分 _score 。
java中测试:
(1) 不改变权重
BoolQueryBuilder filter = QueryBuilders.boolQuery()
.must(QueryBuilders.termsQuery("ordernum", "order4", "order5", "order6"));
结果:
查询结果有:3 hits条
1.0 {"amount":4,"createTime":"2020-08-27T13:42:41.559Z","description":"订单描述4","orderid":5,"ordernum":"order4","username":"zhangsan4"}
1.0 {"amount":5,"createTime":"2020-08-27T13:42:42.662Z","description":"订单描述5","orderid":6,"ordernum":"order5","username":"zhangsan0"}
1.0 {"amount":6,"createTime":"2020-08-27T13:42:43.932Z","description":"订单描述6","orderid":7,"ordernum":"order6","username":"zhangsan1"}
(2) 用should提升username为zhangsan0增加权重1F,username为zhangsan1增加权重0.1F。也就是实现zhangsan0-1-4的排序
BoolQueryBuilder filter = QueryBuilders.boolQuery() .must(QueryBuilders.termsQuery("ordernum", "order4", "order5", "order6")) .should(QueryBuilders.termQuery("username", "zhangsan0")) // 默认是1F .should(QueryBuilders.termQuery("username", "zhangsan1").boost(0.1F));
结果:
查询结果有:3 hits条
2.4816046 {"amount":5,"createTime":"2020-08-27T13:42:42.662Z","description":"订单描述5","orderid":6,"ordernum":"order5","username":"zhangsan0"}
1.1481605 {"amount":6,"createTime":"2020-08-27T13:42:43.932Z","description":"订单描述6","orderid":7,"ordernum":"order6","username":"zhangsan1"}
1.0 {"amount":4,"createTime":"2020-08-27T13:42:41.559Z","description":"订单描述4","orderid":5,"ordernum":"order4","username":"zhangsan4"}
2. 其他还有随机评分等其他方法。