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

1、问题来源

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

https://elasticsearch.cn/question/13135

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

go 复制代码
{
"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

再看咱们的开篇需求,

  • 第一:检索返回结果;

  • 第二:基于结果的 tags 数组下的子字段 depth 进行排序。

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

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

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

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

通常有两大类方案:

  • 使用脚本字段(script_fields)实现;

  • 在查询结果返回后在客户端进行处理,大白话:自己Java或Python程序层面处理。

3、尝试拆解实现

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

创建索引如下:

go 复制代码
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"
          }
        }
      }
    }
  }
}

导入数据:

go 复制代码
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)实现自建排序

go 复制代码
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表达式排序

go 复制代码
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、小结

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

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

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

推荐阅读

  1. 全网首发!从 0 到 1 Elasticsearch 8.X 通关视频

  2. 重磅 | 死磕 Elasticsearch 8.X 方法论认知清单

  3. 如何系统的学习 Elasticsearch ?

  4. 2023,做点事

  5. 干货 | Elasticsearch Nested 数组大小求解,一网打尽!

  6. Elasticsearch Nested 选型,先看这一篇!

  7. 干货 | 拆解一个 Elasticsearch Nested 类型复杂查询问题

  8. 干货 | Elasticsearch Nested类型深入详解

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

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

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

相关推荐
拍客圈5 小时前
宝塔面板屏蔽垃圾搜索引擎蜘蛛和扫描工具的办法
搜索引擎
源码技术栈7 小时前
SaaS基于云计算、大数据的Java云HIS平台信息化系统源码
java·大数据·云计算·云his·his系统·云医院·区域his
Elastic 中国社区官方博客7 小时前
Elasticsearch 索引副本数
大数据·数据库·elasticsearch·搜索引擎·全文检索
Eternity......8 小时前
SparkSQL基本操作
大数据·spark
枫叶落雨2228 小时前
下载的旧版的jenkins,为什么没有旧版的插件
运维·jenkins
268572598 小时前
Elasticsearch 初步认识
大数据·elasticsearch·搜索引擎·全文检索·es
python算法(魔法师版)8 小时前
网络编程入门(一)
大数据·网络·网络协议·计算机网络
caihuayuan59 小时前
生产模式下react项目报错minified react error #130的问题
java·大数据·spring boot·后端·课程设计
兔子坨坨10 小时前
详细了解HDFS
大数据·hadoop·hdfs·big data
夏旭泽11 小时前
系统架构-大数据架构设计
大数据·系统架构