1. 程式人生 > >ES倒排索引與分詞詳解

ES倒排索引與分詞詳解

倒排索引

  • 正排索引:文件id到單詞的關聯關係
  • 倒排索引:單詞到文件id的關聯關係

示例:
對以下三個文件去除停用詞後構造倒排索引

 

image

倒排索引-查詢過程

查詢包含“搜尋引擎”的文件

  1. 通過倒排索引獲得“搜尋引擎”對應的文件id列表,有1,3
  2. 通過正排索引查詢1和3的完整內容
  3. 返回最終結果

倒排索引-組成

  • 單詞詞典(Term Dictionary)
  • 倒排列表(Posting List)

單詞詞典(Term Dictionary)

單詞詞典的實現一般用B+樹,B+樹構造的視覺化過程網址:

B+ Tree Visualization

關於B樹和B+樹

  1. 維基百科-B樹
  2. 維基百科-B+樹
  3. B樹和B+樹的插入、刪除圖文詳解

image

倒排列表(Posting List)

  • 倒排列表記錄了單詞對應的文件集合,有倒排索引項(Posting)組成
  • 倒排索引項主要包含如下資訊:
    1. 文件id用於獲取原始資訊
    2. 單詞頻率(TF,Term Frequency),記錄該單詞在該文件中出現的次數,用於後續相關性算分
    3. 位置(Posting),記錄單詞在文件中的分詞位置(多個),用於做詞語搜尋(Phrase Query)
    4. 偏移(Offset),記錄單詞在文件的開始和結束位置,用於高亮顯示

image

B+樹內部結點存索引,葉子結點存資料,這裡的 單詞詞典就是B+樹索引,倒排列表就是資料,整合在一起後如下所示

note:
B+樹索引中文和英文怎麼比較大小呢?unicode比較還是拼音呢?

image

ES儲存的是一個JSON格式的文件,其中包含多個欄位,每個欄位會有自己的倒排索引

分詞

分詞是將文字轉換成一系列單詞(Term or Token)的過程,也可以叫文字分析,在ES裡面稱為Analysis

image

分詞器

分詞器是ES中專門處理分詞的元件,英文為Analyzer,它的組成如下:

  • Character Filters:針對原始文字進行處理,比如去除html標籤
  • Tokenizer:將原始文字按照一定規則切分為單詞
  • Token Filters:針對Tokenizer處理的單詞進行再加工,比如轉小寫、刪除或增新等處理

分詞器呼叫順序

 

image

Analyze API

ES提供了一個可以測試分詞的API介面,方便驗證分詞效果,endpoint是_analyze

  • 可以直接指定analyzer進行測試

image

  • 可以直接指定索引中的欄位進行測試
POST test_index/doc
{
  "username": "whirly",
  "age":22
}

POST test_index/_analyze
{
  "field": "username",
  "text": ["hello world"]
}
  • 可以自定義分詞器進行測試
POST _analyze
{
  "tokenizer": "standard",
  "filter": ["lowercase"],
  "text": ["Hello World"]
}

預定義的分詞器

ES自帶的分詞器有如下:

  • Standard Analyzer
    • 預設分詞器
    • 按詞切分,支援多語言
    • 小寫處理
  • Simple Analyzer
    • 按照非字母切分
    • 小寫處理
  • Whitespace Analyzer
    • 空白字元作為分隔符
  • Stop Analyzer
    • 相比Simple Analyzer多了去除請用詞處理
    • 停用詞指語氣助詞等修飾性詞語,如the, an, 的, 這等
  • Keyword Analyzer
    • 不分詞,直接將輸入作為一個單詞輸出
  • Pattern Analyzer
    • 通過正則表示式自定義分隔符
    • 預設是\W+,即非字詞的符號作為分隔符
  • Language Analyzer
    • 提供了30+種常見語言的分詞器

示例:停用詞分詞器

POST _analyze
{
  "analyzer": "stop",
  "text": ["The 2 QUICK Brown Foxes jumped over the lazy dog's bone."]
}

結果

