本文以Elasticsearch 6.8.4版本為例,介紹Elasticsearch嵌套文檔的使用。
最近一段時間都在搞Elasticsearch搜尋相關的工作,總結一下搜尋知識點供大家參考。
在Elasticsearch取消了多個索引内建立多個type的機制,由于場景需要,是以調研了嵌套文檔和父子文檔
以文章和文章留言為例,嵌套文檔都在一個文檔内,而父子文檔則分開存儲了父文檔與子文檔,本文我們來學習嵌套文檔的使用。
1、嵌套文檔
嵌套文檔看似與文檔内有一個集合字段類似,但是實則有很大差別,以上面圖中嵌套文檔為例,留言1,留言2,留言3雖然都在目前文章所在的文檔内,但是在内部其實存儲為4個獨立文檔,如下圖所示。
同時,嵌套文檔的字段類型需要設定為nested,設定成nested後的不能被直接查詢,需要使用nested查詢,這裡不做具體介紹,詳細檢視1.2。
1.1 建立索引
接下來,介紹一下如何建立嵌套文檔索引,比如有這樣的資料,如下:
{
"title": "這是一篇文章",
"body": "這是一篇文章,從哪裡說起呢? ... ...",
"comments": [
{
"name": "張三",
"comment": "寫的不錯",
"age": 28,
"date": "2020-05-04"
},
{
"name": "李四",
"comment": "寫的很好",
"age": 20,
"date": "2020-05-04"
},
{
"name": "王五",
"comment": "這是一篇非常棒的文章",
"age": 31,
"date": "2020-05-01"
}
]
}
建立索引名和type均為blog的索引,其中comments字段為嵌套文檔類型,需要将type設定為nested,其餘都是一些正常的字段,建立索引語句如下:
PUT
http://localhost:9200/blog/{
"mappings": {
"blog": {
"properties": {
"comments": {
"type": "nested",
"properties": {
"date": {
"type": "date"
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"comment": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"age": {
"type": "long"
}
}
},
"body": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
}
如下圖所示
1.2 插入資料
将1.1中示例的資料插入blog索引,對嵌套文檔來說,插入沒什麼特别的,如下:
http://localhost:9200/blog/blog/1/{
"title":"這是一篇文章",
"body":"這是一篇文章,從哪裡說起呢? ... ...",
"comments":[
{
"name":"張三",
"comment":"寫的不錯",
"age":28,
"date":"2020-05-04"
},
{
"name":"李四",
"comment":"寫的很好",
"age":20,
"date":"2020-05-04"
},
{
"name":"王五",
"comment":"這是一篇非常棒的文章",
"age":31,
"date":"2020-05-01"
}
]
}
如圖所示:
1.3 查詢
在前面說到,使用嵌套文檔時,直接查詢nested文檔時查詢不到的,這裡試一下,先查詢一下根文檔的内容(文章内容),查詢title包含‘文章’的内容:
POST
http://localhost:9200/blog/blog/_search/{
"query": {
"bool": {
"filter": [
{
"bool": {
"must": [
{
"match_phrase": {
"title": {
"query": "文章"
}
}
}
]
}
}
]
}
}
}
Elasticsearch-Head,如下圖所示
接下來我們查詢一下,留言中name為張三的資料,查詢如下:
{
"query": {
"bool": {
"filter": [
{
"bool": {
"must": [
{
"match_phrase": {
"comments.name": {
"query": "張三"
}
}
}
]
}
}
]
}
}
}
Elasticsearch-Head 如下圖所示
這裡舉例,我們要查詢title中包含‘文章’且留言name中包含‘張三’的資料,使用如下查詢:
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "文章"
}
},
{
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{
"match": {
"comments.name": "張三"
}
}
]
}
}
}
}
]
}
}
}
其實從查詢語句中可以看出,nested中查詢的是嵌套文檔的内容,文法與正常查詢時一緻。
使用嵌套文檔時,文檔的分數計算需要注意,參考官方文檔的描述:
nested 查詢肯定可以比對到多個嵌套的文檔。每一個比對的嵌套文檔都有自己的相關度得分,但是這衆多的分數最終需要彙聚為可供根文檔使用的一個分數。
預設情況下,根文檔的分數是這些嵌套文檔分數的平均值。可以通過設定 score_mode 參數來控制這個得分政策,相關政策有 avg (平均值), max (最大值), sum (加和) 和 none (直接傳回 1.0 常數值分數)。
1.4 排序
可能有一些場景需要按照嵌套文檔的字段記性排序,舉例:
為了符合上述場景,新增兩條資料:
http://localhost:9200/blog/blog/2/{
"title": "這是一篇文章2",
"body": "這是一篇文章2,從哪裡說起呢? ... ...",
"comments": [
{
"name": "張三",
"comment": "寫的不錯",
"age": 28,
"date": "2020-05-11"
},
{
"name": "李四",
"comment": "寫的很好",
"age": 20,
"date": "2020-05-16"
},
{
"name": "王五",
"comment": "這是一篇非常棒的文章",
"age": 31,
"date": "2020-05-01"
}
]
}
http://localhost:9200/blog/blog/3/ {
"title": "這是一篇文章3",
"body": "這是一篇文章3,從哪裡說起呢? ... ...",
"comments": [
{
"name": "張三",
"comment": "寫的不錯",
"age": 28,
"date": "2020-05-03"
},
{
"name": "李四",
"comment": "寫的很好",
"age": 20,
"date": "2020-05-20"
},
{
"name": "王五",
"comment": "這是一篇非常棒的文章",
"age": 31,
"date": "2020-05-01"
}
]
}
查詢title中包含‘文章’且留言name中包含‘張三’,并且按照留言date字段倒序排序,查詢語句如下:
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "文章"
}
},
{
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{
"match": {
"comments.name": "張三"
}
}
]
}
}
}
}
]
}
},
"sort": {
"comments.date": {
"order": "desc",
"mode": "max",
"nested_path": "comments",
"nested_filter": {
"bool": {
"must": [
{
"match": {
"comments.name": "張三"
}
}
]
}
}
}
}
}
需要注意的是,在sort内,又添加了nested_filter來過濾一遍上面嵌套文檔的查詢條件,原因是這樣的,在嵌套文檔查詢排序時是先按照條件進行查詢,查詢後再進行排序,那麼可能由于資料的原因,導緻排序的字段不是按照比對上的資料進行排序,比如這是本文正确的結果,如下圖所示(為了友善檢視,使用圖表展示的資料)。
如果我們去掉nested_filter,在查詢,由于文章3中李四評論的日期是20号,導緻這條記錄排在了最前面,這就是為什麼使用nested_filter的原因,查詢結果如下:
1.5 聚合
聚合的場景可能也比較常見,其實熟悉上面嵌套文檔的使用的話,對聚合文檔使用難度應該也不大,
新增一條資料:
http://localhost:9200/blog/blog/4/{
"title": "這是一篇文章4",
"body": "這是一篇文章4,從哪裡說起呢? ... ...",
"comments": [
{
"name": "張三",
"comment": "寫的不錯",
"age": 28,
"date": "2020-03-03"
},
{
"name": "李四",
"comment": "寫的很好",
"age": 20,
"date": "2020-04-20"
},
{
"name": "王五",
"comment": "這是一篇非常棒的文章",
"age": 31,
"date": "2020-06-01"
}
]
}
舉例:需要查詢每個月評論人數的平均數,查詢語句如下:
{
"size": 0,
"aggs": {
"comments": {
"nested": {
"path": "comments"
},
"aggs": {
"by_month": {
"date_histogram": {
"field": "comments.date",
"interval": "month",
"format": "yyyy-MM"
},
"aggs": {
"avg_stars": {
"avg": {
"field": "comments.age"
}
}
}
}
}
}
}
}
結果如下圖所示:
1.6 使用建議
- 正如本文所說,嵌套文檔中,所有内容都在同一個文檔内,這就導緻嵌套文檔進行增加、修改或者删除時,整個文檔都要重新被索引。嵌套文檔越多,這帶來的成本就越大。當時就是由于這個原因,最終沒有選擇使用嵌套文檔。
- 嵌套文檔的分數計算問題需要注意,可以參考本文1.3最後部分。