
創作人:歐陽楚才
反向索引
Elasticsearch 使用一種稱為反向索引的結構,它适用于快速的全文搜尋。一個反向索引由文檔中所有不重複詞的清單構成,對于其中每個詞,有一個包含它的文檔清單。
假設我們有兩個文檔,每個文檔的正文字段包含如下内容:
- The quick brown fox jumped over the lazy dog
- 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 在壓縮過程中使用如下技巧,它會按依次檢測以下壓縮模式:
- 如果所有的數值各不相同或缺失,設定一個标記并記錄這些值
- 如果這些值小于 256,将使用一個簡單的編碼表
- 如果這些值大于 256,檢測是否存在一個最大公約數
- 如果沒有存在最大公約數,從最小的數值開始,統一計算偏移量進行編碼
你會發現這些壓縮模式不是傳統的通用壓縮算法,比如 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"
}
}
}