{
  "tokens": [
    {
      "token": "quick",
      "start_offset": 6,
      "end_offset": 11,
      "type": "word",
      "position": 1
    },
    {
      "token": "brown",
      "start_offset": 12,
      "end_offset": 17,
      "type": "word",
      "position": 2
    },
    {
      "token": "foxes",
      "start_offset": 18,
      "end_offset": 23,
      "type": "word",
      "position": 3
    },
    {
      "token": "jumped",
      "start_offset": 24,
      "end_offset": 30,
      "type": "word",
      "position": 4
    },
    {
      "token": "over",
      "start_offset": 31,
      "end_offset": 35,
      "type": "word",
      "position": 5
    },
    {
      "token": "lazy",
      "start_offset": 40,
      "end_offset": 44,
      "type": "word",
      "position": 7
    },
    {
      "token": "dog",
      "start_offset": 45,
      "end_offset": 48,
      "type": "word",
      "position": 8
    },
    {
      "token": "s",
      "start_offset": 49,
      "end_offset": 50,
      "type": "word",
      "position": 9
    },
    {
      "token": "bone",
      "start_offset": 51,
      "end_offset": 55,
      "type": "word",
      "position": 10
    }
  ]
}

中文分詞

  • 難點
    • 中文分詞指的是將一個漢字序列切分為一個一個的單獨的詞。在英文中,單詞之間以空格作為自然分界詞,漢語中詞沒有一個形式上的分界符
    • 上下文不同,分詞結果迥異,比如交叉歧義問題
  • 常見分詞系統
    • IK:實現中英文單詞的切分,可自定義詞庫,支援熱更新分詞詞典
    • jieba:支援分詞和詞性標註,支援繁體分詞,自定義詞典,並行分詞等
    • Hanlp:由一系列模型與演算法組成的Java工具包,目標是普及自然語言處理在生產環境中的應用
    • THUAC:中文分詞和詞性標註

安裝ik中文分詞外掛

# 在Elasticsearch安裝目錄下執行命令,然後重啟es
bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.0/elasticsearch-analysis-ik-6.3.0.zip

# 如果由於網路慢,安裝失敗,可以先下載好zip壓縮包,將下面命令改為實際的路徑,執行,然後重啟es
bin/elasticsearch-plugin install file:///path/to/elasticsearch-analysis-ik-6.3.0.zip
  • ik測試 - ik_smart
POST _analyze
{
  "analyzer": "ik_smart",
  "text": ["公安部:各地校車將享最高路權"]
}

# 結果
{
  "tokens": [
    {
      "token": "公安部",
      "start_offset": 0,
      "end_offset": 3,
      "type": "CN_WORD",
      "position": 0
    },
    {
      "token": "各地",
      "start_offset": 4,
      "end_offset": 6,
      "type": "CN_WORD",
      "position": 1
    },
    {
      "token": "校車",
      "start_offset": 6,
      "end_offset": 8,
      "type": "CN_WORD",
      "position": 2
    },
    {
      "token": "將",
      "start_offset": 8,
      "end_offset": 9,
      "type": "CN_CHAR",
      "position": 3
    },
    {
      "token": "享",
      "start_offset": 9,
      "end_offset": 10,
      "type": "CN_CHAR",
      "position": 4
    },
    {
      "token": "最高",
      "start_offset": 10,
      "end_offset": 12,
      "type": "CN_WORD",
      "position": 5
    },
    {
      "token": "路",
      "start_offset": 12,
      "end_offset": 13,
      "type": "CN_CHAR",
      "position": 6
    },
    {
      "token": "權",
      "start_offset": 13,
      "end_offset": 14,
      "type": "CN_CHAR",
      "position": 7
    }
  ]
}
  • ik測試 - ik_max_word
POST _analyze
{
  "analyzer": "ik_max_word",
  "text": ["公安部:各地校車將享最高路權"]
}

