复杂数据建模与嵌套查询详解

195 阅读8分钟

在本篇博客中,我们将进入 Elasticsearch 学习的第三阶段:复杂数据处理和建模。通过学习嵌套对象、父子关系、数组处理等,深入了解如何建模和管理复杂的数据结构。同时,我们将探讨如何在查询中使用这些复杂的数据模型,帮助你构建更加灵活和高效的搜索解决方案。

第三阶段:复杂数据建模与查询

0. 前置知识:Elasticsearch 常见数据类型

在深入探讨复杂的数据建模之前,我们先介绍几种复杂的 Elasticsearch 数据类型及其适用的业务场景:

  1. Object 类型

    • 描述object 类型用于表示简单的嵌套结构。字段被作为 JSON 对象存储在文档中,但这些对象的属性会被扁平化存储,因此在查询时可能会出现属性之间关系的混淆。
    • 场景示例:例如,一个用户的地址包含街道、城市和邮编,我们可以用 object 类型表示这些属性。
    {
      "address": {
        "type": "object",
        "properties": {
          "street": { "type": "text" },
          "city": { "type": "keyword" },
          "zipcode": { "type": "keyword" }
        }
      }
    }
    

    解释object 类型适合简单结构,不要求每个属性在查询时保持独立。

  2. Nested 类型

    • 描述nested 类型用于存储嵌套对象,将子对象作为独立的文档进行存储,从而保证查询时每个属性的独立性。
    • 场景示例:例如,书籍信息中包含多个作者,每个作者有不同的属性。这些属性需要保持独立性,不混淆不同作者的字段。
    {
      "authors": {
        "type": "nested",
        "properties": {
          "name": { "type": "text" },
          "contribution": { "type": "text" }
        }
      }
    }
    

    解释:使用 nested 类型,确保每个作者和其贡献在查询时被独立对待。

  3. Join 类型(父子关系)

    • 描述join 类型允许在文档之间创建父子关系。适用于需要独立更新子文档的大数据场景。
    • 场景示例:例如,一个书籍的评论和书籍信息分离存储,每个评论关联一本书,可以用 join 类型定义父子关系。
    {
      "my_join_field": {
        "type": "join",
        "relations": {
          "book": "review"
        }
      }
    }
    

    解释join 类型适合需要建立父子关系但保持数据独立存储的场景。

  4. Array 类型

    • 描述:虽然 Elasticsearch 没有显式的 array 类型,但你可以将任何字段定义为数组,这意味着字段可以存储多个值。
    • 场景示例:例如,书籍的标签字段,可以包含多个标签。
    {
      "tags": {
        "type": "keyword"
      }
    }
    

    解释tags 字段可以包含多个值,例如 "technology""elasticsearch",方便对多标签进行查询。

1. 嵌套对象与嵌套查询

在 Elasticsearch 中,文档可以包含复杂的数据结构,例如嵌套对象。当文档的某个字段包含多条记录时,嵌套对象的使用是一个不错的选择。例如,考虑一个订单的索引,其中每个订单包含多个订单项,每个订单项有不同的属性,这时候就可以使用嵌套对象。

1.1 嵌套对象的定义

嵌套对象允许我们在索引中保存复杂的数据结构并进行精确查询。嵌套对象与普通的对象类型不同,它们会作为独立的文档被存储。

示例:嵌套对象的映射(订单与订单项)

在电商系统中,一个订单通常包含多个订单项,每个订单项有商品名称、数量和价格等属性。可以将订单项作为嵌套对象进行建模,以便精确查询每个订单项的属性。

PUT /orders
{
  "mappings": {
    "properties": {
      "order_id": {
        "type": "keyword"
      },
      "customer_name": {
        "type": "text"
      },
      "order_items": {
        "type": "nested",
        "properties": {
          "product_name": {
            "type": "text" 
          },
          "quantity": {
            "type": "integer"
          },
          "price": {
            "type": "float"
          }
        }
      }
    }
  }
}

解释

  • order_items 被定义为 nested 类型,表示每个订单包含多个订单项。每个订单项都有 product_namequantityprice 等属性。
1.2 嵌套查询的使用

嵌套查询用于查询嵌套对象中的数据。例如,如果你想查询包含某个特定商品且数量大于 2 的订单,可以使用 nested 查询:

GET /orders/_search
{
  "query": {
    "nested": {
      "path": "order_items",
      "query": {
        "bool": {
          "must": [
            { "match": { "order_items.product_name": "Smartphone X" } },
            { "range": { "order_items.quantity": { "gt": 2 } } }
          ]
        }
      }
    }
  }
}

解释

  • nested 查询 确保在查询时每个订单项的属性是相关联的,这样可以精确地找到包含指定商品且数量大于 2 的订单。
  • path:指定嵌套对象的路径为 order_items
  • bool 查询:使用 must 子句来组合多个条件,确保 product_name"Smartphone X"quantity 大于 2

