Elasticsearch 8.X 如何依据 Nested 嵌套类型的某个字段进行排序?

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: Elasticsearch 8.X 如何依据 Nested 嵌套类型的某个字段进行排序?

1、问题来源

这是来自社区的一个真实企业场景问题。

https://elasticsearch.cn/question/13135

如下所示, 希望在查出的结果后, 对结果进行后处理,对tags列表,根据depth进行排序。

{
"keyProperty":"22",
"name":"测试内容",
"_class":"com.xxxxxxxx.ElasticSearchContent",
"contentType":"attractionArea",
"content":"这是一条测试内容",
"timestamp":1701325254191,
"tags":[
{
"path":"33^^35^^36^^38",
"depth":4,
"id":38,
"label":"测试42"
},
{
"path":"33^^35^^36^^37^^39",
"depth":5,
"id":39,
"label":"测试51"
},
{
"path":"33^^35",
"depth":2,
"id":35,
"label":"测试22"
}
]
}

2、分析一下

Elasticsearch 能支持的排序方式罗列如下:

包含但不限于:

  1. 基于特定字段的排序
  2. 基于Nested对象字段的排序
  3. 基于特定脚本实现的排序

等等......

参见:

https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#nested-sorting

再看咱们的开篇需求,

字段排序分类中的:基于特定字段的排序和基于 Nested 对象字段的排序,是对整个查询结果集进行排序,这在 Elasticsearch 中通常是针对顶层文档字段或者简单嵌套字段进行的。

而咱们开篇需求的应用场景和实现方式与之是不同的,哪咋办?

见招拆招了,只能考虑基于特定脚本实现的排序了。

要实现开篇的需求——即对每个文档的 tags 列表进行排序,需要在返回结果中对这些 tags 列表进行处理。

通常有两大类方案:

  • 使用脚本字段(script_fields)实现;
  • 在查询结果返回后在客户端进行处理,大白话:自己Java或Python程序层面处理。

3、尝试拆解实现

咱们要先模拟构造数据,包含创建索引和bulk 批量构造写入数据两个部分。

创建索引如下:

PUT /example_index
{
  "mappings": {
    "properties": {
      "keyProperty": {
        "type": "keyword"
      },
      "name": {
        "type": "text"
      },
      "_class": {
        "type": "keyword"
      },
      "contentType": {
        "type": "keyword"
      },
      "content": {
        "type": "text"
      },
      "timestamp": {
        "type": "date"
      },
      "tags": {
        "type": "nested",
        "properties": {
          "path": {
            "type": "keyword"
          },
          "depth": {
            "type": "integer"
          },
          "id": {
            "type": "integer"
          },
          "label": {
            "type": "text"
          }
        }
      }
    }
  }
}

导入数据:

POST /example_index/_bulk
{"index":{"_id":1}}
{"keyProperty":"22","name":"测试内容1","_class":"com.xxxxxxxx.ElasticSearchContent","contentType":"attractionArea","content":"这是一条测试内容","timestamp":1701325254191,"tags":[{"path":"33^^35^^36^^38","depth":4,"id":38,"label":"测试42"},{"path":"33^^35^^36^^37^^39","depth":5,"id":39,"label":"测试51"},{"path":"33^^35","depth":2,"id":35,"label":"测试22"}]}
{"index":{"_id":2}}
{"keyProperty":"23","name":"测试内容2","_class":"com.xxxxxxxx.ElasticSearchContent","contentType":"attractionArea","content":"这是另一条测试内容","timestamp":1701325254200,"tags":[{"path":"33^^35^^36","depth":5,"id":36,"label":"测试33"},{"path":"33^^35^^37","depth":3,"id":37,"label":"测试34"}]}

3.1 方案一:脚本字段(script_fields)实现自建排序

GET /example_index/_search
{
  "query": {
    "nested": {
      "path": "tags",
      "query": {
        "match_all": {}
      }
    }
  },
  "script_fields": {
    "sorted_tags": {
      "script": {
        "lang": "painless",
        "source": """
        if (!params._source.tags.empty) {
          def tags = new ArrayList(params._source.tags);
          boolean swapped;
          do {
            swapped = false;
            for (int i = 0; i < tags.size() - 1; i++) {
              if (tags[i].depth > tags[i + 1].depth) {
                def temp = tags[i];
                tags[i] = tags[i + 1];
                tags[i + 1] = temp;
                swapped = true;
              }
            }
          } while (swapped);
          return tags;
        } else {
          return null;
        }
      """
      }
    }
  }
}

召回结果如下:

有人可能会说,这不是扯吗?都整出个冒泡排序来了。

是的,就是传统的数组排序的脚本实现。当没有办法的时候,不考虑性能的时候,笨办法也是办法。

在 Elasticsearch 中处理大量数据时运行复杂的脚本可能会消耗较多的计算资源!

还有,冒泡排序是一种效率较低的排序算法,特别是对于大列表,其性能不是最佳的。

相比于使用 Elasticsearch 内置的排序功能,手动实现排序算法增加了脚本的复杂性。

