Elasticsearch:如何在 Elasticsearch 中存储复杂的关系数据

Elasticsearch:如何在 Elasticsearch 中存储复杂的关系数据_第1张图片

在传统的数据库中,对数据关系的描述无外乎三种:一对一、一对多和多对多关系。 如果有关系相关的数据,我们一般在建表的时候加上主外键。 建立数据链接,然后在查询或者统计中通过 join 恢复或者补全数据,最后得到我们需要的结果数据,然后转换到 Elasticsearch中,如何处理这些关系数据呢?

我们都知道 Elasticsearch 是一个 NoSQL 类型的数据库,弱化了对关系的处理,因为像 Lucene、Elasticsearch、Solor 这样的全文搜索框架对性能的要求更高。 一旦发生 join 操作,性能会很差,所以在使用搜索框架的时候,应该避免把搜索引擎当作关系型数据库来使用。

当然实际数据肯定是有关联的,那么在 Elasticsearch 中如何处理和管理这些关联数据呢?

大家都知道 Elasticsearch 天生支持 JSON 数据是完美的,只要是标准 JSON 结构的数据,不管多复杂,不管嵌套多少层,都可以存储在 Elasticsearch 中,然后可以查询分析,检索。在该机制中,处理和管理关系的方式主要有以下三种:

1)使用 object 和 array[object] 字段类型自动存储多层结构的 JSON 数据

这是 Elasticsearch 默认的机制,也就是我们没有设置任何 mapping,直接往 Elasticsearch 服务器插入一个复杂的 JSON 数据,也能插入成功,而且可以支持检索,(可以这样是因为 Elasticsearch 默认是动态的 mapping ,只要插入标准的 JSON 结构就会自动转换,当然我们也可以控制映射类型,Elasticsearch 支持动态映射和静态映射,静态映射也分严格类型,弱类型,通用类型,不再在这里展开。有兴趣的可以到官网了解)如下数据之一:

PUT cars/_doc/1
{
  "name": "Zach",
  "car": [
    {
      "maker": "Saturn",
      "model": "SL"
    },
    {
      "maker": "Subaru",
      "model": "Imprezza"
    }
  ]
}

我们在 Kibana 中直接打入上面的命令,我们可以查看这个 cars 索引的 mapping。

GET cars/_mapping

上面的命令返回的结果:

{
  "cars": {
    "mappings": {
      "properties": {
        "car": {
          "properties": {
            "maker": {
              "type": "text",
              "fields": {
                "keyword": {
                  "type": "keyword",
                  "ignore_above": 256
                }
              }
            },
            "model": {
              "type": "text",
              "fields": {
                "keyword": {
                  "type": "keyword",
                  "ignore_above": 256
                }
              }
            }
          }
        },
        "name": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    }
  }
}

生成的存储结构类似于以下内容:

{  
  "name"  :  "Zach" ,  
  "car.maker"  : [ "Saturn" , "Subaru" ]  
  "car.model"  : [ "SL" ,  "Imprezza" ]  
}

因为 Elasticsearch 的底层 Lucene 是天然支持多值存储的,所以看起来像上面的数组结构。 实际上,Elasticsearch 是作为一个多值字段存储在这个字段中的。

这样的数据实际上包含了数据和关系。 它看起来像一个一对多的关系。 一个人拥有多辆汽车。 但其实并不是严格的关系,因为 Lucene 底层是平放存储的,所以多辆车的数据其实是混在一起的 数据,你不能根据人名来返回其中的一辆车,因为整个数据是一个整体,无论什么操作都会返回整个数据。

上述的数据结够,在有些时候是很有用的,但是它不能维护 car.maker 及 car.model 的对应关系。比如我们查询 car.maker 为 Subaru 时,它不能返回 car.model 为 Imprezza。

更多阅读,请参阅我之前的文章 “Elasticsearch: object 及 nested 数据类型”。

2)使用 nested[object] 类型来存储具有多级关系的数据

Elasticsearch:如何在 Elasticsearch 中存储复杂的关系数据_第2张图片

在上面的场景中,我们指出了 array 中存储的数组对象并不是严格相关的,因为第二层的数据没有分离。 如果要分离,则必须使用 nested 类型显式定义数据结构。 只有这样,第二层的多辆汽车数据才相互独立,也就是说可以单独获取或查询某辆汽车的数据。Nested 类型是 object 数据类型的特殊版本。它允许对象数组以一种可以彼此独立查询的方式进行索引

同样的 JSON 数据:


  "name": "Zach",
  "car": [
    {
      "maker": "Saturn",
      "model": "SL"
    },
    {
      "maker": "Subaru",
      "model": "Imprezza"
    }
  ]

如果我们使用上面的 object 来进行存储的话,那么 Elasticsearch 将把整个信息当做一个整体进行存储。如果我们把 car 数据定义为 nested 数据类型,它的形式如下:

