一文帶你掌握Elasticsearch復(fù)合字段類型:Array、Flattened、Nested在業(yè)務(wù)系統(tǒng)中最佳實踐
1.概述
elasticsearch在當下互聯(lián)網(wǎng)系統(tǒng)中使用非常廣泛,是一個非常熱門的組件框架。那elasticsearch到底是什么呢?
elasticsearch 是一個分布式、實時的搜索和分析引擎,通常用于處理和查詢大規(guī)模的結(jié)構(gòu)化與非結(jié)構(gòu)化數(shù)據(jù)。它基于 Apache Lucene 構(gòu)建,具有高效的全文搜索、數(shù)據(jù)存儲、數(shù)據(jù)分析和數(shù)據(jù)可視化功能。elasticsearch 主要應(yīng)用于日志分析、監(jiān)控、數(shù)據(jù)檢索、實時分析等場景。
官方文檔地址:https://www.elastic.co/
2.映射mapping
在 elasticsearch 中,mapping(映射)是指為索引中的字段定義結(jié)構(gòu)、類型和規(guī)則。類似于關(guān)系型數(shù)據(jù)庫中的表結(jié)構(gòu),映射決定了 elasticsearch 如何存儲和索引數(shù)據(jù),其作用和關(guān)系型數(shù)據(jù)庫定義表字段差不多:
定義字段類型:例如字符串、數(shù)字、日期等。
控制字段行為:決定字段是否進行分詞、是否索引、如何排序等。
優(yōu)化查詢性能:正確的映射能提高查詢效率,減少不必要的存儲。
數(shù)據(jù)驗證:映射可以確保寫入的文檔符合預(yù)期的數(shù)據(jù)結(jié)構(gòu)和類型。
映射mapping分為:動態(tài)映射和靜態(tài)映射
2.1 動態(tài)映射
顧名思義,就是自動創(chuàng)建出來的映射。es 根據(jù)存入的文檔,自動分析出來文檔中字段的類型以及存儲方式,這種就是動態(tài)映射。示例如下所示:
首先,先定義一個索引,名為user_info
PUT user_info
一個簡單的DSL語句就建立了一個索引,不太清楚的先跳轉(zhuǎn)上面的鏈接入門學習下哈。
緊接著我們直接插入一條es文檔數(shù)據(jù)
PUT user_info/_doc/1
{
"id":1,
"name":"張三",
"amount":88.99
}
插入成功,再來看看索引user_info的mapping定義:
GET user_info/_mapping
查詢結(jié)果如下:
{
"user_info" : {
"mappings" : {
"properties" : {
"amount" : {
"type" : "float"
},
"id" : {
"type" : "long"
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
可以看到,es在我們插入文檔數(shù)據(jù)時自動檢測字段類型動態(tài)定義了mapping結(jié)構(gòu)。
2.2 靜態(tài)映射
靜態(tài)映射就和關(guān)系型數(shù)據(jù)庫表結(jié)構(gòu)加字段一樣,在插入es文檔數(shù)據(jù)之前,先定義字段mapping再插入數(shù)據(jù),比如我們往user_info添加一個身份證號idCard:
PUT user_info/_mapping
{
"properties": {
"idCard": {
"type": "keyword"
}
}
}
keyword關(guān)鍵字類型代表不分詞,elasticsearch會對text字段進行分詞實現(xiàn)全文檢索,但是身份證并不需要分詞,它是一個整體屬性字段,如果是通過動態(tài)映射生成mapping定義,那么會和上面的name字段一樣:
"idCard" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
這里我解釋一下這個mapping的定義:其中既包含 text 類型字段用于全文搜索,也包含 keyword 字段用于精確匹配和聚合。這種設(shè)計方式可以在同一個字段上實現(xiàn)不同類型的查詢
text 類型用于全文搜索(如 matchQuery),Elasticsearch 會對 text 類型字段進行分詞(默認使用標準分詞器)。
keyword是這是 idCard 字段的子字段類型,用于精確匹配和聚合。
keyword 類型的字段不會被分詞,適合存儲如ID、狀態(tài)、標簽等字符串數(shù)據(jù)。
**ignore_above: 256**:如果字段的長度超過 256 個字符,將會被忽略,不會被索引
這種組合讓你可以在同一個字段上支持全文搜索和精確匹配,從而滿足不同查詢需求。但實際上name字段也是并不需要分詞的,直接定義keyword更加合理,性能更好。分詞是一個相對比較耗費性能的操作。
由此可見動態(tài)映射也并不是萬能可行的,我個人建議是:
我們在創(chuàng)建真正索引之前,可以先隨便創(chuàng)建一個測試索引,然后寫入測試的es文檔數(shù)據(jù),然后查看測試索引生成的mapping,這樣后面我們在創(chuàng)建真正索引設(shè)置mapping時,就可以在前面得到的mapping基礎(chǔ)上修改,不需要從無到有一個一個字段地開始寫mapping,極大提高效率。如果是往索引添加新字段時,我覺得還是嚴格按照和關(guān)系型數(shù)據(jù)庫表結(jié)構(gòu)加字段一樣,采用靜態(tài)映射插入數(shù)據(jù)前,先手動添加字段mapping定義,杜絕動態(tài)映射帶來的類型不匹配、性能降低等問題。
3.使用數(shù)組Array實現(xiàn)一對多查詢
在平時業(yè)務(wù)系統(tǒng)表單列表搜索中,一對多查詢是非常常見的,比如說,一個用戶有多個標簽,有多個角色,這時候我們會搜索某些標簽或者某些角色下有哪些用戶,是不是很常見呀???話不多說開干,我這里以標簽展示,首先我們按照建議先通過靜態(tài)映射在索引user_info中添加一個新字段tag存儲標簽id:
PUT user_info/_mapping
{
"properties": {
"tag": {
"type": "long"
}
}
}
接下來我們就可以插入數(shù)據(jù)了。這時候你可能會問,你這個定義tag字段是long類型,并沒有定義array類型呀???這就可以插入數(shù)組多個數(shù)據(jù)了?是的,elasticsearch關(guān)于mapping字段介紹時,明確提到你無需專門聲明某字段為數(shù)組類型,任何字段都可以隱式地存儲為數(shù)組。但是要求數(shù)組中的所有值必須具有相同的數(shù)據(jù)類型。不支持混合數(shù)據(jù)類型的數(shù)組: [10,“ some string”],插入數(shù)據(jù)示例:
PUT user_info/_doc/10
{
"id":10,
"name":"張三2",
"tag":[1,2,3]
}
PUT user_info/_doc/11
{
"id":11,
"name":"張三3",
"tag":[3,4]
}
PUT user_info/_doc/12
{
"id":12,
"name":"張三4",
"tag":[4,8,9]
}
PUT user_info/_doc/13
{
"id":13,
"name":"張三5",
"tag":[1,6]
}
然后我們來搜索試試吧,搜索有標簽id為3或者4的用戶:
GET user_info/_search
{
"query": {
"terms": {
"tag": [3,4]
}
}
}
查詢結(jié)果id=10,11,12都命中了。
Java查詢實現(xiàn)代碼:
boolQueryBuilder.must(QueryBuilders.termsQuery("tag", tagIds));
關(guān)于Spring Boot整合restHighLevelClient實現(xiàn)es相關(guān)操作,請查看上面的入門篇
如果要查詢同時包含標簽id為3和4的員工:
GET user_info/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"tag": {
"value": 3
}
}
},
{
"term": {
"tag": {
"value": 4
}
}
}
]
}
}
}
只命中id=11這一條數(shù)據(jù)
Java實現(xiàn)代碼:
for(tagId : tagIds) {
boolQueryBuilder.must(QueryBuilders.termQuery("tag", tagId));
}
數(shù)組Array的限制:無法建立數(shù)組元素之間的關(guān)系
PUT orders/_doc/1
{
"products": ["laptop", "mouse"],
"quantities": [1, 2]
}
假設(shè)你想查詢購買了 "laptop" 且數(shù)量為 1 的訂單。這在 Elasticsearch 的數(shù)組查詢中無法實現(xiàn),因為 Elasticsearch 無法判斷 "laptop" 和 1 是相關(guān)聯(lián)的(它們只被視為兩個獨立的數(shù)組字段)。
解決方案:如果你需要這種關(guān)聯(lián)關(guān)系,應(yīng)使用 nested 類型來代替數(shù)組字段。后面會講。
總的來說,數(shù)組(Arrays)字段類型適用于簡單的多值字段,如標簽、興趣、角色等場景。使用簡單、查詢高效、
項目推薦:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企業(yè)級系統(tǒng)架構(gòu)底層框架封裝,解決業(yè)務(wù)開發(fā)時常見的非功能性需求,防止重復(fù)造輪子,方便業(yè)務(wù)快速開發(fā)和企業(yè)技術(shù)棧框架統(tǒng)一管理。引入組件化的思想實現(xiàn)高內(nèi)聚低耦合并且高度可配置化,做到可插拔。嚴格控制包依賴和統(tǒng)一版本管理,做到最少化依賴。
4.使用Flattened存儲動態(tài)字段
Flattened 類型是 Elasticsearch 7.3 引入的一種特殊的字段類型,主要用于處理層級結(jié)構(gòu)(嵌套結(jié)構(gòu))數(shù)據(jù),但以扁平化方式存儲,以節(jié)省內(nèi)存并提高查詢效率。它特別適用于存儲動態(tài)的或高可變的鍵值對數(shù)據(jù),例如日志、自定義字段等。
該類型會將嵌套 JSON 對象的鍵值扁平化為單一層級字段。開搞?。。∠榷ㄒ粋€字段存儲用戶附加信息:
PUT user_info/_mapping
{
"properties": {
"extendInfo": {
"type": "flattened"
}
}
}
插入數(shù)據(jù),我們在上面的數(shù)據(jù)更新字段extendInfo信息:
POST user_info/_update/10
{
"doc":{
"extendInfo":{
"age":18,
"gender":"女",
"height":180,
"weight":"68kg"
}
}
}
POST user_info/_update/11
{
"doc":{
"extendInfo":{
"age":30,
"gender":"女"
}
}
}
POST user_info/_update/12
{
"doc":{
"extendInfo":{
"height":170,
"weight":"58kg",
"hobby":"籃球"
}
}
}
POST user_info/_update/13
{
"doc":{
"extendInfo":{
"address":"杭州",
"phone":"12334464"
}
}
}
可以看到我們插入的extendInfo的信息都不太一樣,主打就是一個動態(tài)字段信息。接下來查詢看看:
GET user_info/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"extendInfo.gender": {
"value": "女"
}
}
},
{
"term": {
"extendInfo.age": {
"value": 18
}
}
}
]
}
}
}
查詢結(jié)果會命中id=10這條數(shù)據(jù)
flattened 類型是一種折中的選擇,適合存儲動態(tài)鍵值對數(shù)據(jù),如日志、標簽和指標等。它提供了比 object 類型更好的性能,并且比 nested 類型占用更少的資源。然而,它無法進行復(fù)雜的嵌套關(guān)聯(lián)查詢和深層聚合,因此需要根據(jù)業(yè)務(wù)需求進行合理選擇。
5.使用Nested存儲對象數(shù)組
Nested:嵌套類型,有點千呼萬喚始出來的感覺,他到底能干嘛呢?
nested 類型允許 Elasticsearch 以“獨立文檔”方式存儲每個嵌套對象,并維持每個對象內(nèi)字段之間的關(guān)系。這樣可以確保查詢到的數(shù)據(jù)和文檔結(jié)構(gòu)一致,避免出現(xiàn)跨對象匹配的情況。
上面我們提到數(shù)據(jù)Array類型的限制是無法建立兩個數(shù)組字段直接的關(guān)系,而Flattened是存儲一個對象動態(tài)字段,Nested就是可以存儲數(shù)據(jù)對象,并能維護數(shù)組對象之間關(guān)系,來看看示例,一個用戶有多個家人family:
PUT user_info/_mapping
{
"properties": {
"family": {
"type": "nested"
}
}
}
寫入數(shù)據(jù),基于上面的數(shù)據(jù)寫入family即可:
POST user_info/_update/10
{
"doc":{
"family":[
{
"name":"李四",
"relation":"父子",
"amount":10000
},
{
"name":"王婆",
"relation":"母子",
"amount":6000
}
]
}
}
POST user_info/_update/11
{
"doc":{
"family":[
{
"name":"李四",
"relation":"兄弟",
"amount":10000
},
{
"name":"小紅",
"relation":"夫妻",
"amount":1000
}
]
}
}
POST user_info/_update/12
{
"doc":{
"family":[
{
"name":"王五",
"relation":"父子",
"amount":10000
},
{
"name":"小蘭",
"relation":"兄妹",
"amount":1000
}
]
}
}
接下來我們查詢 relation=父子 and amount=10000的數(shù)據(jù):
GET user_info/_search
{
"query": {
"nested": {
"path": "family",
"query": {
"bool": {
"must": [
{
"term": {
"family.relation.keyword": {
"value": "父子"
}
}
},
{
"term": {
"family.amount": {
"value": 10000
}
}
}
]
}
}
}
}
}
查詢結(jié)果命中了id=10,12兩條數(shù)據(jù)。你可能注意到查詢family.relation后面接了keyword,這是因為我們嵌套字段對象屬性是動態(tài)生成的,family.relation自動映射成text,對text字段進行term精確匹配,必須接上關(guān)鍵字keyword
nested 類型在存儲和查詢時比普通 Flattened 類型更耗資源,因為每個嵌套對象會被當作單獨文檔存儲和索引,并且文檔不能直接更新嵌套對象的部分字段,通常需要重建整個嵌套對象。
nested 類型適合嵌套對象數(shù)組的結(jié)構(gòu)化數(shù)據(jù),支持精確的對象內(nèi)匹配查詢和聚合。當嵌套對象只需要簡單查詢、沒有嚴格的對象內(nèi)關(guān)聯(lián)需求時,使用 Flattened 類型更高效。
6.總結(jié)
上面我們講了各自的定義、使用、局限性,最后這里總結(jié)概括下:
字段類型 | 適用場景 | 優(yōu)點 | 缺點 |
Array | 適合簡單的多值字段(如標簽、愛好) | 易用、性能好 | 無法維護元素之間的關(guān)系 |
Flattened | 動態(tài)鍵值對結(jié)構(gòu)(如可變的配置、元數(shù)據(jù)) | 占用存儲空間少、動態(tài)字段支持好 | 不支持嵌套關(guān)系查詢 |
Nested | 多層級結(jié)構(gòu)對象,且需要查詢對象內(nèi)部關(guān)系 | 支持復(fù)雜的關(guān)系查詢 | 性能和存儲開銷大 |
簡單來說:
- Array 簡單快捷,適合不需要關(guān)系的多值字段。
- Flattened 提供了一種高效存儲動態(tài)鍵值對的方式,但不適合復(fù)雜查詢。
- Nested 適合復(fù)雜的嵌套關(guān)系查詢,但會增加存儲和查詢的開銷。
根據(jù)你的業(yè)務(wù)需求,合理選擇字段類型可以顯著提升系統(tǒng)的查詢性能和數(shù)據(jù)管理效率。