[toc] 想想我們漏了什麼 這篇文章已經是第四篇了,前面很多都只是講了基礎的使用,沒有講到內層的原理,所以這裡就要補一下原理知識了。 回顧 先回顧一下我們前面學過了什麼,再想想我們漏了什麼。 第一篇我們認識了ElasticSearch,大概知道了ElasticSearch的作用 搜索,也瞭解了一些 ...
目錄
想想我們漏了什麼
這篇文章已經是第四篇了,前面很多都只是講了基礎的使用,沒有講到內層的原理,所以這裡就要補一下原理知識了。
回顧
先回顧一下我們前面學過了什麼,再想想我們漏了什麼。
第一篇我們認識了ElasticSearch,大概知道了ElasticSearch的作用--搜索,也瞭解了一些倒排索引和分詞器的知識(需要補充),然後學習瞭如何搭建環境(需要補充一下關於基礎的集群知識),然後講了一些基礎的ElasticSearch概念(重新講述,加深瞭解)。
第二篇講了索引和文檔的CRUD,講創建索引的時候,沒有講mapping,講文檔的時候沒有講文檔的數據類型和元數據,這些都需要補充。
第三篇講了文檔的搜索,主要是語法方面的問題,但相關度分數是怎麼計算出來的,我們並沒有講。
補回
所以下麵將對前面漏了的基礎知識進行補充:
- Json文檔的數據格式?【我們之前只會弄一個簡單的json,而不知道裡面有什麼區別】
- 集群的基礎認識?【我們之前說了ElasticSearch是一個分散式的系統,但我們之前只講瞭如何啟動,如何使用kibana操作ElasticSearch,並沒有講ElasticSearch的集群式怎麼建立的】
- 節點是怎麼提供服務的?【我們之前只知道直接發請求,這個請求ElasticSearch是怎麼處理的,我們並不知道】
- 索引的mapping?【之前創建索引的時候,沒有說清楚mapping,mapping受文檔的數據類型影響,mapping影響查詢方式和分詞方式】
- 相關度分數score是怎麼算出來的?【我們知道score是相關度分數,但我們並不知道這個是怎麼算出來的】
- 分詞器的原理【第一篇講了一點,現在加深】
集群的建立
我們說了,ElasticSearch是分散式的,但我們之前只說瞭如何啟動ElasticSearch,然後使用Kibana操作ElasticSearch,並沒有講ElasticSearch的集群式如何建立的,所以下麵將會講一下集群的知識。但如何管理集群會單獨做成一篇來講。
集群發現機制
首先,每一個ElasticSearch服務端就是一個集群節點,當我們啟動一個elasticsearch時,就相當於啟動了一個集群節點。
那麼,多個節點之間如何建立聯繫呢?當我們啟動一個節點的時候,這個節點會自動創建一個集群,集群的名稱預設為elasticsearch,當我們再次啟動一個節點的時候,它首先會嘗試尋找名稱為elasticsearch的集群,然後加入其中,(所有的都是先嘗試尋找,沒有再自己創建)。【集群名稱可以自行配置,下麵講】
當節點加入到集群中後,這個集群就算是建立起來了。
配置文件
上面講了集群名稱可以自行配置,下麵講一下怎麼配置。
ElasticSearch的配置文件是config/elasticsearch.yml,裡面有以下幾個配置項:
【左邊是配置項,右邊是值,修改配置也就是修改右邊的值】
- 集群名稱:
cluster.name: my-application
- 索引存儲位置:
path.data: /path/to/data
- 日誌存儲位置:
path.logs: /path/to/logs
- 綁定的IP地址:
network.host: 192.168.0.1
- 綁定的http埠:
http.port: 9200
- 是否允許使用通配符來標識索引:
action.destructive_requires_name
,true為禁止。
健康狀態
當集群建立後,我們可以使用命令來查看集群狀態:
GET /_cluster/health
:以json方式顯示集群狀態
GET /_cat/health?v
:行列式顯示集群狀態
返回結果解析:- epoch:時間戳
- timestamp:時間
- cluster:集群名
- status:狀態
- 集群狀態解析看下麵。
- node.total:集群中的節點數
- node.data:集群中的數據節點數
- shards:集群中總的分片數量
- pri:主分片數量
- relo:副本分片數量
- init:初始化中的分片數?【不確定,英文是這樣的:number of initializing nodes 】
- unassign:沒有被分配的分片的數量
- pending_tasks:待處理的任務數
- max_task_wait_time:最大任務等待時間
- activeds_percent:active的分片數量
集群狀態解析:
集群狀態受當前的primary shared和replica shared的數量影響。
當每個索引的primary shared和replica shared都是active的時候,狀態為green;【什麼是active?shard是位於節點上的,一個shard被分配到了運行的節點上,那麼此時就是active的,如果shard沒有分配到節點上,那麼就是inactive】
當每個索引的primary shared都是active的,但replica shared不完全是active的時候,狀態為yellow;
當每個索引的primary shared不完全是active的時候,此時發生了數據丟失,狀態為red。
為什麼現在是yellow?
當我們第一次啟動的時候,kibana會預設為我們創建一個名為kibana的索引,這個索引的primary shard和replica shard都為1,由於此時只有一個節點,所以會優先分配primary shard,而忽略replica shard(不要忘了基於備份的安全性的shard排斥考慮:主分片和副本分片不能位於同一個節點上),所以此時就符合了“每個索引的primary shared都是active的,但replica shared不全是active的”,所以此時是yellow.
你可以嘗試啟動多一個elasticsearch節點來調整狀態。【當然,我覺得你學到這裡了,此時replica shard的數量可能已經變了,所以這裡只啟動多一個可能已經不夠了,具體的下麵“分片的管理”講】
補充:
- 集群的知識還有很多,這裡講集群只是講了個開頭,對於深層的集群管理並沒有涉及,將留到後面集群管理篇講。
小節總結
上面講了當啟動了一個elasticsearch之後,這個節點先尋找集群,沒有的話就自己創建一個,然後後續的其他節點也會加入這個節點,從而自動實現了自動集群化部署。
分片的管理
之前講了一些分片的知識,但並沒有具體用到,可能有些人已經忘了這個概念。這裡重新講一下。
梳理
文檔的數據是存儲到索引中的,而索引的數據是存儲在分片上的,而分片是位於節點上的。
分片shard有主分片primary shard和複製分片replica shard兩種,其中主分片是主要存儲的分片,可以進行讀寫操作;副本分片是主分片的備份,可以進行讀操作(不支持寫操作)。
查詢可以在主分片或副本分片上進行查詢,這樣可以提供查詢效率。【但數據的修改只發生在主分片上。】
分片是面向索引的,分片上的數據屬於同一個索引,在我們創建索引的時候,可以指定主分片和副本分片的數量,預設是5個主分片,5個副本分片。
索引的replica shard的數量可以修改,但primary shard的數量不可以修改。【一個Primary Shard可以有多個Replica Shard,預設創建是1個。】
修改replica shard數量:
PUT /douban/_settings
{
"number_of_replicas":1
}
為了保證數據的不丟失,通常來說Replica Shard不能與其對應的Primary Shard處於同一個節點中。【因為萬一這個節點損壞了,那麼存儲在這個節點上的原數據(primary shard)和備份數據(replica shard)就全部丟失了】,但是可以和其他primary shard的replica shard放在一起
replica shard是primary shard的副本分片,負責承載一定的讀請求,當primary shard都掛掉後,其中一個replica shard會變成primary shard來維持寫功能。
分片的均衡分配
分片是分配到節點上的,但它會預設地均衡分配。
所謂均衡分配,以情況舉例:【priX代表主分片XX,repX代表副本分片XX】
1.索引有1個priA,1個repA,但當前只有一個節點A,那麼priA分配到A節點;repA沒有分配。優先分配主分片。
2.索引有1個priA,1個repA,當前有兩個節點A和B,那麼priA分配到A節點;repA分配到B節點(priA分配到B節點也有可能)。
3.索引當前有2個priA,4個repA,有3個節點,那麼現在每台節點上有兩個分片(但要考慮主分片不能與自己的副本分片同在一個節點上,在下麵的“ 主副分片的排斥”中由一個例子)。
【當後續新增節點的時候,會自動再次重新均衡分配。】
主副分片的排斥
考慮到備份的安全性,不應該讓主分片和副本分片位於一個節點上,不然可能完全丟失數據。(比如你為了避免丟失鑰匙,你給你家的門準備兩條鑰匙,你有一個錢包和一個背包,結果你把兩條鑰匙都放到錢包中,某天你錢包丟了,於是你回不了家了。。。所以你應該把鑰匙分開放。)
對應的排斥情況就是:
1.索引有1個priA,1個repA,但當前只有一個節點A,那麼priA分配到A節點;repA沒有分配。
2.索引有1個priA,1個repA,當前有兩個節點A和B,那麼priA分配到A節點;repA分配到B節點(priA分配到B節點也有可能)。
【請註意,主分片與副本分片不能在一起,但副本分片和副本分片能存放在一起】
在上面的“分片的均衡分配”沒有提到主副分片的排斥的問題,下麵再舉一個例子【下述的分配結果還有可能是其他的】:
容錯性:
既然提到了備份的安全性,那麼就不得不提一下容錯性了。節點是有可能宕機的,宕機後,那麼這個節點的數據起碼會暫時性的丟失,那麼對於不同情況下,最多可以宕機多少個節點呢?下麵舉例:【pri = x代表主分片數量為X,rep是副本分片】
1.如果你只有一個節點,那麼容錯性為0,你不能宕機,宕機不完全意味著數據完全丟失,但暫停服務還是有的。
2.如果你有兩個節點,pri = 2,rep = 2,那麼此時分片分配應該是[P1R2, P2R1],此時容錯性為一個,因為某個節點上有完整的兩個分片的數據【此處一提,假設丟失了P2R1,那麼R1R2中的R2會升級成primary shard來保持寫功能】
3.後面的自己嘗試算一下吧!要綜合均衡分片和排斥性來考慮。
數據路由
每一個document會存儲到唯一的一個primary shared和其副本分片replica shard 中,我們是怎麼根據ID來知道這個document存儲在哪個分片的呢?
ElasticSearch會自動根據document的id值來進行計算,最終得出一個小於分片數量的數值,這個數值就是分片在ElasticSearch中的序號,最終得出應該存儲到哪個分片上。
類似原理舉例,假設我現在有4個主分片,那麼我給它們標序號0,1,2,3。現在進來一個ID為15的數據,我經過一系列計算之後,15入參之後假設得到243這個數值,然後我們使用243來對4求餘,餘數是3,所以就把這個數據放在序號為3的分片上。
依據這個原理,存儲數據的時候就知道把數據放在哪個分片上;讀取數據的時候也知道從哪個分片上讀取數據。
【稍微提一下,在ElasticSearch全文搜索完成之後,此時內部得到的是ID組成的數組,內部再會根據ID來查找數據】
對於集群健康狀態的影響
上面說了,集群狀態受當前的primary shared和replica shared的數量影響。
- 當每個索引的primary shared和replica shared都是active的時候,狀態為green;
- 當每個索引的primary shared都是active的,但replica shared不完全是active的時候,狀態為yellow;
當每個索引的primary shared不完全是active的時候,此時發生了數據丟失,狀態為red。
現在學會了“分片的均衡分配”和“主副分片的排斥”後,你應該能分析出要啟動多少個節點才能改變集群的健康狀態。
下麵舉個例子,以變green為例:當前節點中只有一個索引A,索引A當前有2個priA,4個repA,那麼此時需要兩個節點才可以變green,
一個的時候就不說了,此時所有副本分片都inactive;兩個節點的時候,由於每個主分片有兩個副本分片,此時的分配是[P1-R11-R12,P2-R21-R22]
小節總結:
本節重新講了主分片和副本分片的功能,主分片和副本分片都有存儲數據的功能,一個索引的數據平分到多個主分片上,副本分片拷貝對應主分片的數據;然後講了ElasticSearch對於分片的自動均衡分配,和主分片和副本分片不能存儲在一起的問題。然後講了ElasticSearch如何根據ID來判斷數據存儲在哪個分片上。最後講了節點上分片的分配對於集群健康狀態的影響。
請求的處理
提供服務
- 節點是提供服務的單位,請求是發到節點上的。
- 節點接收到請求後,會對請求進行處理:
- 讀請求:
- 當接受讀請求,如果是根據ID來獲取數據的讀請求,那麼它會選擇有這個ID的數據的分片來獲取數據(可以根據ID來判斷該分片上是否有這份數據);如果是搜索類的請求,首先會根據索引表來搜索,得出相關文檔的ID,然後根據ID從這個索引的相關分片來獲取數據(如果有2個pri,2個rep,那麼搜索的分片可能是p1r2、p2r1、p1p2,r1r2,只要能完整地獲取索引的所有數據即可)。
- 寫請求:
- 當接收寫請求,節點會選擇根據ID來計算應該把這個數據存儲到哪個主分片上,然後通過主分片來修改數據,主分片修改完成後,會將數據同步到副本分片上。【由於存在一個同步過程,同步完成之前,某個分片上可能不存在剛剛插入的數據,但概率較小,因為同步是極快的(NRT)】
- 讀請求:
- 【再次提醒,主分片有讀和寫的能力,副本分片只可以讀,所以數據的更新都發生在主分片上】
協調節點cordination node
索引的數據是存儲在節點上的,當一個請求發到節點上的時候,可能這個節點上並沒有這個索引的數據,那麼這個時候就需要把請求轉發給另一個節點了,這時候原本的節點就是一個“協調節點”。
請求分發的負載均衡
一個索引存儲在多個主分片和副本分片上,索引的數據會平均分配到每一個主分片中,然後每一個副本分片拷貝對應主分片的數據。
對於數據存儲,數據是存儲在分片上的,而分片位於節點上,在上面說了分片會均衡分配到每一個節點上,這樣也保證了節點上的數據量是平均的。除此之外,對於讀取某一個主分片及其副本分片上的數據的時候,會使用輪詢演算法來將讀請求平均分配(大概意思就是,假設現在對於主分片1有三個副本分片,那麼總數為4,假設分別編號1、2、3、4,那麼可能地,第一次請求交給了1,那麼第二次請求要交給2,第三次要交給3。。。以此類推,超過4則從頭開始)。
補充:
- 除了上面的內容,還有一些與集群比較相關的內容,比如某節點宕機後會發生什麼。這些會留到集群管理篇再講。
小節總結
本節簡單說了一下讀寫操作的流程,ID類的讀請求直接根據ID來查找文檔,搜索類的讀請求先搜索索引表,再根據ID來查找文檔;寫數據請求會根據ID來計算應該把這個數據存儲到哪個主分片上,然後通過主分片來修改數據;最後講了一下對於請求分發的負載均衡,一方面通過數據量的平均分配來均衡,一方面使用輪詢演算法來降低單個分片的處理壓力。
文檔的元數據
你在查詢文檔的時候,你可能看到返回結果中有如下的內容:
上面的幾個首碼有_
的就是元數據。
_index
:代表當前document存放到哪個index中。_type
:代表當前document存放在index的哪個type中。_id
:代表document的唯一標識,與index和type一起,可以唯一標識和定位一個document。【在前面我們都是手動指定的,其實可以不手動指定,那樣會隨機產生要給唯一的字元串作為ID】_version
:是當前document的版本,這個版本用於標識這個document的“更新次數”(新建、刪除、修改都會增加版本)_source
:返回的結果是查詢出來的當前存儲在索引中的完整的document數據。之前在搜索篇中講到了,我們可以使用_source
來指定返回docuemnt的哪些欄位。
元數據與具體的文檔數據無關,每一個文檔都有這些數據。
文檔的數據類型
對於web中的json,數據格式主要有字元串
、數值
、數組
和{}
這幾種。
比如:
{
"name": "neo",
"age": 18,
"tool": ["clothes", "computer", "gun"],
"gf": {
"feature": "beauty"
}
}
ElasticSearch並不是這樣的,因為它要考慮分詞,就算是字元串,它也要考慮裡面的數據是不是日期類型的,日期類型通常不會分詞。
數據類型
ElasticSearch主要有以下這幾種數據類型:
- 字元類:
- text:是存儲字元串的類型,在elasticsearch中存儲會分詞的字元串數據一般用text
- keyword:也是存儲字元串的類型,在elasticsearch中用於存儲不會分詞的、結構化的字元串數據
string
:string在5.x之前可以使用,現在已被text和keyword取代。
- 整數類型:
- integer
- long
- short
- byte
- 浮點數類型:
- double
- float
- 日期類型:date
- 布爾類型:boolean
- 數組類型:array
- 對象類型:object
【除了上述的類型之外,還有一些例如half_float、scaled_float、binary、ip等等類型,由於不是非常基礎的內容,所以這裡不講,有興趣的可以自查。】
補充:
- 數據類型本身是沒有多少重要的知識點,重要的是與數據類型緊密相關的mapping。因為mapping存儲的就是索引的結構信息,下麵小節將講述mapping。
mapping
mapping負責維護index中文檔的結構,包括文檔的欄位名、欄位數據類型、分詞器、欄位是否進行分詞等。這些屬性會對我們的搜索造成影響。
dynamic mapping
在前面,其實我們都沒有定義過mapping,直接就是插入數據了。其實這時候ElasticSearch會幫我們自動定義mapping,這個mapping會依據文檔的數據來自動生成。
此時,如果數據是字元串的,會認為是text類型,並且預設進行分詞;如果數據是日期類型類(字元串裡面的數據是日期格式的),那麼這個欄位會認為是date類型的,是不分詞的;如果數據是整數,那麼這個欄位會認為是long類型的數據;如果數據是小數,那麼這個欄位會認為是float類型的;如果是true或者false,會認為是boolean類型的。
舉例:
我們插入以下數據【如果你用了這個索引,那麼可以自定義一個索引,避免我隨意創建的數據污染了你的測試數據】:
PUT /test/person/1
{
"name":"suke",
"age":18,
"tall":178.5,
"birthdate":"2018-01-01"
}
然後查看索引,其中mapping定義了我們剛剛定義的欄位的信息:
GET /test
創建mapping
【之前在第二篇中有提到,可以通過查看索引來查看mapping】
語法:
【[]代表裡面的內容是可選的,非必須的】
PUT /index/
{
"settings": {
"index": {
"number_of_shards": 3,
"number_of_replicas": 1
}
},
"mappings": {
"type名": {
"properties": {
"欄位名": {
"type": "數據類型",
// 是否索引,不索引則不將這個欄位列入索引表,無法對這個欄位進行搜索
["index": "是否索引,值為true或者fasle",]
// 是否新舊keyword保留原始數據
["fields": {
"keyword": {
"type": "keyword",【這個是保留原始數據採用的數據類型,也可以使用text,一般用keyword】
"ignore_above": 256【超過多少字元就忽略,不建立keyword】
}
},]
// (選擇什麼分詞器來對這個欄位進行分詞)
["analyzer": "分詞器名稱"
,]
}
}
}
}
}
【內容補充:在舊版中,index的值有not_analyzed這樣的值,現在只有true或false,它的原意是不分詞,現在是不索引,在舊版中,不分詞則會保留原始數據,在新版中使用keyword來保留原始數據】
舉例:
1.創建一個mapping,只定義數據類型:【定義了每個欄位的數據類型後,插入數據的時候並沒有說要嚴格遵循,數據類型的作用是提前聲明欄位的數據類型,例如在之前第二篇說type的時候,就提到了多個type中的欄位其實都會彙總到mapping中,如果不提前聲明,那麼可能導致因為使用dynamic mapping而使得數據類型定義出錯。比如在type1中birthdate欄位】
> 第二篇中這樣說的:當我們直接插入document的時候,如果不指定document的數據結構,那麼ElastciSearch會基於dynamic mapping來自動幫我們聲明每一個欄位的數據類型,比如"content":"hello world!"會被聲明成字元串類型,"post_date":"2017-07-07"會被認為是date類型。如果我們首先在一個type中聲明瞭content為字元串類型,再在另外一個type中聲明成日期類型,這會報錯,因為對於index來說,這個content已經被聲明成字元串類型了。
PUT /test0101
{
"settings": {
"index":{
"number_of_shards":3,
"number_of_replicas":1
}
},
"mappings": {
"person":{
"properties": {
"name":{
"type": "text"
},
"age":{
"type": "long"
},
"birthdate":{
"type":"date"
}
}
}
}
}
2.設置某個欄位不進行索引【設置後,你可以嘗試對這個欄位搜索,會報錯!】
PUT /test0102
{
"settings": {
"index":{
"number_of_shards":3,
"number_of_replicas":1
}
},
"mappings": {
"person":{
"properties": {
"name":{
"type": "text",
"index": "false"
},
"age":{
"type": "long"
},
"birthdate":{
"type":"date"
}
}
}
}
}
測試:
PUT /test0102/person/1
{
"name":"Paul Smith",
"age":18,
"birthdate":"2018-01-01"
}
GET /test0102/person/_search
{
"query": {
"match": {
"name": "Paul"
}
}
}
3.給某個欄位增加keyword
PUT /test0103
{
"settings": {
"index":{
"number_of_shards":3,
"number_of_replicas":1
}
},
"mappings": {
"person":{
"properties": {
"name":{
"type": "text",
"index": "false",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"age":{
"type": "long"
},
"birthdate":{
"type":"date"
}
}
}
}
}
測試:
PUT /test0103/person/1
{
"name":"Paul Smith",
"age":18,
"birthdate":"2018-01-01"
}
【註意這裡是不能使用name來搜索的,要使用name.keyword來搜索,而且keyword是對原始數據進行**不分詞**的搜索的,所以你搜單個詞是找不到的。】
GET /test0103/person/_search
{
"query": {
"match": {
"name.keyword": "Paul Smith"
}
}
}
修改mapping
mapping只能新增欄位,不能修改原有的欄位。
// 給索引test0103的類型person新增一個欄位height
PUT /test0103/_mapping/person
{
"properties": {
"height":{
"type": "float"
}
}
}
查看mapping
1.之前說過了,可以通過查看索引來查看mapping:GET /index
,例如GET /test0103/_mapping
2.通過GET /index/_mapping
,例子:GET /test0103/_mapping
3.你也可以附加type來查看指定type包裹的mapping。GET /test0103/_mapping/person
keyword
上面定義mapping的時候有定義一個keyword,這是什麼?有什麼用?
ElasticSearch對於一些類型的欄位,例如text類型的欄位,預設是會進行分詞的。但如果我們並不想分詞呢?一些數據我們想要非常精確地查找,並且只找到我們搜索的數據的時候,這個欄位是不應該分詞的。那麼我們可以使用keyword來存儲完整的原有的數據,keyword會作為一個索引詞,然後我們針對欄位.keyword
來搜索。【例子上面已經舉例了】理論上,這個不分詞的欄位的數據應該是比較簡短的,太長的話可能就沒有必要不分詞了。所以在定義keyword的時候還可以提供
"ignore_above": 數值
來限制超過多少個字元就不使用原有數據創建keyword.
現在版本中,對於預設分詞的欄位,現在會預設附加一個keyword。
mapping對分詞的影響
1.數據類型的影響:
以date和text類型存儲的“日期”類型的數據的分詞差異化為例:
date和string這兩者都是使用雙引號包裹的,與其他數值型的不分詞的數據類型不一樣。所以以他們兩個為例。
PUT /test0104
{
"settings": {
"index":{
"number_of_shards":3,
"number_of_replicas":1
}
},
"mappings": {
"test":{
"properties": {
"post_date":{
"type": "text"
},
"birthdate":{
"type":"date"
}
}
}
}
}
PUT /test0104/test/1
{
"post_date":"2019-09-30",
"birthdate":"2018-08-29"
}
PUT /test0104/test/2
{
"post_date":"2018-09-30",
"birthdate":"2017-08-29"
}
PUT /test0104/test/3
{
"post_date":"2017-09-30",
"birthdate":"2016-08-29"
}
//測試1:結果是ID:1
GET /test0104/test/_search
{
"query": {
"match": {
"post_date": "2019"
}
}
}
// 測試2:結果是ID=1,2,3的文檔,理論上text存儲的日期格式的沒有分詞區別
GET /test0104/test/_search
{
"query": {
"match": {
"post_date": "30"
}
}
}
//測試3:結果無,【單搜索08或2017,2018,2016也是無,】
GET /test0104/test/_search
{
"query": {
"match": {
"birthdate": "29"
}
}
}
//測試4, 結果是ID=2的文檔
GET /test0104/test/_search
{
"query": {
"match": {
"birthdate": "2017-08-29"
}
}
}
2.不索引的影響:
不索引的時候不會進行分詞,甚至不能用於搜索。【低版本對於properties中的index設置不一樣,5.x以下是不分詞,5.x以上是不索引,不索引就不可以用於搜索】
3.keyword的影響:
keyword適用於不分詞的搜索的情況,在keyword中的數據不會分詞。
4.其他:
mapping還可以設置分詞器來使用不同的分詞器來分詞。
// 你可以使用格式類似如下的代碼來測試mapping中某個欄位的分詞結果,如果是不允許分詞的,則會報Analysis requests are only supported on tokenized fields錯誤。
GET /test0104/_analyze
{
"field": "post_date",
"text": "2017-09-30"
}
補充:
- dynamic mapping策略是關於自動創建mapping的策略,定義了遇上某某數據的時候把它當作什麼類型,比如可以定義存入"2017-09-30"的時候認為是text而不是date,這些內容可能會留到後面再講,也有可能後面再補充到這裡。
- 複合數據類型比較特殊,可能會留到後面再講,也有可能後面再補充到這裡。
小節總結:
本節介紹了什麼是mapping,mapping負責管理索引的數據結構和欄位的分詞等一些配置。在直接存儲數據的時候,會dynamic mapping;然後介紹瞭如何創建mapping,如何修改mapping,如何查看mapping;然後介紹了keyword這個保留原數據的一個特殊的欄位。
相關度分數
相關度分數的具體演算法我們其實並不需要關心。但可能還是需要大概瞭解一下計算的方式。
為什麼說不需要關心呢?因為實際上相關度是由索引詞直接決定的,分析好哪些是用於搜索的詞就可以大致分析出相關度排序了。
當然,這隻是大概的,因為內部可能因為索引詞的重覆性問題會降低某個詞的score,但差距一般不會太大。
- 在我們進行搜索的時候,你可以看到一個score,這個就是相關度分數,在預設排序中相關度分數最高的會被排在最前面,這個分數是ElasticSearch根據你搜索的內容,使用內部演算法計算出的一個數值。
- 內部演算法主要是指TF演算法和IDF演算法。
TF演算法
TF演算法,全稱Term frequency,索引詞頻率演算法。
意義就像它的名字,會根據索引詞的頻率來計算,索引詞出現的次數越多,分數越高。
例子如下:
搜索
hello
有兩份文檔:A文檔:hello world!
,B文檔:hello hello hello
結果是B文檔的score大於A文檔。搜索
hello world
有兩份文檔:A文檔:hello world!
,B文檔:hello,are you ok?
結果是A文檔的score大於B文檔。要根據索引詞來綜合考慮。
IDF演算法
IDF演算法全稱Inverse Document Frequency,逆文本頻率。
搜索文本的詞在整個索引的所有文檔中出現的次數越多,這個詞所占的score的比重就越低。
例子如下:
搜索
hello world
,其中索引中hello出現次數1000次,world出現100次。
有三份文檔:A文檔hello,are you ok?
,B文檔The world is interesting!
,C文檔hello world!
結果是:C>B>A
由於hello出現頻率高,所以單個hello得到的score比不上world。
Field-length norm演算法。
- 這個演算法elasticsearch並沒有單獨列出來,但也有生效。
- field越長,相關度就越低,數據長度會拉低相關度。
例子如下:
搜索
hello world!
有兩份文檔:A文檔hello world!
,B文檔hello world,I'm xxx!
結果是:A>B
三種演算法的綜合:
(下麵屬於理論分析,並不真實這樣計算)
TF演算法針對在Field中,索引詞出現的頻率;
IDF演算法針對在整個索引中的索引詞出現的頻率;
Field-length norm演算法針對Field的長度。
那麼可以這樣分析,由於Field-length norm演算法並不直接針對score,所以它是最後起作用的,它理論上類似於一個除數。而TF和IDF是平等的,IDF計算出每一個索引詞的score量,TF來計算整個文檔中索引詞的score的加和。
也就是如下的計算:
1.IDF:計算索引詞的單位score,比如hello=0.1,world=0.2,
2.TF:計算整個文檔的sum(score),hello world!I'm xxx.
得到0.1+0.2=0.3
3.Field-length norm:將sum(score)/對應Field的長度,得出的結果就是score。
score計算API
elasticsearch提供了測試計算score的API,語法類似如下:
GET /index/type/_search?explain=true
{
"query": {
"match": {
"搜索欄位": "搜索值"
}
}
}
例子:
GET /douban/book/_search?explain=true
{
"query": {
"match": {
"book_name": "Story"
}
}
}
返回結果數據解析:
_explanation是score的原因,_explanation的格式是先列出分數,然後detail部分解釋分數,內層也是。
小節總結
這節介紹了相關度分數的計算方式,可以大致瞭解一下score是怎麼得出來得,TF是索引詞頻率演算法,是對索引詞分數的加和;IDF是基於整個索引對每個索引詞的計算分數;Field-length用於降低數據長的文檔的相關度分數。
分詞器
什麼是分詞器?
分詞器負責對document進行處理,以提高搜索效率。
分詞器通常由分解器tokenizer和詞元過濾器token filter組成。分詞器對數據的分詞處理:為了提高索引的效率,ElasticSearch會數據進行處理,處理方式主要有字元過濾、詞轉換、詞拆分
字元過濾:過濾一些特殊字元,例如&
、||
、html標簽,因為這些詞通常搜索意義不大。詞轉換:把一些意義相同的詞統一轉成一個詞,(同詞義轉換)比如mom,mother統一轉成mom;(大小寫轉換)he,He統一歸為He;還處理一些詞意義不大的詞(停用詞清除),比如英文的“the”,“to”,這些詞使用頻率很高,但沒有具體意義。
詞拆分:進行數據的拆分,拆分成詞,比如把
good morning,mom
拆分成good
,morining
,mom
。另外,詞拆分並不完全是按照數據的最小單位分解的,某一些分詞器會把一些詞進行組合,因為一些詞的組合起來才有索引的意義,比如中文的一些詞通常要組合起來才有意義,比如“大”和“家”要組成“大家”才有比較具體的意義,這是為了確保索引詞的最小單位是有意義的(比如英文mom的最小單位是m,o,m,內部的分詞器要能夠區分出mom整個是有意義的才可以確保是採用mom作為索引,而不是採用m和o,也正是因為這個問題,所以英文分詞器不能用於中文分詞器)。【分詞器有很多個,預設的分詞器是不能適當對中文數據分詞的,它只能把一個個數據按最小的單位拆分,因為英文分詞器不能分清楚怎麼把詞拆分才有意義,由於配置分詞器是一個較為靠後的知識點,所以前期將以英文數據為測試數據。】
常見的內置分詞器:
每一個分詞器的都有一些自己的規則,比如一些分詞器會把a當成停用詞,有些則不會;有些分詞器會去掉標點符號,有些則不會;有些分詞器能識別英文詞,但識別不了中文詞。要按需要來選擇分詞器。
- 標準分詞器standard analyzer:最基礎的分詞器。可以把詞進行拆分、
- 簡單分詞器simple analyzer:分詞、做字元過濾、不做詞轉換。
- 空格分詞器whitespace analyzer:僅僅根據空格來劃分詞,不會做字元過濾也不會做詞轉換。
- 英文分詞器english:根據英文標準來分詞,進行字元過濾,進行詞轉換(去除英文中的停用詞(a,the),同義詞轉換等)
- 小寫lowercase tokenizer:把英文全轉成小寫再分詞
- 字母分詞器letter tokenizer:根據字母來分詞
分詞示例
可以使用下麵的格式的命令來測試不同分詞器的分詞效果:
GET /_analyze
{
"analyzer": "分詞器",
"text": "要測試的文本"
}
// 舉例:
GET /_analyze
{
"analyzer": "simple",
"text": "I am a little boy,I have 5$.dogs,long-distance,<html></html>"
}
示例:
原句:I am a little boy,I have 5$.dogs,long-distance,<html></html>
- 標準分詞器standard analyzer:
i
,am
,a
,little
,boy
,i
,have
,5
,dogs
,long
,distance
,html
,html
【對於拆分成同一個詞的,會形成同一個索引詞】【這個分詞器,做了小寫、去掉特殊字元等操作】 - 簡單分詞器simple analyzer:
i
,am
,a
,little,boy
,i
,have
,dogs
,long
,distance
,html
,html
【這個分詞器做了小寫、去掉特殊字元和數字等操作】 - 空格分詞器whitespace analyzer:
I
,am
,a
,little
,boy,I
,have
,5$.dogs,long-distance,<html></html>
【根據空格分詞】 英文分詞器英文分詞器english::
i
,am
,littl
,boi
,i
,have
,5
,dog
,long
,distanc
,html
,html
【語言分詞器會比較特殊,會做一部分的形式轉換,有些時候會盲目地切分單詞,比如er和e這些常見尾碼,會被切掉,所以little變成了littl。】可能有人不是很懂分詞器的作用,這裡再次重談一下:
如果使用的分詞器是standard,那你輸入的loves會認為是loves,loved會認為是loved;
而english會把loves認為是love,loved認為是love.
所以在english分詞器中,loves和loved的搜索用的是love的索引詞搜索,而standard中用的是loves和loved的索引詞,從搜索效率來說,english的搜索才是我們想要的,它比較靈活。
而且有些分詞器會幫你把片語合起來,比如“中國人”這個詞不該被拆分成“中”“國”“人”】
修改分詞器
修改分詞器就是修改mapping中的analyzer,mapping中某個欄位一但創建就不能針對這個欄位修改,所以只能刪除再修改或新增。
PUT /test0106
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"test":{
"properties": {
"content":{
"type": "text",
"analyzer": "standard"
},
"detail":{
"type": "text",
"analyzer": "english"
}
}
}
}
}
中文分詞器
在前面,我們都是使用英文作為欄位的數據,因為ElasticSearch預設情況下無法很好地對中文進行分詞,它預設只能一個個字地分詞,所以它會把“中國人”這個詞拆分成“中”“國”“人”。
所以我們需要使用“插件”來對ElasticSearch來進行擴展。
我們常用的中文分詞器就是IK分詞器。
如何安裝
1.去github上下載IK的zip文件,下載的插件版本要與當前使用的elasticsearch版本一致,比如你是elasticsearch6.2.3,就下載6.2.3的【對於沒有自己版本的,這時候可能需要下載新的elasticsearch,有些人說可以通過修改pom.xml來強行適配,但還是存在一些問題的。】。github-IK
2.把zip文件中的elasticsearch
文件夾解壓到elasticsearch安裝目錄\plugins
中,並且重命名文件夾為ik-analyzer
3.然後重啟elasticsearch
使用
IK分詞器提供了兩種分詞器ik_max_word和ik_smart
- ik_max_word: 會將文本做最細粒度的拆分,比如
北京天安門廣場
會被拆分為北京
,京
,天安門廣場
,天安門
,天安
,門
,廣場
,會嘗試各種在IK中可能的組合; - ik_smart: 會做最粗粒度的拆分,比如會將
北京天安門廣場
拆分為北京
,天安門廣場
。 - 一般都會使用ik_max_word,只有在某些確實只需要最粗粒度的時候才使用ik_smart。
// 測試一:比較標準分詞器和IK分詞器的區別:
// 結果:標準分詞器只會一個個字地拆分
GET /_analyze
{
"analyzer": "standard",
"text": "北京天安門廣場"
}
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "北京天安門廣場"
}
GET /_analyze
{
"analyzer": "ik_smart",
"text": "北京天安門廣場"
}
// 測試二:給某個欄位的mapping配置分詞器為ik_max_word,插入測試數據,併進行搜索
PUT /test0107
{
"mappings": {
"test": {
"properties": {
"content": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
}
PUT /test0107/test/1
{
"content":"北京天安門廣場"
}
PUT /test0107/test/2
{
"content":"上海世博會"
}
PUT /test0107/test/3
{
"content":"北京鳥巢"
}
PUT /test0107/test/4
{
"content":"上海外灘"
}
// 開始搜索:
GET /test0107/test/_search
{
"query": {
"match": {
"content": "北京"
}
}
}
GET /test0107/test/_search
{
"query": {
"match": {
"content": "鳥巢"
}
}
}
GET /test0107/test/_search
{
"query": {
"match": {
"content": "世博會"
}
}
}
// 這條搜索是搜索不出結果的,因為分詞的結果沒有“京”字
GET /test0107/test/_search
{
"query": {
"match": {
"content": "北"
}
}
}
補充:
- 上面說了“分詞器通常由分解器tokenizer和詞元過濾器token filter組成。”,所以其實我們也可以自己通過組合分解器和過濾器來成形成一個分詞器。這裡有興趣自查的。【有可能後面某天會補充】
- 上面提到了中文分詞器,其實這已經涉及到了“插件”的內容了,這個內容會後面的篇章再講。
小節總結:
本節重新解釋了分詞器的作用(字元過濾,詞轉換,詞拆分),介紹了幾種常見的內置的分詞器(standard,english這些),並舉了一些分詞的例子,以及如何修改欄位的分詞器(mapping的analyzer),最後還介紹了怎麼安裝支持中文分詞的ik分詞器。
文檔的ID
文檔的ID其實是可以不指定的,不指定的時候會隨機生成唯一的一串字元串,
手動指定和不指定的區別:
手動指定通常適用於一些將資料庫的數據轉存到ElasticSearch的場景,因為這時候ID有特殊意義,讓資料庫的數據與ElasticSearch的數據關聯起來,(有時候可能需要同時查資料庫和ElasticSearch,那麼可以直接根據資料庫中的ID來查ElasticSearch中的數據)。
不指定的時候(這時候通常ID是沒有特殊意義的),ElasticSearch會自動地幫我們生成一個ID值,自動生成的ID長度為20個字元,這個ID它能確保是不會重覆的,就算是在分散式併發情況下也不會發生衝突。【由於此時的ID是隨機字元串,所以根據ID來查詢數據會比較麻煩】
這裡講的還是比較偏應用方面的基礎知識,後面可能還會補充偏底層的一些基礎知識,比如底層寫入流程和NRT這些。