天天看點

inverted index,doc_values,store 及 source—Elastic Stack 實戰手冊

inverted index,doc_values,store 及 source—Elastic Stack 實戰手冊
https://developer.aliyun.com/topic/download?id=1295 · 更多精彩内容,請下載下傳閱讀全本《Elastic Stack實戰手冊》 https://developer.aliyun.com/topic/download?id=1295 https://developer.aliyun.com/topic/es100 · 加入創作人行列,一起交流碰撞,參與技術圈年度盛事吧 https://developer.aliyun.com/topic/es100

創作人:歐陽楚才

反向索引

Elasticsearch 使用一種稱為反向索引的結構,它适用于快速的全文搜尋。一個反向索引由文檔中所有不重複詞的清單構成,對于其中每個詞,有一個包含它的文檔清單。

假設我們有兩個文檔,每個文檔的正文字段包含如下内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

為了建立反向索引,我們首先将每個文檔的正文字段,拆分成單獨的詞(我們稱它為詞條或 Tokens),建立一個包含所有不重複詞條的排序清單,然後列出每個詞條出現在哪個文檔。

結果如下所示:

詞條 文檔1 文檔2
Quick X
The
brown
dog
dogs
fox
foxes
in
jumped
lazy
leap
over
quick
summer
the

現在,如果我們想搜尋 Quick brown,我們隻需要查找包含每個詞條的文檔:

比對詞條數量 2 1

兩個文檔都比對,但是第一個文檔比第二個比對度更高。如果我們使用,僅計算比對詞條數量的簡單相似性算法,那麼我們可以說,對于我們查詢的相關性來講,第一個文檔比第二個文檔更佳。

但是,我們目前的反向索引有一些問題:

· Quick 和 quick 以獨立的詞條出現,然而使用者可能認為它們是相同的詞。

· fox 和 foxes 非常相似,就像 dog 和 dogs,他們有相同的詞根。

· jumped 和 leap,盡管沒有相同的詞根,但他們是同義詞。

使用前面的索引搜尋 + Quick + fox 不會得到任何比對文檔。(記住,+ 字首表明這個詞必須存在)隻有同時出現 Quick 和 fox 的文檔才滿足這個查詢條件,但是第一個文檔包含 quick fox,第二個文檔包含 Quick foxes。

我們的使用者可以合理的期望兩個文檔與查詢比對,我們可以做的更好。

如果我們将詞條規範為标準模式,那麼我們可以找到與使用者搜尋的詞條不完全一緻,但具有足夠相關性的文檔,例如:

· Quick 可以小寫化為 quick

· foxes 可以詞幹提取 -- 變為詞根的格式 -- 為 fox。類似的,dogs 可以為提取為 dog

· jumped 和 leap 是同義詞,可以索引為相同的單詞 jump

現在索引看上去像這樣:

jump

這還遠遠不夠。我們搜尋 +Quick +fox 仍然會失敗,因為在我們的索引中,已經沒有 Quick 了。但是,如果我們對搜尋的字元串,使用與正文字段相同的标準化規則,會變成查詢 +quick +fox,這樣兩個文檔都會比對。

禁用索引

預設情況下,Elasticsearch 文檔每個字段都會被索引。如果某些字段不需要支援查詢,可以在映射中配置 "index": false ,減少存儲空間占用,并且提升寫入速度。盡管這個字段不能被搜尋,但是它并不妨礙做聚合(如果該字段是可以聚合的字段)。

例如,文章的标題、正文、釋出時間字段,需要建立索引,文章的 url 字段不需要被索引,建立索引映射時可以按以下方式禁用它:

PUT news
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "content": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "createtime": {
        "type": "date"
      },
      "url": {
        "type": "keyword",
        "index": false
      }
    }
  }
}           

文檔值

在 Elasticsearch 中,Doc Values 是一種列式存儲結構。Doc Values 預設對所有字段啟用,除了 text 和 annotated_text 類型字段。

