我們在使用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
翻譯過來大概有如下幾點:
- nested類型在索引裏是作爲Document單獨存儲的。nested類型可能是1個包含100數據的數組,那就是100個Document。每多1個nested類型,就多增加對應length的Document.
- 普通的query對nested字段查詢無效,必須使用nested Query
- highlight高亮也需要使用專門的Query
- 對於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文檔主要乾了兩件事:
- 指定_id值爲父Doc的id,用來關聯
- 指定_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也有兩個缺點:
- nested無形中增加了索引量,如果不瞭解具體實現,將無法很好的進行文檔劃分和預估。ES限制了Field個數和nested對象的size,避免無限制的擴大。
- 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:在大量文檔,並且會經常發生改變的情況下使用。 比如:用戶的瀏覽記錄,瀏覽記錄會很大,並且會頻繁更新