# 結果
{
  "tokens": [
    {
      "token": "公安部",
      "start_offset": 0,
      "end_offset": 3,
      "type": "CN_WORD",
      "position": 0
    },
    {
      "token": "公安",
      "start_offset": 0,
      "end_offset": 2,
      "type": "CN_WORD",
      "position": 1
    },
    {
      "token": "部",
      "start_offset": 2,
      "end_offset": 3,
      "type": "CN_CHAR",
      "position": 2
    },
    {
      "token": "各地",
      "start_offset": 4,
      "end_offset": 6,
      "type": "CN_WORD",
      "position": 3
    },
    {
      "token": "校車",
      "start_offset": 6,
      "end_offset": 8,
      "type": "CN_WORD",
      "position": 4
    },
    {
      "token": "將",
      "start_offset": 8,
      "end_offset": 9,
      "type": "CN_CHAR",
      "position": 5
    },
    {
      "token": "享",
      "start_offset": 9,
      "end_offset": 10,
      "type": "CN_CHAR",
      "position": 6
    },
    {
      "token": "最高",
      "start_offset": 10,
      "end_offset": 12,
      "type": "CN_WORD",
      "position": 7
    },
    {
      "token": "路",
      "start_offset": 12,
      "end_offset": 13,
      "type": "CN_CHAR",
      "position": 8
    },
    {
      "token": "權",
      "start_offset": 13,
      "end_offset": 14,
      "type": "CN_CHAR",
      "position": 9
    }
  ]
}
  • ik兩種分詞模式ik_max_word 和 ik_smart 什麼區別?
    • ik_max_word: 會將文字做最細粒度的拆分,比如會將“中華人民共和國國歌”拆分為“中華人民共和國,中華人民,中華,華人,人民共和國,人民,人,民,共和國,共和,和,國國,國歌”,會窮盡各種可能的組合;

    • ik_smart: 會做最粗粒度的拆分,比如會將“中華人民共和國國歌”拆分為“中華人民共和國,國歌”。

自定義分詞

當自帶的分詞無法滿足需求時,可以自定義分詞,通過定義Character Filters、Tokenizer和Token Filters實現

Character Filters

  • 在Tokenizer之前對原始文字進行處理,比如增加、刪除或替換字元等
  • 自帶的如下:
    • HTML Strip Character Filter:去除HTML標籤和轉換HTML實體
    • Mapping Character Filter:進行字元替換操作
    • Pattern Replace Character Filter:進行正則匹配替換
  • 會影響後續tokenizer解析的position和offset資訊

Character Filters測試

POST _analyze
{
  "tokenizer": "keyword",
  "char_filter": ["html_strip"],
  "text": ["<p>I&apos;m so <b>happy</b>!</p>"]
}

# 結果
{
  "tokens": [
    {
      "token": """

I'm so happy!

""",
      "start_offset": 0,
      "end_offset": 32,
      "type": "word",
      "position": 0
    }
  ]
}

Tokenizers

  • 將原始文字按照一定規則切分為單詞(term or token)
  • 自帶的如下:
    • standard 按照單詞進行分割
    • letter 按照非字元類進行分割
    • whitespace 按照空格進行分割
    • UAX URL Email 按照standard進行分割,但不會分割郵箱和URL
    • Ngram 和 Edge NGram 連詞分割
    • Path Hierarchy 按照檔案路徑進行分割

Tokenizers 測試

POST _analyze
{
  "tokenizer": "path_hierarchy",
  "text": ["/path/to/file"]
}

# 結果
{
  "tokens": [
    {
      "token": "/path",
      "start_offset": 0,
      "end_offset": 5,
      "type": "word",
      "position": 0
    },
    {
      "token": "/path/to",
      "start_offset": 0,
      "end_offset": 8,
      "type": "word",
      "position": 0
    },
    {
      "token": "/path/to/file",
      "start_offset": 0,
      "end_offset": 13,
      "type": "word",
      "position": 0
    }
  ]
}

Token Filters

  • 對於tokenizer輸出的單詞(term)進行增加、刪除、修改等操作
  • 自帶的如下:
    • lowercase 將所有term轉為小寫
    • stop 刪除停用詞
    • Ngram 和 Edge NGram 連詞分割
    • Synonym 新增近義詞的term

Token Filters測試

POST _analyze
{
  "text": [
    "a Hello World!"
  ],
  "tokenizer": "standard",
  "filter": [
    "stop",
    "lowercase",
    {
      "type": "ngram",
      "min_gram": 4,
      "max_gram": 4
    }
  ]
}

