按照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": [] } ] } ] } ] } } ] } }