PUT cars
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "car": {
        "type": "nested",
        "properties": {
          "maker": {
            "type": "keyword"
          },
          "model": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

如上所示, car 被定义为 nested 数据类型。最终 Elasticsearch 显示的存储为3个:1 个是 root 文档,另外两个是 car 数组中的两个文档。查询的时候可以独立查询,性能还不错,缺点是更新的代价比较大,每次子文档更新都要重建整个结构的索引,所以 nested 适用于嵌套多级关系不经常更新的场景。

Nested 类型的数据,需要使用其指定的查询和聚合方式才能生效,普通的 Elasticsearch 查询只能查询 1 级或根级属性,nested 属性无法查询,如果要查询,必须使用 embedded 的 Set 查询或聚合。

嵌套应用程序有两种模式:

  • 嵌套查询:每个查询在单个文档中有效,包括排序
  • 嵌套聚合或过滤:同级别所有文档全局有效,包括过滤排序

更多阅读:Elasticsearch: object 及 nested 数据类型

3)父/子关系

父/子模式与嵌套非常相似,但应用侧重点不同。

在使用 parent/children 管理关系时,Elasticsearch 会在每个 shard 的内存中维护一张关系表。 检索时,关联数据由 has_parent 和 has_child 过滤器获取。 在这种模式下,使用父文档和子文档。 也是独立的,查询性能会比嵌套模式略低,因为插入时父文档和子文档会通过路由分布在同一个 shard,但不保证在同一个Lucene sengment index segment,所以检索性能略低。 此外,每次检索 Elasticsearch 时,都需要从内存关系表中获取数据关联信息。 也需要一定的时间。 嵌套的好处是更新父文档或子文档。 不影响其他文档,所以更新频繁的多级关系使用 parent/children 模式是最合适的。

在 Elasticsearch 中,Join 可以让我们创建 parent/child 关系。Elasticsearch 不是一个 RDMS。通常 join 数据类型尽量不要使用,除非不得已。那么 Elasticsearch 为什么需要 Join 数据类型呢?

在 Elasticsearch 中,更新一个 object 需要 root object 一个完整的 reindex:

  • 即使是一个 field 的一个字符的改变
  • 即便是 nested object 也需要完整的 reindex 才可以实现搜索

通常情况下,这是完全 OK 的,但是在有些场合下,如果我们有频繁的更新操作,这样可能对性能带来很大的影响。

join 数据类型可以完全地把两个 object 分开,但是还是保持这两者之前的关系。

  1. parent 及 child 是完全分开的两个文档
  2. parent 可以单独更新而不需要重新 reindex child
  3. children 可以任意被添加/串改/删除而不影响 parent 及其它的 children

与 nested 类型类似,父子关系也允许你将不同的实体关联在一起,但它们在实现和行为上有所不同。 与 nested 文档不同,它们不在同一文档中,而 parent/child 文档是完全独立的文档。 它们遵循一对多关系原则,允许你将一种类型定义为 parent 类型,将一种或多种类型定义为 child 类型

即便 join 数据类型给我们带来了方便,但是,它也在搜索时给我带来额外的内存及计算方便的开销。

join 数据类型是一个特殊字段,用于在同一索引的文档中创建父/子关系。 关系部分定义文档中的一组可能关系,每个关系是父(parent)名称和子(child)名称。 

一个例子:

PUT my_index
{
  "mappings": {
    "properties": {
      "my_join_field": { 
        "type": "join",
        "relations": {
          "question": "answer" 
        }
      }
    }
  }
}

在这里我们定义了一个叫做 my_index 的索引。在这个索引中,我们定义了一个 field,它的名字是 my_join_field。它的类型是 join 数据类型。同时我们定义了单个关系:question 是 answer 的 parent。

要使用 join 来 index 文档,必须在 source 中提供关系的 name 和文档的可选 parent。 例如,以下示例在 question 上下文中创建两个 parent 文档:

PUT my_index/_doc/1?refresh
{
  "text": "This is a question",
  "my_join_field": {
    "name": "question" 
  }
}
 
PUT my_index/_doc/2?refresh
{
  "text": "This is another question",
  "my_join_field": {
    "name": "question"
  }
}

更多阅读:Elasticsearch:Join 数据类型,Elasticsearch:在 Elasticsearch 中的 join 数据类型父子关系。

总结

方法一:

  • 简单、快速、高性能
  • 善于维持一对一的关系
  • 无需特别查询

方法二:

  • 由于底层存储在同一个 Lucene sengment 中,因此读取和查询性能比较方法更快。
  • 更新单个子文档会重建整个数据结构,所以不适合更新频繁嵌套的场景。
  • 可以维护一对多和多对多的存储关系

方法三:

  • 多关系数据,存储完全独立,但存在于同一个分片中,因此读取和查询性能略低于第二种方式。
  • 需要额外内存,维护管理关系表
  • 更新文档不会影响其他子文档,适合更新频繁使用的场景。
  • 排序和打分操作繁琐,需要额外的脚本函数支持
  • 每种方式都有自己适合的应用场景,所以在实践中,我们需要根据实际业务场景选择合适的存储方式

你可能感兴趣的:(Elasticsearch,Elastic,elasticsearch,大数据,搜索引擎,全文检索,big,data)