在现代应用程序开发中,数据建模是非常关键的一步。Elasticsearch作为一个分布式搜索和分析引擎,提供了多种数据建模方式来适应不同的应用需求。除了面向文档的NoSQL数据存储模型外,Elasticsearch还支持嵌套类型和join类型,使得我们可以更好地维护关系型模型数据。本文将介绍嵌套类型和join类型的概念、解决的问题、使用示例、使用建议以及两种方式的对比分析。
1.嵌套类型
nested
类型是一种特殊的object
数据类型,它允许将数组中的对象单独的进行索引和查询。通过将相关数据嵌套在同一文档中,嵌套类型能够保持数据的一致性和原子性。
使用普通的 object
数组不好么? 为什么要使用 nested
类型?
下面用一个示例来说明 nested 类型能解决什么 object
类型解决不了的问题。
# 创建 Mapping
PUT books_index
{
"mappings": {
"properties": {
"book_id": { "type": "keyword" },
"author": {
"properties": {
"first_name": { "type": "keyword" },
"last_name": { "type": "keyword" }
}
}
}
}
}
# 写入书本数据
PUT books_index/_doc/1
{
"book_id": "1",
"author": [
{ "first_name": "zhang", "last_name": "fei" },
{ "first_name": "guan", "last_name": "yu" }
]
}
我们创建了 books_index
索引,其中 author
字段是一个对象,包含了 first_name
和 last_name
两个属性。并且我们写入数据的时候,书本的作者有两个(描述了一对多的关系):zhangfei
和 guanyu
。下面我们来执行一段SQL:
GET books_index/_search
{
"query": {
"bool": {
"must": [
{ "term": { "author.first_name": "zhang" } },
{ "term": { "author.last_name": "yu" } }
]
}
}
}
对于上面的SQL,我们的数据中是没有 zhangyu
这个作者的,但是这个查询却可以命中文档 1,跟我们预期的不一样。
为什么呢?因为 object
被扁平化处理后,其丢失了 first_name
和 last_name
之间的关系,变成了下面这样的关系:
{
"book_id": "1234",
"author.first_name": ["zhang", "guan"],
"author.last_name": ["fei", "yu"]
}
对于这个扁平化数组,原先 first_name
和 last_name
间的对应关系已经不复存在了。所以我们的查询语句在 author.first_name
中匹配了 "zhang",在 author.last_name
匹配了 "yu",自然就命中了文档 1。
解决这个痛点的核心自然就在于 nested 类型。
nested
数据类型可以使对象数组中的对象被独立索引,这样 first_name
和 last_name
间的对应关系就不会丢失了。下面我们来修改一下 Mapping,把 author
的类型定义为 nested
:
# 删除索引
DELETE books_index
# 创建索引,author 类型为 nested
PUT books_index
{
"mappings": {
"properties": {
"book_id": { "type": "keyword" },
"author": {
"type": "nested", # author 定义为 nested 类型的对象
"properties": {
"first_name": { "type": "keyword" },
"last_name": { "type": "keyword" }
}
}
}
}
}
对于上面的新索引,我们在 author
中指定了这个对象的类型为 nested
,在内部 nested
类型将数组中的每个对象索引为单独的隐藏文档,这样数组中的每个对象就可以被单独检索了。For Example:
# nested 数据类型的查询
GET books_index/_search
{
"query": {
"nested": { # 使用 nested 关键字
"path": "author", # path 关键字指定对象名字
"query": {
"bool": {
"must": [
{ "term": { "author.first_name": "zhang" } },
{ "term": { "author.last_name": "fei" } }
]
}
}
}
}
}
使用 nested
关键字指定一个 nested
对象的查询,使用 path
指定 nested
对象的名字。
nested
通过冗余的方式将对象和文档存储在一起,所以查询时的性能是很高的,但在需要更新对象信息的时候需要更新所有包含此对象的文档,例如某个作者的信息更改了,那么所有这个作者的书本文档都要更新。所以 nested
适合查询频繁但更新频率低的场景。
- 嵌套类型只适用于小规模和较简单的数据关系模型,当数据规模和复杂性增加时,可能导致性能下降。
- 查询嵌套类型的数据需要使用嵌套查询来检索相关的子对象。
- 对于大规模的关系型数据模型,传统的关系型数据库可能更适合。
2.Join类型
Join类型允许我们在不同类型的文档之间建立关联。通过定义父文档和子文档的关系,我们可以在父文档和子文档之间进行查询和检索。
Join类型主要用于解决具有层次结构的数据模型。例如,当我们需要表示作者和书籍之间的关系时,我们可以将书籍定义为作者的子文档,建立父子关系。这样一来,在查询作者信息时,我们可以轻松地检索到与之关联的书籍信息。
假设我们有一个文学数据库,需要存储作者和书籍的信息。我们可以创建一个"authors"索引,并定义如下的映射:
2.1 定义join数据类型的字段
PUT /authors
{
"mappings": {
"properties": {
"author_id": { "type": "keyword" },
"name": { "type": "text" },
# books 代表字段名字
"books": {
# 指定 books 字段的类型为join
"type": "join",
# relations 属性用来声明文档之间的父子关系
"relations": {
# 这里的意思是:author 代表父亲 , book 代表儿子。
"author": "book"
}
}
}
}
}
2.2 索引父文档
PUT /authors/_doc/1
{
"author_id": "1",
"name": "Jane Austen",
"books":{
"name":"author"
}
}
这里父文档的 ID 为 1,其中 books 声明了文档类型为 author
(即我们概念里的父文档)。
2.3 索引子文档
PUT /authors/_doc/2?routing=1&refresh=true
{
"book_name": "三国演义",
"price": 49.9,
"books": {
"name": "book",
"parent": "1"
}
}
books
中声明了文档的类型为 book
(即我们概念里的子文档),并且使用 parent
字段指定父文档的 ID。
为了确保查询时的性能,父文档和子文档必须在同一个分片,所以需要强制使用 routing
参数,并且其值为父文档的 ID(如果写入父文档的时候也用 routing
参数,那么需要保证它们的值是一样的)。
2.4 数据检索
获取父文档信息
# 获取父文档
GET authors/_doc/1
# response
{
...
"_source" :
} {
"author_id" : "1",
"name" : "Jane Austen",
"books" : {
"name" : "author"
}
}
可以看到获取的父文档的数据是不包含子文档的信息的,因为父子文档是相互独立的。
获取子文档
GET authors/_doc/2?routing=1
# response
{
...
"_source" : {
"book_name" : "三国演义",
"price" : 49.9,
"books" : {
"name" : "book",
"parent" : "1"
}
}
}
在获取子文档时,如果不加 routing
参数是无法找到对应的子文档的。routing
参数的值为父文档的 ID。
Parent ID 查询
查看一个作者写过的书,可以使用Parent ID
查询。
GET authors/_search
{
"query": {
"parent_id": {
"type": "book",
"id": "1"
}
}
}
Has Child 查询
查询价格大于10元的书有哪些,可以使用 Has Child
查询。Has Child
查询将在子文档中进行条件匹配,然后返回匹配文档对应的父文档的信息。
GET authors/_search
{
"query": {
"has_child": {
"type": "book",
"query": {
"range": {
"price": {
"gte": 10
}
}
}
}
}
}
Has Parent 查询
查询 Jane Austen
编写过哪些书时,可以使用 Has Parent
查询。 Has Parent
查询会在父文档中进行匹配,然后返回匹配文档对应的子文档的信息。
GET authors/_search
{
"query": {
"has_parent": {
"parent_type": "author",
"query": {
"match": {
"name": "Jane Austen"
}
}
}
}
}
使用Join类型时,需要注意以下几点:
- Join类型只适用于少数文档之间的父子关系,不适用于大规模的关联模型。
- 查询Join类型的数据需要使用特定的Join查询,以检索父文档和子文档的相关信息。
- 对于复杂的多层次关系模型,Join类型的使用可能会导致查询性能下降。
3.两种方式的对比分析
嵌套类型和Join类型都提供了在Elasticsearch中维护关系型模型数据的方式,但它们有不同的适用场景和优劣势。
- 性能:嵌套类型通常比Join类型具有更好的查询性能,因为所有相关数据都存储在同一个文档中,无需进行额外的查询操作。然而,当数据规模增大时,嵌套类型的性能可能下降。
- 灵活性:Join类型在处理复杂关系模型时更加灵活,可以处理多层次关系和多对多关系。而嵌套类型适用于简单的一对多关系模型。
- 复杂性:Join类型相对较复杂,需要额外的配置和查询语法,而嵌套类型更加简单易用。
- 可扩展性:当数据规模增大时,嵌套类型的索引大小会增加,可能导致性能下降。而Join类型的性能通常较稳定。
根据具体的应用需求和数据模型的复杂性,选择合适的数据建模方式是很重要的。
nested->官方文档
join->官方文档