数据建模:nested & join

2023年 7月 14日 37.3k 0

在现代应用程序开发中,数据建模是非常关键的一步。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_namelast_name 两个属性。并且我们写入数据的时候,书本的作者有两个(描述了一对多的关系):zhangfeiguanyu。下面我们来执行一段SQL:

GET books_index/_search
{
  "query": {
    "bool": {
      "must": [
        { "term": { "author.first_name": "zhang" } },
        { "term": { "author.last_name": "yu" } }
      ]
    }
  }
}

对于上面的SQL,我们的数据中是没有 zhangyu 这个作者的,但是这个查询却可以命中文档 1,跟我们预期的不一样。

为什么呢?因为 object 被扁平化处理后,其丢失了 first_namelast_name 之间的关系,变成了下面这样的关系:

{
    "book_id": "1234",
    "author.first_name": ["zhang", "guan"],
    "author.last_name": ["fei", "yu"]
}

对于这个扁平化数组,原先 first_namelast_name 间的对应关系已经不复存在了。所以我们的查询语句在 author.first_name 中匹配了 "zhang",在 author.last_name 匹配了 "yu",自然就命中了文档 1。

解决这个痛点的核心自然就在于 nested 类型。

nested 数据类型可以使对象数组中的对象被独立索引,这样 first_namelast_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->官方文档

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论