1. 程式人生 > >〈四〉ElasticSearch的認識:基礎原理的補充

〈四〉ElasticSearch的認識:基礎原理的補充

目錄

  • 想想我們漏了什麼
    • 回顧
    • 補回
  • 叢集的建立
    • 叢集發現機制
    • 配置檔案
    • 健康狀態
    • 補充:
    • 小節總結
  • 分片的管理
    • 梳理
    • 分片的均衡分配
    • 主副分片的排斥
    • 容錯性:
    • 資料路由
    • 對於叢集健康狀態的影響
    • 小節總結:
  • 請求的處理
    • 提供服務
    • 協調節點cordination node
    • 請求分發的負載均衡
    • 補充:
    • 小節總結
  • 文件的元資料
  • 文件的資料型別
    • 資料型別
    • 補充:
  • mapping
    • dynamic mapping
      • 舉例:
    • 建立mapping
    • 修改mapping
    • 檢視mapping
    • keyword
    • mapping對分詞的影響
    • 補充:
    • 小節總結:
  • 相關度分數
    • TF演算法
    • IDF演算法
    • Field-length norm演算法。
    • 三種演算法的綜合:
    • score計算API
    • 小節總結
  • 分詞器
    • 常見的內建分詞器:
    • 分詞示例
    • 修改分詞器
    • 中文分詞器
      • 如何安裝
      • 使用
    • 補充:
    • 小節總結:
  • 文件的ID

想想我們漏了什麼

這篇文章已經是第四篇了,前面很多都只是講了基礎的使用,沒有講到內層的原理,所以這裡就要補一下原理知識了。

回顧

先回顧一下我們前面學過了什麼,再想想我們漏了什麼。
第一篇我們認識了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這些。