Doc Values 是在索引時建立的,當字段索引時,Elasticsearch 為了能夠快速檢索,會把字段的值加入反向索引中,同時它也會存儲該字段的 Doc Values。

Elasticsearch 中的 Doc Values 常被應用到以下場景:

· 對一個字段進行排序

· 對一個字段進行聚合

· 某些過濾,比如地理位置過濾

· 某些與字段相關的腳本計算

· 使用 docvalue_fields 傳回搜尋結果部分字段值

因為文檔值被序列化到磁盤,可以依靠作業系統的幫助來快速通路。當資料集遠小于節點的可用記憶體,作業系統會自動将所有的 Doc Values 儲存在記憶體中,使得其讀寫十分高速;當其遠大于可用記憶體,作業系統會自動把 Doc Values 加載到系統的頁緩存中,進而避免了 JVM 堆記憶體溢出異常。

列式存儲的壓縮

Doc Values 本質上是一個序列化的列式存儲,适用于聚合、排序、腳本等操作。這種存儲方式也非常便于壓縮,特别是數字類型,這樣可以節約磁盤空間,并且提高通路速度。

來看一組數字類型的 Doc Values,了解它如何壓縮資料:

文檔
100
1000
文檔3 1500
文檔4 1200
文檔5 300
文檔6 1900
文檔7 4200

按列布局意味着我們有一個連續的資料塊:[100,1000,1500,1200,300,1900,4200]。

因為我們已經知道他們都是數字,而不是像文檔或行中看到的異構集合,是以我們可以使用統一的偏移來将他們緊緊排列。

而且,針對這樣的數字有很多種壓縮技巧。你會注意到這裡每個數字都是 100 的倍數,Doc Values 會檢測一個段裡面的所有數值,并使用一個最大公約數,友善做進一步的資料壓縮。

如果我們儲存100 作為此段的除數,我們可以對每個數字都除以100,然後得到: [1,10,15,12,3,19,42]。

現在這些數字變小了,隻需要很少的位就可以存儲下,也減少了磁盤存放的大小。Doc Values 在壓縮過程中使用如下技巧,它會按依次檢測以下壓縮模式:

  1. 如果所有的數值各不相同或缺失,設定一個标記并記錄這些值
  2. 如果這些值小于 256,将使用一個簡單的編碼表
  3. 如果這些值大于 256,檢測是否存在一個最大公約數
  4. 如果沒有存在最大公約數,從最小的數值開始,統一計算偏移量進行編碼

你會發現這些壓縮模式不是傳統的通用壓縮算法,比如 DEFLATE 或者 LZ4。因為列式存儲的結構是嚴格且良好定義的,我們可以通過使用專門的模式來達到,比通用壓縮算法(如 LZ4)更高的壓縮效果。

禁用 Doc Values

Doc Values 預設對所有字段啟用,除了 text 和 annotated_text 類型字段。也就是說所有的數字、地理坐标、日期、IP 和 keyword 類型都會預設開啟。

Text 類型字段不能使用 Doc Values,文本經過分析流程生成很多 Token,使得 Doc Values 不能高效運作。

因為 Doc Values 預設啟用,你可以選擇對你資料集裡面的大多數字段,進行聚合和排序操作。如果你知道你永遠也不會對某些字段進行聚合、排序或是使用腳本操作,你可以通過禁用特定字段的 Doc Values。這樣不僅節省磁盤空間,也會提升索引的速度。

要禁用 Doc Values,在字段的映射 (mapping) 設定 doc_values: false 即可。例如,這裡我們建立了一個新的索引,字段 "session_id" 禁用了 Doc Values:

PUT my_index
{
  "mappings": {
    "properties": {
      "session_id": {
        "type": "keyword",
        "doc_values": false
      }
    }
  }
}           

通過設定 doc_values: false,這個字段将不能被用于聚合、排序以及腳本操作。

反過來也是可以進行配置的:讓一個字段可以被聚合,通過禁用反向索引,使它不能被正常搜尋,例如:

