Elasticsearch 从入门到实践 小册笔记

Mapping

JSON 中是可以嵌套对象的,保存对象类型可以用 object 类型,但实际上在 ES 中会将原 JSON 文档扁平化存储的。假如作者字段是一个对象,那么可以表示为:

{
  "author": {
    "first":"zhang",
    "last":"san"
  }
}

但实际上,ES 在存储的时候会转化为以下格式存储:

{
  "author.first": "zhang",
  "author.last": "san"
}

对于数组来说,ES 并没有定义关键字来表示一个字段为数组类型。默认的情况下,任何一个字段都可以包含 0 个或者多个值,只要这些值是相同的数据类型。所以我们在创建数据的时候可以直接写入数组类型:

PUT books/_doc/3
{
  "author": ["Neil Matthew","Richard Stones"],
}

doc_values

Doc values 是基于列式存储的结构,在索引数据的时候创建。它存储的值与 _source 中的值相同,使用列式存储结构使得 Doc values 在处理聚合、排序操作上更高效。Doc values 支持几乎所有的类型字段,但是 text 和 annotated_text 除外。

Doc values 默认是开启的,保存 Doc values 结构需要很大的空间开销,如果某个字段不需要排序、聚合、使用脚本访问,那么应该禁用此字段的 Doc values 来节省磁盘空间。

嵌套类型

在传统的关系型数据库领域,想要表达关系型模型是非常自然的,但在 ES 里要处理这个事情就并不那么简单了。

在 ES 中可以保存关系型模型数据的方式主要有以下两种:

  • nested:在这种方式中,会将一对多的关系保存在同一个文档中。
  • join(Parent / Child) :通过维护文档的父子关系,将两个对象分离。

上述的这两种方式都可以描述一对多的关系。

nested 类型是一种特别的 object 数据类型,其允许数组中的对象可以被单独索引,使它们可以被独立地检索。下面的示例是使用普通的 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": "1234",
  "author": [
    { "first_name": "zhang", "last_name": "san" },
    { "first_name": "wang", "last_name": "wu" }
  ]
}

如上面的示例,我们创建了 books_index 索引,其中 author 字段是一个对象,包含了 first_name 和 last_name 两个属性。并且我们写入数据的时候,书本的作者有两个(描述了一对多的关系):zhangsan 和 wangwu。下面我们来执行一段查询:

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

如上查询示例,你会发现我们的数据中是没有 zhangwu 这个作者的,但是这个查询却可以命中文档 1,跟我们预期的不一样。

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

{
    "book_id": "1234",
    "author.first_name": ["zhang", "wang"],
    "author.last_name": ["san", "wu"]
}

对于这个扁平化数组,原先 first_name 和 last_name 间的对应关系已经不复存在了。所以我们的查询语句在 author.first_name 中匹配了 "zhang",在 author.last_name 匹配了 "wu",自然而然就命中了文档 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 类型将数组中的每个对象索引为单独的隐藏文档,这样数组中的每个对象就可以被单独检索了。nested 数据类型的检索示例如下:

# nested 数据类型的查询
GET books_index/_search
{
  "query": {
    "nested": { # 使用 nested 关键字
      "path": "author", # path 关键字指定对象名字
      "query": {
        "bool": {
          "must": [
            { "term": { "author.first_name": "zhang" } },
            { "term": { "author.last_name": "san" } }
          ]
        }
      }
    }
  }
}

如上示例,使用 nested 关键字指定一个 nested 对象的查询,使用 path 指定 nested 对象的名字。

从上面的示例来看,nested 通过冗余的方式将对象和文档存储在一起,所以查询时的性能是很高的,但在需要更新对象信息的时候需要更新所有包含此对象的文档,例如某个作者的信息更改了,那么所有这个作者的书本文档都要更新。所以 nested 适合查询频繁但更新频率低的场景。

父子文档

除了提供 nested 来描述一对多关系外,ES 还提供了 join 数据类型来表达关系型数据模型。

join 数据类型允许在一个索引中的文档创建父子关系,通过维护父子文档的关系独立出来两个对象。父文档和子文档是相互独立的,通过类似引用的关系进行绑定,所以当父文档更新时,不需要更新子文档,而子文档可以被任意添加、修改、删除而不会影响到父文档和其他子文档。

需要注意的是,为了维护父子文档的关系需要占用额外的内存资源,并且读取性能相对较差。但由于父子文档是互相独立的,所以适合子文档更新频率高的场景

为了确保查询时的性能,父文档和子文档必须在同一个分片,所以需要强制使用 routing 参数,并且其值为父文档的 ID(如果写入父文档的时候也用 routing 参数,那么需要保证它们的值是一样的)。
在获取子文档时,如果不加 routing 参数是无法找到对应的子文档的。routing 参数的值为父文档的 ID。

posted @ 2023-01-31 20:42  Dazzling!  阅读(74)  评论(0编辑  收藏  举报