# 結果
{
  "tokens": [
    {
      "token": "hell",
      "start_offset": 2,
      "end_offset": 7,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "ello",
      "start_offset": 2,
      "end_offset": 7,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "worl",
      "start_offset": 8,
      "end_offset": 13,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "orld",
      "start_offset": 8,
      "end_offset": 13,
      "type": "<ALPHANUM>",
      "position": 2
    }
  ]
}

自定義分詞

自定義分詞需要在索引配置中設定 char_filter、tokenizer、filter、analyzer等

自定義分詞示例:

  • 分詞器名稱:my_custom\
  • 過濾器將token轉為大寫
PUT test_index_1
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_custom_analyzer": {
          "type":      "custom",
          "tokenizer": "standard",
          "char_filter": [
            "html_strip"
          ],
          "filter": [
            "uppercase",
            "asciifolding"
          ]
        }
      }
    }
  }
}

自定義分詞器測試

POST test_index_1/_analyze
{
  "analyzer": "my_custom_analyzer",
  "text": ["<p>I&apos;m so <b>happy</b>!</p>"]
}

# 結果
{
  "tokens": [
    {
      "token": "I'M",
      "start_offset": 3,
      "end_offset": 11,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "SO",
      "start_offset": 12,
      "end_offset": 14,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "HAPPY",
      "start_offset": 18,
      "end_offset": 27,
      "type": "<ALPHANUM>",
      "position": 2
    }
  ]
}

分詞使用說明

分詞會在如下兩個時機使用:

  • 建立或更新文件時(Index Time),會對相應的文件進行分詞處理
  • 查詢時(Search Time),會對查詢語句進行分詞
    • 查詢時通過analyzer指定分詞器
    • 通過index mapping設定search_analyzer實現
    • 一般不需要特別指定查詢時分詞器,直接使用索引分詞器即可,否則會出現無法匹配的情況

分詞使用建議

  • 明確欄位是否需要分詞,不需要分詞的欄位就將type設定為keyword,可以節省空間和提高寫效能
  • 善用_analyze API,檢視文件的分詞結果

分析與分析器

分析 包含下面的過程:

  • 首先,將一塊文字分成適合於倒排索引的獨立的 詞條 ,
  • 之後,將這些詞條統一化為標準格式以提高它們的“可搜尋性”,或者 recall

分析器執行上面的工作。 分析器 實際上是將三個功能封裝到了一個包裡:

字元過濾器

首先,字串按順序通過每個 字元過濾器 。他們的任務是在分詞前整理字串。一個字元過濾器可以用來去掉HTML,或者將 & 轉化成 `and`。

分詞器

其次,字串被 分詞器 分為單個的詞條。一個簡單的分詞器遇到空格和標點的時候,可能會將文字拆分成詞條。

Token 過濾器

最後,詞條按順序通過每個 token 過濾器 。這個過程可能會改變詞條(例如,小寫化 Quick ),刪除詞條(例如, 像 a`, `and`, `the 等無用詞),或者增加詞條(例如,像 jump 和 leap 這種同義詞)。

Elasticsearch提供了開箱即用的字元過濾器、分詞器和token 過濾器。 這些可以組合起來形成自定義的分析器以用於不同的目的。我們會在 自定義分析器 章節詳細討論。

內建分析器

但是, Elasticsearch還附帶了可以直接使用的預包裝的分析器。 接下來我們會列出最重要的分析器。為了證明它們的差異,我們看看每個分析器會從下面的字串得到哪些詞條:

"Set the shape to semi-transparent by calling set_trans(5)"

標準分析器

標準分析器是Elasticsearch預設使用的分析器。它是分析各種語言文字最常用的選擇。它根據 Unicode 聯盟 定義的 單詞邊界 劃分文字。刪除絕大部分標點。最後,將詞條小寫。它會產生

set, the, shape, to, semi, transparent, by, calling, set_trans, 5

簡單分析器

簡單分析器在任何不是字母的地方分隔文字,將詞條小寫。它會產生

set, the, shape, to, semi, transparent, by, calling, set, trans

空格分析器

空格分析器在空格的地方劃分文字。它會產生

Set, the, shape, to, semi-transparent, by, calling, set_trans(5)