3.2 方案二:脚本字段实现自建排序——lamda表达式排序

GET /example_index/_search
{
  "query": {
    "nested": {
      "path": "tags",
      "query": {
        "match_all": {}
      }
    }
  },
  "script_fields": {
    "sorted_tags": {
      "script": {
        "lang": "painless",
        "source": """
          if (!params._source.tags.empty) {
            def tags = new ArrayList(params._source.tags);
            tags.sort((a, b) -> a.depth.compareTo(b.depth));
            return tags;
          } else {
            return null;
          }
        """
      }
    }
  },
  "size": 10
}

这里使用了一个 lambda 表达式 (a, b) -> a.depth.compareTo(b.depth)。最后,返回排序后的 tags。

参见:

https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-fields.html

执行结果如下:

3.3 方案三:业务层面代码实现

啥意思,召回检索结果后,自己在内存里排序,想使用什么排序自便,按照自己习惯就可以。当然,Elastic中文社区创始人 、极限科技 CEO medcl 大佬也给出了他的网关方案:

写个 JS 脚本,通过极限网关,无缝的对查询结果进行改写就行了:

https://infinilabs.com/docs/latest/gateway/tutorial/path_rewrite_by_javascript/

这其实给复杂查询给了另一个更高维度升维的思考,值得借鉴。

4、小结

将问题展开,才能找到解决问题的方案。

当实现方案变得非常复杂,涉及性能问题时候,数据量少都没有问题;数据量大后,可以考虑找其他方案。

你如果也有类似困惑,欢迎交流。

推荐阅读


更短时间更快习得更多干货!

和全球 近2000+ Elastic 爱好者一起精进!

比同事抢先一步学习进阶干货!


相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
3月前
|
SQL JSON 大数据
ElasticSearch的简单介绍与使用【进阶检索】 实时搜索 | 分布式搜索 | 全文搜索 | 大数据处理 | 搜索过滤 | 搜索排序
这篇文章是Elasticsearch的进阶使用指南,涵盖了Search API的两种检索方式、Query DSL的基本语法和多种查询示例,包括全文检索、短语匹配、多字段匹配、复合查询、结果过滤、聚合操作以及Mapping的概念和操作,还讨论了Elasticsearch 7.x和8.x版本中type概念的变更和数据迁移的方法。
ElasticSearch的简单介绍与使用【进阶检索】 实时搜索 | 分布式搜索 | 全文搜索 | 大数据处理 | 搜索过滤 | 搜索排序
|
4月前
|
自然语言处理 关系型数据库 数据库
ElasticSearch 映射类型及数据类型区分
ElasticSearch 映射类型及数据类型区分
52 0
|
6月前
|
Oracle 关系型数据库 API
实时计算 Flink版产品使用合集之当sink到elasticsearch时,可以指定es的指定字段吗
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStreamAPI、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
实时计算 Flink版产品使用合集之当sink到elasticsearch时,可以指定es的指定字段吗
|
4月前
|
数据库
面试题ES问题之Elasticsearch的排序分页和高亮功能如何解决
面试题ES问题之Elasticsearch的排序分页和高亮功能如何解决
38 0
|
5月前
|
SQL 安全 数据挖掘
Elasticsearch如何聚合查询多个统计值,如何嵌套聚合?并相互引用,统计索引中某一个字段的空值率?语法是怎么样的?
Elasticsearch聚合查询用于复杂数据分析,包括统计空值率。示例展示了如何计算字段`my_field`非空非零文档的百分比。查询分为三步:总文档数计数、符合条件文档数计数及计算百分比。聚合概念涵盖度量、桶和管道聚合。脚本在聚合中用于动态计算。常见聚合类型如`sum`、`avg`、`date_histogram`等。组合使用可实现多值统计、嵌套聚合和空值率计算。[阅读更多](https://zhangfeidezhu.com/?p=515)
297 0
Elasticsearch如何聚合查询多个统计值,如何嵌套聚合?并相互引用,统计索引中某一个字段的空值率?语法是怎么样的?
|
5月前
|
存储 索引
Elasticsearch中父子文档的关联:利用Join类型赋予文档的层级关系
Elasticsearch中父子文档的关联:利用Join类型赋予文档的层级关系
|
5月前
|
存储 索引
Elasticsearch索引之嵌套类型:深度剖析与实战应用
Elasticsearch索引之嵌套类型:深度剖析与实战应用
|
6月前
|
存储 机器学习/深度学习 搜索推荐
Elasticsearch 8.X 向量检索和普通检索能否实现组合检索?如何实现?
Elasticsearch 8.X 向量检索和普通检索能否实现组合检索?如何实现?
161 3
|
6月前
|
SQL Oracle 关系型数据库
实时计算 Flink版产品使用合集之源MySQL表新增字段后,要同步这个改变到Elasticsearch的步骤是什么
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
6月前
|
SQL 关系型数据库 数据库
实时计算 Flink版产品使用合集之将数据写入Elasticsearch时,若Elasticsearch中的字段类型为date,对应的SQL类型应该是什么
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。