PUT my_index
{
  "mappings": {
    "properties": {
      "customer_token": {
        "type": "keyword",
        "doc_values": true,
        "index": false
      }
    }
  }
}           

通過設定 doc_values: true 和 index: false,我們得到一個隻能被用于聚合/排序/腳本的字段。無可否認,這是一個非常少見的情況,但很有用。

存儲

預設情況下,字段原始值會被索引用于查詢,但是不會被存儲。為了展示文檔内容,通過一個叫 _source 的字段用于存儲整個文檔的原始值。

在字段的映射 (mapping) 設定 store: true,可以使索引單獨儲存這個字段。通常情況下,如果文檔本身十分龐大,而一些字段又會經常單獨使用,那麼這樣的字段,就可以設定為單獨存儲,然後可以使用 stored_fields 單獨檢索這些字段。

例如,如果你的文檔包含标題、時間和一個很大的正文字段,你可能隻需要檢索标題、時間字段,沒必要從很大的 _source 原文中解析出這些字段:

#建立索引,指定常用字段store屬性
PUT /my-index-000001
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "store": true 
      },
      "date": {
        "type": "date",
        "store": true 
      },
      "content": {
        "type": "text"
      }
    }
  }
}

#插入記錄
PUT /my-index-000001/_doc/1
{
  "title":   "短文本标題",
  "date":    "2021-05-01",
  "content": "很長很長很長的正文字段..."
}

#查詢結果傳回stored_fields指定字段
GET /my-index-000001/_search
{
  "stored_fields": [ "title", "date" ] 
}           

注意:stored_fields 傳回結果是數組格式。如果你需要擷取原始文檔,可以通過_source字段替代。

原文

_source 字段包含索引時發送的原始 JSON 文檔。_source 字段本身不建索引,但是存儲原始文檔,以便在執行查詢請求時,可以将其傳回。可以通過設定,禁用原文字段,或者隻存儲特定字段。

_source 在 Lucene 中是映射為一個特殊的字段:

Field Index IndexType Analyzer DocValues Store
_source No Yes

Elasticsearch 中 _source 字段的主要目的,是通過 doc_id 讀取該文檔的原始内容,是以隻需要存儲 Store 即可。

Elasticsearch 中使用 _source 字段可以實作以下功能:

Update:

部分更新時,需要從讀取文檔儲存在 _source 字段中的原文,然後和請求中的部分字段合并為一個完整文檔。如果沒有 _source,則不能完成部分字段的 Update 操作。

Reindex:

可以通過 Reindex API 完成索引重建,過程中不需要從其他系統導入全量資料,而是從目前文檔的 _source 中讀取。如果沒有 _source,則不能使用 Reindex API。

Script:

不管是 Index 還是 Search 的 Script,都可能用到存儲在 Store 中的原始内容,如果禁用了 _source,則這部分功能不再可用。

Summary:

摘要資訊也是來源于 _source 字段。

禁用 _source

盡管使用非常友善,但是 _source 字段會導緻占用更多的存儲空間。如果業務上不需要存儲原始文檔,可以按以下方式禁用它:

PUT my-index-000001
{
  "mappings": {
    "_source": {
      "enabled": false
    }
  }
}           

注意:禁用 _source 會導緻更新、重建索引、摘要功能不可用,生産環境慎用。考慮節省存儲空間,可以通過修改索引設定 index.codec 提高壓縮效率。

包含/排除部分字段

包含/排除 _source 部分字段可以按以下方式設定它:

PUT logs
{
  "mappings": {
    "_source": {
      "includes": [
        "*.count",
        "meta.*"
      ],
      "excludes": [
        "meta.description",
        "meta.other.*"
      ]
    }
  }
}

PUT logs/_doc/1
{
  "requests": {
    "count": 10,
    "foo": "bar" 
  },
  "meta": {
    "name": "Some metric",
    "description": "Some metric description", 
    "other": {
      "foo": "one", 
      "baz": "two" 
    }
  }
}

GET logs/_search
{
  "query": {
    "match": {
      "meta.other.foo": "one" 
    }
  }
}