語言分析器

特定語言分析器可用於 很多語言。它們可以考慮指定語言的特點。例如, 英語 分析器附帶了一組英語無用詞(常用單詞,例如 and 或者 the ,它們對相關性沒有多少影響),它們會被刪除。 由於理解英語語法的規則,這個分詞器可以提取英語單詞的 詞幹 。

英語 分詞器會產生下面的詞條:

set, shape, semi, transpar, call, set_tran, 5

注意看 transparent`、 `calling 和 set_trans 已經變為詞根格式。

什麼時候使用分析器

當我們 索引 一個文件,它的全文域被分析成詞條以用來建立倒排索引。 但是,當我們在全文域 搜尋 的時候,我們需要將查詢字串通過 相同的分析過程 ,以保證我們搜尋的詞條格式與索引中的詞條格式一致。

全文查詢,理解每個域是如何定義的,因此它們可以做 正確的事:

  • 當你查詢一個 全文 域時, 會對查詢字串應用相同的分析器,以產生正確的搜尋詞條列表。
  • 當你查詢一個 精確值 域時,不會分析查詢字串, 而是搜尋你指定的精確值。

現在你可以理解在 開始章節 的查詢為什麼返回那樣的結果:

  • date 域包含一個精確值:單獨的詞條 `2014-09-15`。
  • _all 域是一個全文域,所以分詞程序將日期轉化為三個詞條: `2014`, `09`, 和 `15`。

當我們在 _all 域查詢 2014`,它匹配所有的12條推文,因為它們都含有 `2014 :

GET /_search?q=2014              # 12 results

當我們在 _all 域查詢 2014-09-15`,它首先分析查詢字串,產生匹配 `2014`, `09`, 或 `15 中 任意 詞條的查詢。這也會匹配所有12條推文,因為它們都含有 2014 :

GET /_search?q=2014-09-15        # 12 results !

當我們在 date 域查詢 `2014-09-15`,它尋找 精確 日期,只找到一個推文:

GET /_search?q=date:2014-09-15   # 1  result

當我們在 date 域查詢 `2014`,它找不到任何文件,因為沒有文件含有這個精確日誌:

GET /_search?q=date:2014         # 0  results !

測試分析器

有些時候很難理解分詞的過程和實際被儲存到索引中的詞條,特別是你剛接觸 Elasticsearch。為了理解發生了什麼,你可以使用 analyze API 來看文字是如何被分析的。在訊息體裡,指定分析器和要分析的文字:

GET /_analyze
{
  "analyzer": "standard",
  "text": "Text to analyze"
}

結果中每個元素代表一個單獨的詞條:

{
   "tokens": [
      {
         "token":        "text",
         "start_offset": 0,
         "end_offset":   4,
         "type":         "<ALPHANUM>",
         "position":     1
      },
      {
         "token":        "to",
         "start_offset": 5,
         "end_offset":   7,
         "type":         "<ALPHANUM>",
         "position":     2
      },
      {
         "token":        "analyze",
         "start_offset": 8,
         "end_offset":   15,
         "type":         "<ALPHANUM>",
         "position":     3
      }
   ]
}

token 是實際儲存到索引中的詞條。 position 指明詞條在原始文字中出現的位置。 start_offset 和 end_offset 指明字元在原始字串中的位置。

每個分析器的 type 值都不一樣,可以忽略它們。它們在Elasticsearch中的唯一作用在於keep_types token 過濾器

analyze API 是一個有用的工具,它有助於我們理解Elasticsearch索引內部發生了什麼,隨著深入,我們會進一步討論它。

指定分析器

當Elasticsearch在你的文件中檢測到一個新的字串域 ,它會自動設定其為一個全文 字串 域,使用 標準 分析器對它進行分析。

你不希望總是這樣。可能你想使用一個不同的分析器,適用於你的資料使用的語言。有時候你想要一個字串域就是一個字串域--不使用分析,直接索引你傳入的精確值,例如使用者ID或者一個內部的狀態域或標籤。

要做到這一點,我們必須手動指定這些域的對映。

 

更多內容請訪問網站: http://laijianfeng.org

轉自:https://www.jianshu.com/p/3abaa0083bac