Elasticsearch學習-嵌套文檔

本文以Elasticsearch 6.8.4版本爲例,介紹Elasticsearch嵌套文檔的使用。

image

最近一段時間都在搞Elasticsearch搜索相關的工作,總結一下搜索知識點供大家參考。

在Elasticsearch取消了多個索引內創建多個type的機制,由於場景需要,所以調研了嵌套文檔父子文檔

image

以文章和文章留言爲例,嵌套文檔都在一個文檔內,而父子文檔則分開存儲了父文檔與子文檔,本文我們來學習嵌套文檔的使用。

1、嵌套文檔

嵌套文檔看似與文檔內有一個集合字段類似,但是實則有很大區別,以上面圖中嵌套文檔爲例,留言1,留言2,留言3雖然都在當前文章所在的文檔內,但是在內部其實存儲爲4個獨立文檔,如下圖所示。

image

同時,嵌套文檔的字段類型需要設置爲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"
            }
          }
        }
      }
    }
  }
}

如下圖所示

image

1.2 插入數據

將1.1中示例的數據插入blog索引,對嵌套文檔來說,插入沒什麼特別的,如下:

PUT 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"
        }
    ]
}

如圖所示:

image

image

1.3 查詢

在前面說到,使用嵌套文檔時,直接查詢nested文檔時查詢不到的,這裏試一下,先查詢一下根文檔的內容(文章內容),查詢title包含‘文章’的內容:

POST http://localhost:9200/blog/blog/_search/

{
  "query": {
    "bool": {
      "filter": [
        {
          "bool": {
            "must": [
              {
                "match_phrase": {
                  "title": {
                    "query": "文章"
                  }
                }
              }
            ]
          }
        }
      ]
    }
  }
}

Elasticsearch-Head,如下圖所示

image

接下來我們查詢一下,留言中name爲張三的數據,查詢如下:

{
  "query": {
    "bool": {
      "filter": [
        {
          "bool": {
            "must": [
              {
                "match_phrase": {
                  "comments.name": {
                    "query": "張三"
                  }
                }
              }
            ]
          }
        }
      ]
    }
  }
}

Elasticsearch-Head 如下圖所示

image

這裏舉例,我們要查詢title中包含‘文章’且留言name中包含‘張三’的數據,使用如下查詢:

POST http://localhost:9200/blog/blog/_search/

{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "文章"
          }
        },
        {
          "nested": {
            "path": "comments",
            "query": {
              "bool": {
                "must": [
                  {
                    "match": {
                      "comments.name": "張三"
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

Elasticsearch-Head 如下圖所示

image

其實從查詢語句中可以看出,nested中查詢的是嵌套文檔的內容,語法與正常查詢時一致。

使用嵌套文檔時,文檔的分數計算需要注意,參考官方文檔的描述:

nested 查詢肯定可以匹配到多個嵌套的文檔。每一個匹配的嵌套文檔都有自己的相關度得分,但是這衆多的分數最終需要匯聚爲可供根文檔使用的一個分數。

默認情況下,根文檔的分數是這些嵌套文檔分數的平均值。可以通過設置 score_mode 參數來控制這個得分策略,相關策略有 avg (平均值), max (最大值), sum (加和) 和 none (直接返回 1.0 常數值分數)。

1.4 排序

可能有一些場景需要按照嵌套文檔的字段記性排序,舉例:

爲了符合上述場景,新增兩條數據:

PUT 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"
    }
  ]
}

PUT 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來過濾一遍上面嵌套文檔的查詢條件,原因是這樣的,在嵌套文檔查詢排序時是先按照條件進行查詢,查詢後再進行排序,那麼可能由於數據的原因,導致排序的字段不是按照匹配上的數據進行排序,比如這是本文正確的結果,如下圖所示(爲了方便查看,使用圖表展示的數據)。

image

如果我們去掉nested_filter,在查詢,由於文章3中李四評論的日期是20號,導致這條記錄排在了最前面,這就是爲什麼使用nested_filter的原因,查詢結果如下:

image

1.5 聚合

聚合的場景可能也比較常見,其實熟悉上面嵌套文檔的使用的話,對聚合文檔使用難度應該也不大,

新增一條數據:

PUT 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"
    }
  ]
}

舉例:需要查詢每個月評論人數的平均數,查詢語句如下:

POST http://localhost:9200/blog/blog/_search/

{
  "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"
              }
            }
          }
        }
      }
    }
  }
}

結果如下圖所示:

image

1.6 使用建議

  • 正如本文所說,嵌套文檔中,所有內容都在同一個文檔內,這就導致嵌套文檔進行增加、修改或者刪除時,整個文檔都要重新被索引。嵌套文檔越多,這帶來的成本就越大。當時就是由於這個原因,最終沒有選擇使用嵌套文檔。
  • 嵌套文檔的分數計算問題需要注意,可以參考本文1.3最後部分。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章