按照es-ik分析器安装了ik分词器。建立索引:PUT /index_ik_test
。索引包含2个字段:content和nick,以下:php
GET index_ik_test/_mapping { "index_ik_test": { "mappings": { "fulltext": { "properties": { "content": { "type": "text", "analyzer": "ik_max_word" }, "nick": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } } }
实验环境为:单台的ElasticSearch6.3.2版本。索引配置以下:html
GET index_ik_test/_settings { "index_ik_test": { "settings": { "index": { "creation_date": "1533383757075", "number_of_shards": "5", "number_of_replicas": "1", "uuid": "JajsYmAIT0-uhm-L5xKbeA", "version": { "created": "6030299" }, "provided_name": "index_ik_test" } } } }
由此可知,ElasticSearch建立索引时,默认为5个primary shard,每一个primary shard 一个replica。node
在Kibana的Monitoring界面查看:有5个primary shard。其中有5个还没有分配的副本:git
为何有5个还没有分配的副本呢?由于是单节点的ElasticSearch,索引 index_ik_test 的每一个primary shard 都有一个副本,而primary shard 与副本 不能在同一台机器上,因为一共有5个primary shard,故存在着5个还没有分配的副本。github
该索引一共存储着5篇文档,算法
GET index_ik_test/fulltext/_search { "query": { "match_all": {} } }
查询文档以下:apache
这5篇文档中有三篇文档(文档id为 五、四、3)包含了 词 “中国”。因为采用的ik_max_word分词,所以“其中国家投资了500万”,是包含“中国”这个词的。app
每一个分片中存储的文档以下:elasticsearch
其中,shard2表明 分片:[index_ik_test][2]
,shard2上存储着 doc id为 4和6 的两篇文档。shard1 表明分片:[index_ik_test][1]
,shard1上存储着 文档id为 5 的一篇文档。其它分片存储的文档以此类推。(是否是很奇怪我是怎么知道每一个分片上存储具体哪篇文档的?这是由于:在这个演示环境中,文档数量少,我是经过不一样的查询词(好比 经过 query explian "咱们",就能知道 doc_6 存储在shard2上了)进行explian查询测试获得的。哈哈,知道这个主要是为了后面的 idf 计算分析)分布式
下面以词 "中国" 为例 来解释:query explian。执行:
GET index_ik_test/fulltext/_search { "explain": true, "query": { "match": { "content": "中国" } } }
下面从该命令的执行结果详细分析:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 0.5480699,
这代表,查询请求 scatter 到了全部的 shard (5个shard),其中有3个shard “命中了” 查询词 “中国”。这3个shard以下:
"_shard": "[index_ik_test][2]" "_shard": "[index_ik_test][1]" "_shard": "[index_ik_test][4]"
每一个shard都会计算一个score,这3个shard中,得分最大的分片是shard2 [index_ik_test][2]
,它的score是:0.5480699。所以,shard2上的返回结果排在了最前面,只是这里有个小疑问,为何score返回结果是取最大值(max_score)?
[index_ik_test][2]
:"_shard": "[index_ik_test][2]", "_node": "7MyDkEDrRj2RPHCPoaWveQ", "_index": "index_ik_test", "_type": "fulltext", "_id": "4", "_score": 0.5480699, "_source": { "content": "中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首" },
文档id 4 存储在shard2 上。该文档针对查询字符串 “中国” 计算出来的得分是0.5480699。具体的计算细节以下:
"_explanation": { "value": 0.5480699, "description": "weight(content:中国 in 0) [PerFieldSimilarity], result of:", "details": [ { "value": 0.5480699, "description": "score(doc=0,freq=1.0 = termFreq=1.0\n), product of:", "details": [ { "value": 0.6931472, "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:", "details": [ { "value": 1, "description": "docFreq", "details": [] }, { "value": 2, "description": "docCount", "details": [] } ] }, { "value": 0.7906977, "description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:", "details": [ { "value": 1, "description": "termFreq=1.0", "details": [] }, { "value": 1.2, "description": "parameter k1", "details": [] }, { "value": 0.75, "description": "parameter b", "details": [] }, { "value": 8.5, "description": "avgFieldLength", "details": [] }, { "value": 14, "description": "fieldLength", "details": [] } ] } ] } ] }
0.5480699 由idf 乘以 tfNorm 计算获得。其中 idf=0.6931472,tfNorm=0.7906977
idf
idf由公式 log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5))
计算得出。其中,docFreq=1,docCount=2,由于如上面图所示 :在shard2上,一共有2篇文档,所以docCount为2,其中只有文档id为4的这篇文档包含 "中国" 这个词,也即:词 "中国" 出现 在了一篇文档中,所以docFreq=1。
不信的话就亲自动手算算看。^~^
我这里有疑问的地方是:这里的 idf 计算公式与官网提到的计算公式有一点不同:
后来发现,在ElasticSearch6.3版本以后,字段评分算法默认是BM25算法了。
Elasticsearch allows you to configure a scoring algorithm or similarity per field. The similarity setting provides a simple way of choosing a similarity algorithm other than the default BM25, such as TF/IDF.
在BM25算法的官方文档API中发现IDF的计算公式以下:
这样也就知道了,ElasticSearch在计算Term的字段得分是,采用的是BM25算法。计算出该term在这个字段中的idf值后,再结合其余因子(好比tf、字段长度Normalization、文档长度Normalization)最终得出文档的Score。
那么tf-idf与BM25的区别是什么?tf-idf是一个term scoring method,而BM25是:给定一个查询字符串,计算该查询字符串与文档之间的得分的一种方法。文档是由一个个的term组成的,计算文档得分须要计算文档中term的得分。将tf-idf结合余弦类似度就是另一种计算查询字符串与文档之间的得分的一种方法。
BM25 is more than a term scoring method, but rather a method for scoring documents with relation to a query. Tf-idf is a term scoring method, which can be incorporated in a document scoring method using a similarity measure (say cosine).
而且BM25的理论基础是probabilistic retrieval model,而tf-idf的理论基础是 vector space model。
docFreq=1表示:"中国"这个词 只在 一篇文档中出现了。
https://lucene.apache.org/core/7_4_0/core/org/apache/lucene/search/similarities/TFIDFSimilarity.html
docFreq (the number of documents in which the term t appears)
docFreq
- the number of documents which contain the term
docCount
- the total number of documents in the collection
dcoCount=2表示:分片[index_ik_test][2]
里面一共存储了2篇文档(即doc_4 和 doc_6)。
tfNorm
tfNorm由公式(freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength))
计算。
freq ,即termFreq,应该是:term 在该分片下的全部文档中出现的频率。在这里,“中国” 在 shard2 的两篇文档中,只出现过一次
k1 ,这个参数颇有意思,默认值为1.2,是用来 平衡 词频termFreq 对评分的影响。在传统的TF评分计算过程当中,termFreq越大,计算出来的评分就越大。可是当termFreq大到必定程度时,通常是那种经常使用词(或者叫stop words),而这种词会干扰文档的评分,所以引入参数 k1 惩罚 termFreq 对评分的影响。要想了解更多,可参考这篇文章:bm25-the-next-generation-of-lucene-relevation。这里也说明,ElasticSearch6.3.2中已经采用了BM25算法做为相关性得分计算公式了。
b,从tfNorm公式可看出:用来调节字段长度对评分的影响。
avgFieldLength 值为8.5。为何是8.5呢?
在咱们的示例中,shard2 [index_ik_test][2]
中一共存储了2篇文档,一篇是doc_4,它的content字段就是"中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"。另外一篇是doc_6,它的content字段是"咱们的国家"。
对doc_6的content字段进行分析:
GET index_ik_test/_analyze { "text": ["咱们的国家"], "analyzer": "ik_max_word" } 获得的各个 token 以下: 咱们、的、国家 一共3个token
在下面的第五点fieldLength中,对doc_4的content字段进行分析获得 14个token。
所以,avgFieldLength = (14+3)/2=8.5
。14 是doc_4 content字段分词以后的token数目;3是doc_6 content字段分词以后的token数目;2 表明:有两篇文档。
由此可知:avgFieldLength 应该是:shard2分片中 content字段下全部内容 通过 ik_max_word 分词后的token 总数 除以 shard2里面的文档数目。
fieldLength,长度为14。这个是doc_4 “中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首” ik_max_word分词以后的长度。所以,fieldLength指的是 查询字段(content字段) 被分析(建立索引时指定了 ik_max_word分析器) 以后 的长度。
GET index_ik_test/_analyze { "text": ["中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"], "analyzer": "ik_max_word" } 获得的各个token以下: 中国、驻、洛杉矶、领事馆、领事、馆、遭、亚裔、男子、子枪、枪击、嫌犯、已、自首 一共 14 个token
各个参数的值以及计算过程以下:
freq=1.0
k1=1.2
b=0.75
avgFieldLength=8.5
fieldLength=14
(freq*(k1+1))/(freq+k1*(1-b+b*fieldLength/avgFieldLength))
0.7906976744186047
如今,针对shard2,咱们已经详细分析了 tfNorm 和 idf 这两个参数的计算结果。最终,shard2上的查询得分为 tfNorm*idf=0.7906977*0.6931472=0.5480699
。另外两个命中 “中国” 的分片的得分计算也相似,就不说了。
由此可看出:ElasticSearch中 tf-idf 的值 是根据单个分片来计算的,也即:以单个的shard为单位来计算 score,更具体地说:当咱们讲 某 term 一共在 文档集合中出现了多少次?这个文档集合指的是:单个分片上存储的全部文档。为何是统计单个分片上的文档/term 数量呢?这个就要从ElasticSearch的索引方式提及了。这里就简单地提一下,毕竟这不是本文的重点。
ElasticSearch中有两个不一样Level的索引,一个是:文档到分片 这个级别的索引,它讲的是 数据的分布方式,即决定把哪篇文档存储在哪一个分片上,这是经过hash文档ID的方式来实现的。采用hash方式的好处是,ElasticSearch不须要维护文档的位置信息(boundary),文档可以均匀地分布在各个shard上。ElasticSearch采用的哈希函数是:murmur3。
另外一个级别的索引是:term 到 文档的索引,俗称倒排索引,又称为:Secondary index。由于咱们的查询需求并非:给定一个docId,返回这个docId所表明的文档内容。咱们的查询需求是:给定一个 查询关键词,找出哪些docId 包含了这个 查询关键词。所以,要完成这个查询,第一步是要知道 有哪些docId 包含了 查询关键词;第二步则是:根据docId,拿到相应的文档内容。
当文档的数量不少不少时,一台机器或者说一个shard都存储不下这个倒排索引了,所以须要对倒排索引进行分割(partition)。一种分割方式是:Secondary index by Document,另外一种是:Secondary index by Term。
这种Secondary Index的分布方式(或者叫数据分布方式,这里的数据固然是倒排索引数据了)是针对每一个Partition上的文档创建一个独立的Secondary Index(倒排索引)。这种索引方式的好处是:当写入/更新文档时,只涉及到该Partition中的倒排索引,而不会修改其余的Partition中的倒排索引内容。
更具体地,以ElasticSearch举例,由于ElasticSearch就是采用Secondary index by Document。当建立索引时,是默认5个Primary shard,每一个Primary shard 一个副本(replica)。Primary shard 就至关于这里的Partition概念。当向ElasticSearch的索引中写入文档时,写请求是请求给某个Primary shard,而后在该Primary shard上构建 倒排索引(posting list),而并不须要修改 其余4个Primary shard 中的倒排索引内容。
each partition is completely separate: each partition maintains its own secondary indexes, covering only the documents in that partition. It doesn’t care what data is stored in other partitions.
所以,查询的时候,须要将查询请求发送到每个partition(shard)。为何呢?由于当咱们查询的时候,通常是输入某个词进行查询,好比输入"中国"进行查询,而因为Elasticsearch采用 secondary index by document 这种方式,各个shard 维护着本身的 secondary index,好比,在文档1 中 包含了 "中国" 这个词,而文档1 被哈希分片到 shard1中存储;文档2也包含了"中国" 这个词,可是文档2可能被哈希到另一个shard,好比shard2上存储……所以,全部包含"中国" 这个词的文档 可能分布在 Elasticsearch的全部分片中,所以查询请求须要分发到每一个shard上去,这就是所谓的Scatter 查询。固然了,因为primary shard能够设置若干个 replica,所以,将查询请求分发到 replica上,经过 replica 来扛 大量的查询请求。毕竟 index操做(将文档写入Elasticsearch)是由primary shard处理的,那将查询请求交由 replica处理,必定程度上缓解了primary shard的压力。
这种Secondary index的分布方式 是按”范围“ 来进行分布,关于数据的分布方式,可参考:[分布式系统原理介绍。好比说,对于 color 这个字段,颜色有 black、red、silver……文档中 颜色范围首字母为 [a-r] 的那些docId 存储在 Partition0 分片上。而全部 颜色范围首字母 [s-z] 的docId,则存储在Partition1分片上。 采用这种分布方式的倒排索引,一篇文档中的不一样字段 可能会在 多个Partition的字段中被索引。好比,文档893 的color 字段的内容是 silver,它在Partition1中被索引了;而文档893的make字段内容是Audi,它在Partition0中被索引了。这种索引方式的缺点显而易见:当更新/插入一篇文档时,有可能须要更新多个Partition中的倒排索引内容。 所以,查询的时候,能够只将查询请求发送到某个特定的partition(shard)。
以上内容,全是本身的理解。可能会有不少不严谨的地方。
补充说明
这篇文章记录的文档得分计算比较简单:1,它只涉及到 单个字段查询,即只查询content字段;2,查询字符串只有一个term,即:“中国”。
而现实中的查询,查询字符串可能包含多个term,而且针对索引中的多个字段查询。所以,文档得分的计算要复杂得多。
参考文献:
{ "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 3, "max_score": 0.5480699, "hits": [ { "_shard": "[index_ik_test][2]", "_node": "7MyDkEDrRj2RPHCPoaWveQ", "_index": "index_ik_test", "_type": "fulltext", "_id": "4", "_score": 0.5480699, "_source": { "content": "中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首" }, "_explanation": { "value": 0.5480699, "description": "weight(content:中国 in 0) [PerFieldSimilarity], result of:", "details": [ { "value": 0.5480699, "description": "score(doc=0,freq=1.0 = termFreq=1.0\n), product of:", "details": [ { "value": 0.6931472, "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:", "details": [ { "value": 1, "description": "docFreq", "details": [] }, { "value": 2, "description": "docCount", "details": [] } ] }, { "value": 0.7906977, "description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:", "details": [ { "value": 1, "description": "termFreq=1.0", "details": [] }, { "value": 1.2, "description": "parameter k1", "details": [] }, { "value": 0.75, "description": "parameter b", "details": [] }, { "value": 8.5, "description": "avgFieldLength", "details": [] }, { "value": 14, "description": "fieldLength", "details": [] } ] } ] } ] } }, { "_shard": "[index_ik_test][1]", "_node": "7MyDkEDrRj2RPHCPoaWveQ", "_index": "index_ik_test", "_type": "fulltext", "_id": "5", "_score": 0.2876821, "_source": { "content": "其中国家投资了500万" }, "_explanation": { "value": 0.2876821, "description": "weight(content:中国 in 0) [PerFieldSimilarity], result of:", "details": [ { "value": 0.2876821, "description": "score(doc=0,freq=1.0 = termFreq=1.0\n), product of:", "details": [ { "value": 0.2876821, "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:", "details": [ { "value": 1, "description": "docFreq", "details": [] }, { "value": 1, "description": "docCount", "details": [] } ] }, { "value": 1, "description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:", "details": [ { "value": 1, "description": "termFreq=1.0", "details": [] }, { "value": 1.2, "description": "parameter k1", "details": [] }, { "value": 0.75, "description": "parameter b", "details": [] }, { "value": 7, "description": "avgFieldLength", "details": [] }, { "value": 7, "description": "fieldLength", "details": [] } ] } ] } ] } }, { "_shard": "[index_ik_test][4]", "_node": "7MyDkEDrRj2RPHCPoaWveQ", "_index": "index_ik_test", "_type": "fulltext", "_id": "3", "_score": 0.2876821, "_source": { "content": "中韩渔警冲突调查:韩警平均天天扣1艘中国渔船" }, "_explanation": { "value": 0.2876821, "description": "weight(content:中国 in 0) [PerFieldSimilarity], result of:", "details": [ { "value": 0.2876821, "description": "score(doc=0,freq=1.0 = termFreq=1.0\n), product of:", "details": [ { "value": 0.2876821, "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:", "details": [ { "value": 1, "description": "docFreq", "details": [] }, { "value": 1, "description": "docCount", "details": [] } ] }, { "value": 1, "description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:", "details": [ { "value": 1, "description": "termFreq=1.0", "details": [] }, { "value": 1.2, "description": "parameter k1", "details": [] }, { "value": 0.75, "description": "parameter b", "details": [] }, { "value": 14, "description": "avgFieldLength", "details": [] }, { "value": 14, "description": "fieldLength", "details": [] } ] } ] } ] } } ] } }