2. 父子关系(Parent-Child Relationship)

在某些情况下,嵌套对象不能满足所有的数据需求,特别是当数据量非常大且需要独立更新子文档时。这种情况下,使用父子关系模型是更好的选择。父子关系允许在不同类型之间建立关联,而不需要将所有数据保存在一个文档中。

2.1 父子关系的定义

通过 join 字段,可以在 Elasticsearch 中建立父子关系。例如,定义一个父类型 product 和一个子类型 review,每个 review 都与一个 product 相关联:

PUT /ecommerce
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "my_join_field": {
        "type": "join",
        "relations": {
          "product": "review"
        }
      }
    }
  }
}

解释

  • my_join_field 定义了父子关系,其中 product 是父类型,review 是子类型。
2.2 父子关系的详细业务示例:电商平台中的商品与评论

业务场景: 考虑一个电子商务网站,用户可以对商品进行评论。在这种情况下,商品和评论之间有非常明确的层次关系:商品是父文档,评论是子文档。使用父子关系可以方便管理评论而不需要在商品文档中进行大量的数据更新。

1. 创建索引及父子关系定义
PUT /ecommerce
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "my_join_field": {
        "type": "join",
        "relations": {
          "product": "review"
        }
      }
    }
  }
}

解释

  • my_join_field:定义了 product(商品)是父文档,而 review(评论)是子文档。
2. 插入父文档(商品)

接下来,我们添加一件商品到索引中。

POST /ecommerce/_doc/1
{
  "name": "Smartphone X",
  "my_join_field": {
    "name": "product"
  }
}

解释

  • name:商品名称。
  • my_join_field:声明该文档是 product 类型的父文档。
3. 插入子文档(评论)

然后,我们为该商品插入一条评论文档,将其作为子文档。

POST /ecommerce/_doc/2?routing=1
{
  "review_text": "Great smartphone, very responsive.",
  "rating": 5,
  "my_join_field": {
    "name": "review",
    "parent": "1"
  }
}

解释

  • review_text:评论内容。
  • rating:评分。
  • my_join_field:声明该文档是 review,且与父文档 ID 为 1 的商品关联。
  • routing:通过 routing 参数指定该子文档与父文档有关联,确保父子文档存储在同一分片中。
4. 查询具有特定评论的商品

为了查找包含高评分评论(例如评分为 5)的商品,我们可以使用 has_child 查询。

GET /ecommerce/_search
{
  "query": {
    "has_child": {
      "type": "review",
      "query": {
        "term": {
          "rating": 5
        }
      }
    }
  }
}

解释

  • has_child:用于查找包含满足条件的子文档的父文档。在这里,我们查找包含评分为 5 的评论的商品。
5. 查询某个评论对应的商品信息

如果我们有评论的内容,并想查找它对应的商品,可以使用 has_parent 查询。

GET /ecommerce/_search
{
  "query": {
    "has_parent": {
      "parent_type": "product",
      "query": {
        "match": {
          "name": "Smartphone X"
        }
      }
    }
  }
}

解释

  • has_parent:用于查找与父文档匹配条件相关联的子文档。在这里,我们查找所有父文档名称为 Smartphone X 的评论。

通过这种方式,父子关系可以让我们保持数据的独立性与灵活性。在电商场景中,商品和评论可以独立管理,评论可以频繁添加和更新,而不会影响商品的整体文档结构。父子关系还避免了单一文档过于复杂,方便进行高效的存储和查询。

3. 数组与对象的处理

Elasticsearch 支持将数组作为字段进行存储。数组字段可以存储多个值,但要注意,数组中的所有值会被作为同一个字段的一部分存储和查询。

3.1 数组字段的定义与查询

数组字段的定义非常简单,例如:

PUT /library
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text"
      },
      "tags": {
        "type": "keyword"
      }
    }
  }
}

解释

  • tags 是一个 keyword 数组,可以包含多个标签。
示例:查询包含多个特定标签的书籍
GET /library/_search
{
  "query": {
    "terms": {
      "tags": ["technology", "elasticsearch"]
    }
  }
}

解释

  • terms 查询用于查找包含指定数组值的文档,多个值的匹配是 OR 逻辑,这意味着如果文档中包含任意一个指定的标签,它都会被检索出来。

总结

在这篇博客中,我们详细探讨了 Elasticsearch 中复杂数据建模的三种方式:嵌套对象、父子关系和数组处理。通过这些结构,我们可以更加灵活地管理复杂数据,并使用嵌套查询、父子查询等来高效地检索数据。在实际应用中,理解这些概念并根据业务需求选择合适的数据建模方式,可以显著提升 Elasticsearch 的使用效率和查询性能。

希望这篇博客对你理解复杂数据结构及其查询有帮助。如果你有任何疑问或需要进一步的案例分析,欢迎在评论区留言交流!