12、ES实战:复合查询

本学习笔记基于ElasticSearch 7.10版本,旧版本已经废弃的查询功能暂时不做笔记,以后有涉及到再做补充。
参考官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/joining-queries.html

关系型数据库中有表的关联关系,在 ElasticSearch 中,也会有类似的需求,例如订单表和商品表,我们可以使用以下两种方式实现:

  • 嵌套文档(nested)
  • 父子文档

1、嵌套文档

嵌套文档 nested 在之前介绍字段类型的笔记中有学习过:ElasticSearch学习笔记六(字段类型 Field data types),这里不重复介绍。

现在,假设有一个电影文档,每个电影都有演员信息:

# 需要将 actors 定义为 nested 类型,否则字段中的关联关系会丢失
PUT movies
{
   
     
  "mappings": {
   
     
    "properties": {
   
     
      "actors": {
   
     
        "type": "nested"
      }
    }
  }
}

PUT movies/_doc/1
{
   
     
  "name": "霸王别姬",
  "actors": [
    {
   
     
     "name": "张国荣",
     "gender": "男"
    },
    {
   
     
     "name": "巩俐",
     "gender": "女"
    }
  ]
}

嵌套类型的缺点:
首先我们来看一下 movies 索引中的文档数量

GET _cat/indices?v

我们只插入了一篇文档,但是文档数量却变成3:
*
这是因为 nested 文档在 ElasticSearch 内部其实也是独立的 lucene 文档,也就是说actors 在内部单独保存为两份文档。只是在我们查询的时候,ElasticSearch 内部帮我们做了 join 处理,所以最终看起来就像一个独立文档一样。

如果nested 文档中的数据比较多时,可能会生成多分嵌套文档,所以这种方案性能并不是特别好。

此外,nested 文档更新的时候,也会更新所有的嵌套文档,比较耗性能。

2、嵌套查询

使用嵌套查询 nested 来查询嵌套文档:

GET movies/_search
{
   
     
  "query": {
   
     
    "nested": {
   
     
      "path": "actors",
      "query": {
   
     
        "bool": {
   
     
          "must": [
            {
   
     
              "match": {
   
     
                "actors.name": "张国荣"
              }
            },
            {
   
     
              "match": {
   
     
                "actors.gender": "男"
              }
            }
          ]
        }
      }
    }
  }
}

3、父子文档

nested 文档中,嵌套的文档只能是一对一的关系,不能复用。比如“张国荣”还演过其他电影,那只能在每一部电影的 actors 中再添加一次。

而父子文档就可以解决这种问题,相比于嵌套文档,主要有如下优势:

  • 更新父文档时,不会重新索引子文档。
  • 创建、修改或者删除子文档时,不会影响父文档或其他子文档。
  • 子文档可以作为搜索结果独立返回。

例如学生和班级的关系:

PUT stu_class
{
   
     
  "mappings": {
   
     
    "properties": {
   
     
      "name": {
   
     
        "type": "keyword"
      },
      "s_c": {
   
     
        "type": "join",
        "relations": {
   
     
          "class": "student"
        }
      }
    }
  }
}

解释一下几个参数的意义:

  • s_c :表示定义父子文档的字段名,可以自定义。
  • join :表示这是一个父子文档。
  • relations :里面分别定义 class 是 parent 父文档,student 是 child 子文档。

插入两个父文档:

PUT stu_class/_doc/1
{
   
     
  "name": "一班",
  "s_c": {
   
     
    "name": "class"
  }
}
PUT stu_class/_doc/2
{
   
     
  "name": "二班",
  "s_c": {
   
     
    "name": "class"
  }
}

插入三个子文档:

PUT stu_class/_doc/3?routing=1
{
   
     
  "name": "zhangsan",
  "s_c": {
   
     
    "name": "student",
    "parent": 1
  }
}
PUT stu_class/_doc/4?routing=1
{
   
     
  "name": "lisi",
  "s_c": {
   
     
    "name": "student",
    "parent": 1
  }
}
PUT stu_class/_doc/5?routing=2
{
   
     
  "name": "wangwu",
  "s_c": {
   
     
    "name": "student",
    "parent": 2
  }
}

注意: 子文档需要和父文档在同一个分片上,而 ElasticSearch 默认使用文档 _id 进行哈希计算后决定存在哪个分片上,所以 routing 关键字的值为父文档的 id 即可。

另外,一个索引只能定义一个 join 字段,可以向一个已经存在的 join 字段上新增关系。

3.1、has_child

通过子文档查询父文档使用 has_child 查询:

GET stu_class/_search
{
   
     
  "query": {
   
     
    "has_child": {
   
     
      "type": "student",
      "query": {
   
     
        "match": {
   
     
          "name": "lisi"
        }
      }
    }
  }
}

3.2、has_parent

通过父文档查询子文档使用 has_parent 查询:

GET stu_class/_search
{
   
     
  "query": {
   
     
    "has_parent": {
   
     
      "parent_type": "class",
      "query": {
   
     
        "match": {
   
     
          "name": "一班"
        }
      }
    }
  }
}

注意: 使用 has_parent 查询不进行评分,这时可以考虑使用 parent_id 查询子文档:

GET stu_class/_search
{
   
     
  "query": {
   
     
    "parent_id": {
   
     
      "type": "student",
      "id": 1
    }
  }
}

4、小结

1、 普通子对象实现一对多(不使用nested或父子文档),会损失子文档的边界,子对象之间的关联关系丢失;
2、 nested可以解决关联关系丢失的问题,但是有两个缺点:更新主文档的时候要全部更新,不支持子文档属于多个主文档;
3、 父子文档则可以解决nested的问题,但是主要适用于写多读少的场景;

版权声明:

本文仅记录ElasticSearch学习心得,如有侵权请联系删除。
更多内容请访问原创作者:江南一点雨
*

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: