Elasticsearch原理(八):嵌套結構的實現

我們在使用Elasticsearch的過程中,很多業務場景都會用到關聯查詢。而目前Elasticsearch支持的關聯查詢無非就是兩種方式,一種使用嵌套(nested)和父子文檔。本文主要來聊聊關於nested,Elasticsearch。

文章末尾會附上nested和父子文檔的差別和使用場景。

如果大家有過一些Lucene基礎的話,相信都會知道Lucene中是不支持像嵌套這種數據結構的,而Elasticsearch不過是在Lucene的基礎之上,通過hack的方式做了一些修改來支持了嵌套結構,使搜索功能更佳強大,用起來更方便。那麼Elasticsearch具體是如何實現的?

ElasticSearch的官方說法:
在這裏插入圖片描述
在這裏插入圖片描述

參考:https://www.elastic.co/guide/en/elasticsearch/reference/7.4/nested.html

翻譯過來大概有如下幾點:

  1. nested類型在索引裏是作爲Document單獨存儲的。nested類型可能是1個包含100數據的數組,那就是100個Document。每多1個nested類型,就多增加對應length的Document.
  2. 普通的query對nested字段查詢無效,必須使用nested Query
  3. highlight高亮也需要使用專門的Query
  4. 對於nested類型的field個數是有限制的, 長度也是有限制的。

從官網的介紹中我們瞭解到,nested的存儲是單獨進行存儲的,這其實也就解釋了爲什麼我們明明寫入了100條數據用CAT接口查看時數據條數時卻顯示要比100多很多的原因了。

那麼爲什麼我們普通的檢索並沒有檢索到nested文檔的內容呢,Elasticsearch是如何把nested過濾掉的?

首先我們先看Elasticsearch是如何將nested進行分開存儲的。

Elasticsearch有一個類org.elasticsearch.index.mapper.DocumentParser,是專門用來解析要索引的Document的。

源碼如下:
在這裏插入圖片描述

我們進入nestedContext方法:
在這裏插入圖片描述

源碼位置:
https://github.com/elastic/elasticsearch/blob/6a5bae184b80c8a0012158c217de340535e9f45c/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java

從源碼中可以看出來,生成nested文檔主要乾了兩件事:

  1. 指定_id值爲父Doc的id,用來關聯
  2. 指定_type值爲以”__”開頭的,標識特定nested 類型。

看完了nested文檔的生成我們知道了nested文檔是和普通文檔一樣被儲存的,但是對外確實隱藏的,我們來看看Elasticsearch是如何做到的。

源碼如下:

在這裏插入圖片描述在這裏插入圖片描述
大家看到這裏應該清楚了,查詢query的判斷決定是否返回nested文檔的依據是文檔中是否包含_primary_term字段,由此可知,nested文檔是不包含_primary_term字段的,而其他文檔是包含這個字段的。

_primary_term:是一個整數,每當Primary Shard發生重新分配時,比如重啓,Primary選舉等,_primary_term會遞增1。

我們下面來看_primary_term是如何添加到字段中的。
源碼如下:
在這裏插入圖片描述

源碼位置:https://github.com/elastic/elasticsearch/blob/3ac6d527a1386d19008cdd08cdbfef265da30f00/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java

到這裏相信大家都看清楚怎麼回事了,那麼有的同學就會說了,前面看到的type爲“__”開頭這個源碼裏沒有處理邏輯呀。那是因爲上面的源碼是目前最新版本(7.x)的源碼,大家都知道7.x版本開始已經不再需要type了。但是大家絕大多數使用的還是7.x以前的版本,筆者特意找了一下早期版本的源碼,實現方式還是有些區別的。
源碼如下:

/**
 * Creates a new non-nested docs query
 * @param indexVersionCreated the index version created since newer indices can identify a parent field more efficiently
 */
public static Query newNonNestedFilter(Version indexVersionCreated) {
    if (indexVersionCreated.onOrAfter(Version.V_6_1_0)) {
        // 6.1.0版本之後。 只保留有_primary_term這個元字段的query
        return new DocValuesFieldExistsQuery(SeqNoFieldMapper.PRIMARY_TERM_NAME);
    } else {
        // 6.1.0版本之前版本
        return new BooleanQuery.Builder()
            .add(new MatchAllDocsQuery(), Occur.FILTER)
            //過濾掉nested query
            .add(newNestedFilter(), Occur.MUST_NOT)  
            .build();
    }
}

public static Query newNestedFilter() {
    // _type以“__”爲前綴
    return new PrefixQuery(new Term(TypeFieldMapper.NAME, new BytesRef("__")));
}

總結來說ElasticSearch對nested隱藏實現方式:

  • 小於6.1.0版本中,過濾掉_type以“__”爲前綴的document
  • 大於等於6.1.0版本中只獲取有 __primary_term Field的document

同時nested也有兩個缺點:

  1. nested無形中增加了索引量,如果不瞭解具體實現,將無法很好的進行文檔劃分和預估。ES限制了Field個數和nested對象的size,避免無限制的擴大。
  2. nested Query 整體性能慢,但比parent/child Query稍快。應從業務上儘可能的避免使用NestedQuery,
    對於性能要求高的場景,應該直接禁止使用。

附:nested 和 parent-child的區別以及使用場景

主要區別:

  • 由於存儲結構的不同,nested和parent-child的方式有不同的應用場景。
  • nested 所有實體存儲在同一個文檔,parent-child模式,子type和父type存儲在不同的文檔裏。
  • 查詢效率上nested要高於parent-child。
  • 更新效率上parent-child要高於nested,更新的時候nested模式下,es會刪除整個文檔再創建,而parent-child只會刪除你更新的文檔在重新創建,不影響其他文檔。

使用場景:

  • nested:在少量子文檔,並且不會經常改變的情況下使用。
    比如:訂單裏面的產品,一個訂單不可能會有成千上萬個不同的產品,一般不會很多,並且一旦下單後,下單的產品是不可更新的。
  • parent-child:在大量文檔,並且會經常發生改變的情況下使用。 比如:用戶的瀏覽記錄,瀏覽記錄會很大,並且會頻繁更新
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章