[ElasticSearch]分析京东商城商品搜索实现|聚合|全文查找|搜索引擎|ES Java High Level Rest Client|ES Java API Client

背景

商城搜索功能在电子商务平台上扮演着至关重要的角色,它直接影响到用户的购物体验和平台的转化率。一个高效、准确的搜索系统能够迅速帮助用户找到他们想要的商品,提高用户满意度和平台的用户黏性。当涉及到全文匹配复杂的搜索需求时,传统的数据库查询往往难以满足这些要求,因此,许多电商平台选择基于搜索引擎的解决方案,如Elasticsearch,来构建他们的搜索系统。

Elasticsearch 背景介绍

Elasticsearch 是一个基于 Lucene 的开源搜索和分析引擎,它提供了一个分布式、多租户能力的全文搜索引擎,具有 HTTP 网络接口和无模式的 JSON 文档。Elasticsearch 的设计目标是让全文搜索变得简单而快速,同时提供接近实时的搜索和分析。

Elasticsearch 在商城搜索中的应用

  1. 全文搜索:Elasticsearch 允许你索引整个文本字段,并提供高效的搜索查询。在商城中,这意味着用户可以在搜索框中输入任何关键词,系统都能迅速找到包含这些关键词的商品。
  2. 实时搜索:Elasticsearch 提供了接近实时的搜索体验。当新的商品上架或商品信息发生变化时,这些变化会立即反映到搜索结果中,确保用户始终看到最新的信息。
  3. 复杂的查询和过滤:Elasticsearch 支持多种查询类型,如布尔查询、范围查询、模糊查询等,以及丰富的过滤功能。这允许商城根据用户的搜索习惯和偏好,构建复杂的搜索逻辑,提供更加个性化的搜索结果。
  4. 搜索建议:Elasticsearch 的自动补全和搜索建议功能可以提高搜索的便捷性和效率。当用户输入搜索词时,系统可以实时提供相关的搜索建议,帮助用户更快地找到他们想要的商品。
  5. 高性能和可扩展性:Elasticsearch 是一个分布式系统,可以水平扩展到数百个节点,以支持大规模的数据和查询。这使得 Elasticsearch 成为处理大规模商城搜索需求的理想选择。
  6. 丰富的分析功能:除了搜索之外,Elasticsearch 还提供了丰富的分析功能,如数据聚合、可视化等。这些功能可以帮助商城更好地理解用户行为和市场趋势,为业务决策提供支持。

在商城搜索类需求中,使用 Elasticsearch 作为搜索引擎是一个明智的选择。它提供了高效、准确的全文搜索功能,支持复杂的查询和过滤,同时提供了接近实时的搜索体验。此外,Elasticsearch 的高性能和可扩展性也使其成为处理大规模商城搜索需求的理想解决方案。

Elasticsearch版本选择

Spring Data Elasticsearch官网给出的版本对照矩阵:Spring Data Elasticsearch / versions

在这里插入图片描述

从版本对照矩阵得知对应于Spring Framework 5.x的最新的Elasticsearch版本是7.17.3,之后的是针对Spring Framework 6.x版本的使用

基于Spring Boot 2.x(基于Spring Framework 5.x)开发,因此Elasticsearch选择使用7.17.3版本,Java版本Java 8+(包括Java 8)

SpringBoot 3.x (基于Spring Framework 6)要求Java最低版本Java 17

SpringBoot 2.x (基于Spring Framework 5) 要求Java最低版本Java 8

1. SpringBoot 3.x与 2.x差别

Spring Boot 3.x与2.x之间存在一些显著的区别和新特性,以下是对这些区别的详细概述:

  1. Java版本要求
    • Spring Boot 3.x要求至少使用Java 17作为最低版本要求,并且已经通过了Java 19的测试,确保了更好的兼容性和性能。
    • 相比之下,Spring Boot 2.x则基于Java 8或更高版本
  2. Spring Framework版本
    • Spring Boot 3.x基于最新的Spring Framework 6构建,提供了更好的性能和功能。
    • Spring Boot 2.x则基于Spring Framework 5.x
  3. GraalVM支持和原生镜像
    • Spring Boot 3.x引入了对GraalVM的支持,允许开发者使用GraalVM将Spring应用程序编译成本地可执行的镜像文件,这可以显著提升应用程序的启动速度、峰值性能以及减少内存使用。
    • 而在Spring Boot 2.x中,这一特性并未直接支持。
  4. Jakarta EE API
    • 由于Java EE已经变更为Jakarta EE,Spring Boot 3.x支持Jakarta EE 10,并且所有的Java EE依赖项都已经迁移到了Jakarta EE API。
    • 这意味着开发者在使用这些依赖项时,需要相应地更新包名从javax开头变更为jakarta
  5. 配置属性兼容性
    • 在Spring Boot 3.x中,一些配置属性被重新命名或删除,开发人员需要更新application.propertiesapplication.yml配置文件。
    • 为了帮助开发者进行升级,Spring Boot 3.x提供了spring-boot-properties-migrator模块,该模块可以在启动时分析应用程序的环境并打印诊断结果,同时在运行时为开发者临时迁移属性。
  6. 应用可观察性
    • Spring Boot 3.x通过Micrometer和Micrometer追踪提高了应用的可观察性。
  7. 配置属性绑定改进
    • Spring Boot 3.x引入了新的配置属性绑定机制,支持更复杂的绑定需求。可以通过@ConfigurationProperties注解实现更灵活的配置属性绑定。
  8. AOT(Ahead-of-Time)编译支持
    • Spring Boot 3.x引入了AOT编译支持,可以在编译时生成优化后的代码,以提高运行时性能和减少启动时间。
  9. Native Image支持
    • Spring Boot 3.x支持GraalVM Native Image,可以将Spring Boot应用编译成本地可执行文件,进一步减少启动时间和内存占用。

总结来说,Spring Boot 3.x在Java版本要求、Spring Framework版本、对新技术(如GraalVM和Jakarta EE)的支持、配置属性兼容性、应用可观察性、配置属性绑定、AOT编译支持以及Native Image支持等方面都进行了显著的改进和优化,为开发者提供了更强大、更灵活、更高效的开发体验。

2. Spring Framework 5.x 与 6的区别

Spring Framework 5.x与6的区别主要体现在以下几个方面:

一、基础支持和改进

  1. Java版本要求
    • Spring Framework 5.x:需要Java 8或更高版本,并充分利用了Java 8的新功能,如lambda表达式。
    • Spring Framework 6:需要Java 17或更高版本,意味着开发者可以享受更新、更先进的Java语言特性。
  2. Kotlin支持
    • Spring Framework 6提供了对Kotlin的完全支持,使开发者可以使用Kotlin编写Spring应用程序,并充分利用其优势。

二、编程模型和特性

  1. 响应式编程
    • Spring Framework 5.x引入了Spring WebFlux,用于在Spring中构建响应式应用。它允许使用响应式编程模型来处理异步和非阻塞的操作。
    • Spring Framework 6进一步增强了响应式编程的支持,包括新的反应式API和增强的运行时支持,使开发者能够更轻松地构建高吞吐量、低延迟的应用程序。
  2. 函数式编程
    • Spring Framework 5.x:支持函数式风格的API,这是Java 8中引入函数式编程概念之后的一个逻辑进步。
    • Spring Framework 6:继续强化函数式编程的支持,特别是在WebFlux中,通过引入改进的WebFlux API和增强的路由器函数,提高了性能和可用性。

三、数据访问和集成

  1. 数据访问特性
    • Spring Framework 6引入了许多新的数据访问特性,包括JDBC的异步查询、MongoDB的文本搜索、以及针对NoSQL数据库的新的查询功能等。
  2. 外部库支持
    • Spring Framework 6提供了对Netty和Undertow的支持,使得开发者可以轻松地构建响应式Web应用程序。

四、测试和支持

  1. 测试改进
    • Spring Framework 5.x:增加了很多测试相关的改进,比如对JUnit 5的支持,并提供了WebTestClient来测试spring-webflux。
    • Spring Framework 6:在测试方面可能继续增强对现代测试框架和工具的支持。
  2. 模块化
    • Spring Framework 5.x:进一步模块化,允许开发人员更容易地选择需要的部分,从而减小了最终应用程序的大小。
    • Spring Framework 6:可能继续优化模块化结构,以提供更好的灵活性和可维护性。

五、其他

  1. 性能优化
    • Spring Framework 6可能会带来一系列的性能优化和改进,以提高应用程序的整体性能。
  2. 新特性和API
    • Spring Framework 6可能会引入一些新的特性和API,以满足不断发展的开发需求。

总结来说,Spring Framework 5.x与6在Java版本要求、Kotlin支持、响应式编程、函数式编程、数据访问和集成、测试和支持等方面都存在一定的区别。这些区别不仅体现了Spring框架的不断进步和发展,也为开发者提供了更多选择和更好的开发体验。

Elasticsearch环境搭建

参考文章:Elasticsearch环境搭建|ES单机|ES单节点模式启动|ES集群搭建|ES集群环境搭建

京东商城搜索页面

以下截图搜索时间2024/06/20

搜索显示器

上部分聚合结果,下部分是商品列表

在这里插入图片描述

限制搜索100页,一页50个商品,允许跳页

在这里插入图片描述

搜索大床

上部分聚合结果,下部分是商品列表

在这里插入图片描述

限制搜索100页,一页50个商品,允许跳页

在这里插入图片描述

分析搜索页面

通过分析上面两次商城搜索的结果(当然还可再继续分析一些产品,这里不再赘述)可以得出以下结论:

整体上可以分为两部分

  1. 上半部分部分是聚合结果Aggregation Results),这部分会显示与搜索关键词相关的聚合信息,例如商品的品牌、类别(分类)、以及其他一些商品的属性(如价格范围、颜色、尺寸等)。

    这些聚合结果为用户提供了丰富的过滤条件,使用户能够更精确地缩小搜索范围,找到他们真正想要的商品。

  2. 下面的部分是商品分页数据Product Pagination Data):在聚合结果的下方,会展示与搜索关键词匹配的商品列表。这些商品通常会按照某种排序方式(如相关性、价格、销量等)进行排序,并且会进行分页展示,以便用户浏览。

异构数据

在商城搜索中,商品数据通常是异构Heterogeneous Data)的,因为不同的商品类别具有各自独特的属性集。商品间共享一些通用属性,如品牌、类别,但每个商品类别(如显示器、床等)都有其特定的属性集,这些属性集对于其他类别来说可能是不相关的。

  1. 通用属性(Common Attributes)
    • 品牌(Brand)
    • 类别(Category)
    • 价格(Price)
    • 库存状态(Stock Status)
    • …(其他所有商品都可能具有的属性)
  2. 特定商品类别属性(Specific Category Attributes)
    • 显示器(Monitors)
      • 面板类型(Panel Type: IPS, VA, TN)
      • 刷新率(Refresh Rate: 60Hz, 120Hz, …)
      • 接口类型(Interface Type: Type-C, DP, HDMI)
      • 分辨率(Resolution: 1080p, 4K, …)
      • …(其他显示器特有的属性)
    • 床(Beds)
      • 窗体结构(Frame Structure: 框架结构, 箱框结构)
      • 风格(Style: 简约风, 欧美风, 法式, 意式)
      • 材质(Material: 木质, 金属, …)
      • 尺寸(Size: 单人床, 双人床, …)
      • …(其他床特有的属性)

注意高级选项是将一些属性折叠起来了,不然看着太乱。可以通过将高级选项外边的属性进行筛选,然后可以发现高级选项中的一些属性就会跑到高级选项外面去了,保证外部展示一定数目的属性,剩余的折叠在高级选项中

异构数据建模

于是我们在定义商品的 索引映射 (index mapping) (类似于关系型数据库建表定义表结构),商品的属性就要包括两大类,

  • 一类是通用属性(Common Attributes)

  • 一类是特定商品类别属性(Specific Category Attributes)

对于通用属性,每个商品都有这些属性,因此在index mapping中明确声明这些属性字段即可;

对于特定商品类别属性,也就是体现数据异构型的这些属性,可以不止一个,attrs是个数组列表结构,列表中每个元素又都是一个对象,可以抽象为属性id (attrId),属性名(attrName),属性值(attrVal);

这样我们就把商品结构基本定义出来了。

需要注意,对于attrs属性列表,它就不是简单的字段类型(如long, date, text等)属性,而是一个对象数组。

如果是关系型数据库建模

如果在关系型数据库(如MySQL)可以通过子表的方式表示,公共属性建立一张主表,为每个商品单独建立一张表存储商品的特定属性(如某商品有10个特定属性,就为它单独建一张子表,包含10个属性字段,这样主表+子表共同表示一个商品,且主表:子表 = 1:1,但是商城上千万个sku就会有上千万张子表)

还可以将属性结构抽象出来,每个特定属性都可以由attrId, attrName, AttrValue表示,那么为所有商品建立一张子表即可,通过productId关联主表商品通用属性表,即可表示所有商品,由于一个商品有多个属性,因此product_common与product_attr的关系就是1:N(商城上千万个sku,product_attr表数据量就会非常大)

在这里插入图片描述

ES建模

通过上面关系型数据库建模分析,可以将attrs表示商品异构性的属性,作为子表形式与商品公共属性主表关联处理;

在ES中可以使用反数据库范式设计,作为JSON数组将它作为属性嵌入到商品mapping中。

通常使用父子关联parent/child relation)或者嵌套文档Using nested fields for arrays of objects

  • 嵌套文档更新嵌套的子文档,需要更新整个文档,但是读取性能更高,适用于读多写少的场景
  • 父子文档可以独立更新,但是读取性能较嵌套文档弱些,适用于子文档频繁更新的场景

商城中商品的特定属性可以使用嵌套类型表示。

创建索引

相当于关系型数据库建表

PUT product
{
  "mappings": {
    "properties": {
      "id": {//商品id
        "type": "long"
      },
      "name": {//商品名字,
        "type": "text",//text类型可以分词,用于全文匹配
        "analyzer": "ik_max_word" //指定中文分词器
      },
      "subTitle": {//商品副标题
        "type": "text",
        "analyzer": "ik_max_word" //指定中文分词器
      },
      "saleCount":{//销量
        "type": "long"
      },
       "putAwayDate":{//上架时间
        "type": "date"
      },
      "price": {//价格
        "type": "double"
      },
      "pic": {//商品图片
        "type": "keyword"//图片没必要分词,使用keyword来表示无需分词的字符串类型
      },
      "hasStock": { //是否有库存
        "type": "boolean"
      },
      "brandId": { //品牌id
        "type": "long"
      },
      "brandName": {//品牌名称
        "type": "keyword"  //品牌也无需分词
      },
      "brandImg": {//品牌图标
        "type": "keyword"
      },
      "categoryId": {//商品类别
        "type": "long"
      },
      "categoryName": {//类别名称
        "type": "keyword"
      },
      "attrs": {//商品特定属性-嵌套类型
        "type": "nested",  //指定嵌套类型
        "properties": { //相当于上面关系型数据库建模图中的子表
          "attrId": {
            "type": "long"
          },
          "attrName": {
            "type": "keyword"
          },
          "attrValue": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

构建Query DSL

官方文档:Query DSL

上面已经分析过,商城搜索页面由两部分构成:聚合结果Aggregation Results),商品分页数据Product Pagination Data

因此DSL需要两种操作:查询、聚合

对于查询操作,又涉及到关键字搜索,以及属性过滤,因此不是简单的基础查询,使用复合查询子句,bool复合查询来组合查询语句和过滤语句(参考ES支持的子句类型

还包括排序高亮分页

我们逐步拆解出来,最后组成完整的DSL

根据输入框进行全文搜索

在这里插入图片描述

这是一个基础查询语句。(在最终的DSL中会嵌入到bool复合查询子句中,参考ES支持的子句类型,之后不再赘述)

对单个字段进行全文查询

match query

对单个字段进行全文查询full text query),name是被分词(analyzed)的字段(text类型字段),查询的文本会被分析器(analyzer)分解成单独的词项(tokens),这些词项随后会与索引中的词项进行匹配match),以找出与查询相关的文档。

GET product/_search
{
  "query": {
    "match": {
      "name": "显示器"
    }
  }
}

对多个字段进行全文查询

multi_match query

允许在多个字段上执行全文搜索,而无需为每个字段指定一个单独的查询。

GET product/_search
{
  "query": {
    "multi_match": {
      "query": "显示器",
      "fields": [
        "name",
        "subTitle"
      ]
    }
  }
}

根据聚合结果进行数据过滤

聚合结果为用户提供了丰富的过滤条件,使用户能够更精确地缩小搜索范围,找到他们真正想要的商品。

在这里插入图片描述

这些条件都是过滤条件(也称为查询条件或筛选条件),用于从数据集中选择满足特定条件的记录

公共属性

比如用户选择了某商品分类,指定了价格区间,选择有库存的,并指定商品品牌

按照这些条件过滤filter context)数据(筛选结构性数据),不算分,从而提升查询性能(算分通常会增加查询的复杂性和计算量)

GET product/_search
{
  "query": {
    "bool": {
      "filter": [
        {//指定类别
          "term": {
            "categoryId": 1
          }
        },
        {//指定价格区间
          "range": {
            "price": {
              "gte": 10,
              "lte": 20000
            }
          }
        },
        {//有库存
          "term": {
            "hasStock": "true"
          }
        },
        {//指定商品品牌
          "terms": {
            "brandId": [
              1
            ]
          }
        }
      ]
    }
  }
}

嵌套属性

nested query

嵌套查询搜索嵌套字段对象,就像它们作为单独的文档进行索引一样。

筛选了一个属性
GET product/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {//刷新率
                    "term": {
                      "attrs.attrId": {
                        "value": 1
                      }
                    }
                  },
                  {
                    "term": {
                      "attrs.attrValue": {
                        "value": "120Hz"
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}
筛选多个嵌套属性
GET product/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {//刷新率
                    "term": {
                      "attrs.attrId": {
                        "value": 1
                      }
                    }
                  },
                  {
                    "term": {
                      "attrs.attrValue": {
                        "value": "120Hz"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {//接口类型
                    "term": {
                      "attrs.attrId": {
                        "value": 2
                      }
                    }
                  },
                  {
                    "term": {
                      "attrs.attrValue": {
                        "value": "Type-C"
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

聚合操作

Aggregation

如果仅仅是聚合,可以设置参数***“size”:0***,只获取聚合结果。

商品品牌聚合

聚合获取品牌id,子聚合获取品牌名和品牌图标

GET product/_search
{
  "size": 0, 
  //aggs是缩写,kibana提示的。完整也可手动打aggregations
  "aggs": {
    // brand aggs
    "brandId_aggs": {
      //terms 术语聚合类型,类似于MySQL的groupBy分组
      "terms": {
        "field": "brandId",
        "size": 100  //根据实际情况,展示多少个品牌,展示全的话,设置大些
      },
      "aggs": {
        "brandName_aggs": {
          "terms": {
            "field": "brandName",
           //品牌子聚合,品牌名聚合,只应该取一个才对
            "size": 1
          }
        },
        "brandImg_aggs":{
          "terms": {
            "field": "brandImg",
           //品牌子聚合,品牌图片聚合,只应该取到一个才对
            "size": 1
          }
        }
      }
    }
  }
}

商品分类聚合

聚合获取商品分类id列表,子聚合获取商品分类的分类名称

GET product/_search
{
  "size": 0, 
  //aggs是缩写,kibana提示的。完整也可手动打aggregations
  "aggs": {
    //category聚合
    "categoryId_aggs":{
      "terms": {
        "field": "categoryId",
        "size": 100 //根据实际看展示多少个品牌,可设置大些,展示所有品牌
      },
      "aggs": {
        "categoryName_aggs": {
          "terms": {
            "field": "categoryName",
            "size": 1   //一个分类id对应一个分类名称,设置1即可
          }
        }
      }
    }
  }
}

嵌套类型聚合

商品特定属性的聚合,根据属性id聚合,子聚合来获取属性名(1个属性对应一个属性名),和属性值(一个属性对应多个属性值)

GET product/_search
{
  "size": 0, 
  //aggs是缩写,kibana提示的。完整也可手动打aggregations
  "aggs": {
      "attr_aggs":{
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attrId_aggs": {
          "terms": {
            "field": "attrs.attrId",
            "size": 100  //展示多少商品特定属性,根据实际情况可以适当大点,展示全些
          },
          "aggs": {
            "attrName_aggs": {
              "terms": {
                "field": "attrs.attrName",
                "size": 1  //一个属性对应一个属性名字
              }
            },
            "attrVal_aggs":{
              "terms": {
                "field": "attrs.attrValue",
                "size": 100  //每个属性会有多个属性值,如显示器的面板属性有IPS,TN,VA等属性
              }
            }
          }
        }
      }
    }
  }
}

排序

在这里插入图片描述

可以指定字段,指定排序方式,也可以支持多字段排序。这里假设用户选择了按照销量倒排

GET product/_search
{
  "sort": [
    {
      "saleCount": {
        "order": "desc"
      }
    }
  ]
}

高亮

在这里插入图片描述

对name和keywords两个字段高亮,样式可以自定义,这里设置的样式是加粗、红色

当搜索的文字分词后出现在这两个字段中都会进行高亮显示。

GET product/_search
{
  "highlight": {
    "pre_tags": [
      "<strong style='color: red;'>"
    ],
    "post_tags": [
      "</strong>"
    ],
    "fields": {
      "name": {},
      "keywords": {}
    }
  }
}

分页

在这里插入图片描述

前边分析了支持跳页,且限制了搜索100页,每页50个商品,即限制最大搜索5000个商品,从业务侧解决了深分页问题

GET product/_search
{
 "from": 0,
 "size": 50
}

最终的DSL

将上面的语句组合起来,就构成了最终的DSL:

  • 使用bool复合查询语句组合全文查询子句、过滤子句,嵌套查询子句;
  • 聚合语句
  • 排序
  • 高亮
  • 分页

这个DSL的整体结构

GET product/_search
{
  "query": {
    "bool": {//bool复合查询语句拼上基础查询语句
      "must": [
        
      ],
      "filter": [
        //拼上筛选条件
        //包括嵌套类型的筛选条件
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {//bool复合语句来组合嵌套类型筛选条件
                "must": []
              }
            }
          }
        },
        //可以拼上多个nested查询
      ]
    }
  },
  "aggs": {
    "attr_aggs": {
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        //按照属性id聚合,类似于MySQL的groupBy分组
        "attrId_aggs": {
          //子聚合获取属性信息
          "aggs": {
            "attrName_aggs": {

            }
          }
        }
      }
    }
  }
}

完整的DSL

GET product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "显示器",
            "fields": [
              "name",
              "subTitle"
            ]
          }
        }
      ],
      "filter": [
        {
          "term": {
            "categoryId": 1
          }
        },
        {
          "range": {
            "price": {
              "gte": 10,
              "lte": 20000
            }
          }
        },
        {
          "term": {
            "hasStock": "true"
          }
        },
        {
          "terms": {
            "brandId": [
              1
            ]
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": 1
                      }
                    }
                  },
                  {
                    "term": {
                      "attrs.attrValue": {
                        "value": "120Hz"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": 2
                      }
                    }
                  },
                  {
                    "term": {
                      "attrs.attrValue": {
                        "value": "Type-C"
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "aggs": {
    "brandId_aggs": {
      "terms": {
        "field": "brandId",
        "size": 100
      },
      "aggs": {
        "brandName_aggs": {
          "terms": {
            "field": "brandName",
            "size": 1
          }
        },
        "brandImg_aggs": {
          "terms": {
            "field": "brandImg",
            "size": 1
          }
        }
      }
    },
    "categoryId_aggs": {
      "terms": {
        "field": "categoryId",
        "size": 100
      },
      "aggs": {
        "categoryName_aggs": {
          "terms": {
            "field": "categoryName",
            "size": 1
          }
        }
      }
    },
    "attr_aggs": {
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attrId_aggs": {
          "terms": {
            "field": "attrs.attrId",
            "size": 100
          },
          "aggs": {
            "attrName_aggs": {
              "terms": {
                "field": "attrs.attrName",
                "size": 1
              }
            },
            "attrVal_aggs": {
              "terms": {
                "field": "attrs.attrValue",
                "size": 100
              }
            }
          }
        }
      }
    }
  },
  "sort": [
    {
      "saleCount": {
        "order": "desc"
      }
    }
  ],
  "highlight": {
    "pre_tags": [
      "<strong style='color: red;'>"
    ],
    "post_tags": [
      "</strong>"
    ],
    "fields": {
      "name": {},
      "keywords": {}
    }
  },
  "from": 0,
  "size": 50
}

根据DSL编写Java代码

🔍️源码链接,点击查看👈️

警告:7.15.0 中已弃用。

Java REST 客户端已被弃用,取而代之的是 Java API 客户端。

特别注意下,有时候你发现自己的Java代码执行打印的DSL缺少东西,可能是构造了查询条件,但是没添加到请求中(如忘记将 boolQuery 对象构造到 searchRequestBuilder 中,导致bool条件缺失)!

使用Java High Level Rest Client

maven依赖

<dependencies>
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.17.3</version>
    </dependency>
</dependencies>

测试类:基类

测试类:封装RestHighLevelClient到基类,实际写代码的子类可以直接获取RestHighLevelClient对象,进行查询

package com.polaris.es.testdemo;

import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;


public class ESHighLevelRestClientBase {
    private static Logger logger = LoggerFactory.getLogger(ESHighLevelRestClientBase.class);
    private static RestHighLevelClient client = null;
    public RestHighLevelClient getClient() {
        return client;
    }

    @BeforeAll
    public static void init(){

        //RestHighLevelClient client = new RestHighLevelClient(
        // RestClient.builder(new HttpHost("192.168.43.7", 9200, "http")));


        //https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/_basic_authentication.html
        final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY,
                new UsernamePasswordCredentials("elastic", "123456"));  //es账号密码(默认用户名为elastic)
        client = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("192.168.43.7", 9200, "http"))
                        .setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
                            public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
                                httpClientBuilder.disableAuthCaching();
                                return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
                            }
                        }));
        logger.info("init executed....");
    }


    @AfterAll
    public static void afterAll(){
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        logger.info("finished....");
    }
}

测试类:子类,转化DSL为Java代码

public class ESHighLevelRestClientTest extends ESHighLevelRestClientBase {
    @BeforeAll
    public static void initParam() {
        param = ESRequestParam.builder()
                .keyword("显示器")
                .categoryId(1L)
                .price("10_20000")
                .hasStock(1)
                .brandId(Collections.singletonList(1L))
                .attrs(Arrays.asList("1_120Hz", "2_Type-C"))
                .sort("saleCount_desc")
                .pageNum(0)
                .pageSize(50)
                .queryString("")
                .build();
    }
    @Test
    public void search() {
        //1.构建查询对象
        SearchRequest searchRequest = parseParamAndBuildSearchRequest(param);
        //2.查询
        SearchResponse searchResponse;
        try {
            searchResponse = getClient().search(searchRequest, RequestOptions.DEFAULT);
            logger.info("----resp---:" + searchResponse.toString());
        } catch (Exception e) {
            logger.error("ES查询异常:", e);
            throw new RuntimeException("ES查询异常,稍后重试!");
        }
        //3.解析查询结果
        ESResponseResult result = parseESResponse(param, searchResponse);
        logger.info("----|result|---:" + JSON.toJSONString(result));

    }
    
    /**
     * <参考官方文档>
     * <p>
     * java rest high level client:
     * <p>
     * Search api:
     * https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search.html
     * <p>
     * Building Queries:
     * https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-query-builders.html
     * <p>
     * Building Aggregations
     * https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-aggregation-builders.html
     *
     * @param param 请求参数
     * @return 构造好的searchRequest
     */
    private SearchRequest parseParamAndBuildSearchRequest(ESRequestParam param) {

        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        SearchRequest searchRequest = new SearchRequest("product");
        //1.搜索文字
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        String keyword = param.getKeyword();
        if (StringUtils.isNotBlank(keyword)) {
            searchSourceBuilder
                    .query(boolQueryBuilder
                            .must(QueryBuilders
                                    .multiMatchQuery(keyword, Arrays.asList("keywords", "subTitle", "name").toArray(new String[0]))));
        }

        //2.品牌ids
        if (CollectionUtils.isNotEmpty(param.getBrandId())) {
            searchSourceBuilder
                    .query(boolQueryBuilder
                            .filter(QueryBuilders
                                    .termsQuery("brandId", param.getBrandId().toArray())));
        }

        //3.分类
        if (param.getCategoryId() != null) {
            searchSourceBuilder
                    .query(boolQueryBuilder
                            .filter(QueryBuilders
                                    .termQuery("categoryId", param.getCategoryId())));
        }

        //4.排序
        if (StringUtils.isNotBlank(param.getSort())) {
            String[] s = param.getSort().split("_");
            if (s[1].equals("asc")) {
                searchSourceBuilder
                        .sort(new FieldSortBuilder(s[0]).order(SortOrder.ASC));
            } else {
                searchSourceBuilder
                        .sort(new FieldSortBuilder(s[0]).order(SortOrder.DESC));
            }
        }

        //5.是否有货
        if (param.getHasStock() != null) {
            searchSourceBuilder
                    .query(boolQueryBuilder
                            .filter(QueryBuilders
                                    .termQuery("hasStock", param.getHasStock() == 1)));
        }

        //6.价格范围过滤
        if (StringUtils.isNotBlank(param.getPrice())) {
            String[] s = param.getPrice().split("_");

            if (param.getPrice().startsWith("_")) {
                searchSourceBuilder
                        .query(boolQueryBuilder
                                .filter(QueryBuilders
                                        .rangeQuery("price")
                                        .lte(s[1])
                                )
                        );
            } else if (param.getPrice().endsWith("_")) {
                searchSourceBuilder
                        .query(boolQueryBuilder
                                .filter(QueryBuilders
                                        .rangeQuery("price")
                                        .gte(s[0])
                                )
                        );
            } else {
                searchSourceBuilder
                        .query(boolQueryBuilder
                                .filter(QueryBuilders
                                        .rangeQuery("price")
                                        .gte(s[0])
                                        .lte(s[1])
                                )
                        );
            }
        }

        //7.属性查询:嵌套类型
        if (CollectionUtils.isNotEmpty(param.getAttrs())) {
            for (String attr : param.getAttrs()) {
                String[] s = attr.split("_");

                BoolQueryBuilder nestedBoolQueryBuilder = QueryBuilders
                        .boolQuery()
                        .must(QueryBuilders
                                .termQuery("attrs.attrId", s[0]))
                        .must(QueryBuilders
                                .termQuery("attrs.attrValue", s[1]));

                searchSourceBuilder
                        .query(boolQueryBuilder
                                .filter(QueryBuilders
                                        .nestedQuery("attrs", nestedBoolQueryBuilder, ScoreMode.None)));
            }
        }

        //8.分页
        if (param.getPageNum() != null) {
            searchSourceBuilder.from((param.getPageNum() - 1) * param.getPageSize());
            searchSourceBuilder.size(param.getPageSize());
        }

        //9.高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        HighlightBuilder.Field name = new HighlightBuilder.Field("name");
        //默认类型unified
        name.highlighterType("unified");
        highlightBuilder.field(name);
        HighlightBuilder.Field keywords = new HighlightBuilder.Field("keywords");
        highlightBuilder.field(keywords);
        highlightBuilder.preTags("<strong style='color: red;'>").postTags("</strong>");
        searchSourceBuilder.highlighter(highlightBuilder);

        //10.聚合
        //10.1 品牌-聚合,根据品牌id聚合,并通过子聚合获取品牌名字和品牌图片
        TermsAggregationBuilder brandIdAggregationBuilder =
                AggregationBuilders
                        .terms("brandId_aggs")
                        .field("brandId")
                        .size(50);
        brandIdAggregationBuilder
                .subAggregation(AggregationBuilders
                        .terms("brandName_aggs")
                        .field("brandName")
                        .size(1)
                );
        brandIdAggregationBuilder
                .subAggregation(AggregationBuilders
                        .terms("brandImg_aggs")
                        .field("brandImg")
                        .size(1)
                );
        searchSourceBuilder
                .aggregation(brandIdAggregationBuilder);

        //10.2 分类属性-聚合,根据分类属性id聚合,并通过子聚合获取分类名字
        TermsAggregationBuilder categoryIdAggregationBuilder =
                AggregationBuilders
                        .terms("categoryId_aggs")
                        .field("categoryId")
                        .size(50);
        categoryIdAggregationBuilder
                .subAggregation(AggregationBuilders
                        .terms("categoryName_aggs")
                        .field("categoryName")
                        .size(1)
                );
        searchSourceBuilder.aggregation(categoryIdAggregationBuilder);

        //10.3 嵌套属性聚合,根据嵌套属性id聚合,并通过子聚合获取嵌套属性名,及所有嵌套属性值
        TermsAggregationBuilder attrIdAgg =
                AggregationBuilders
                        .terms("attr_id_agg")
                        .field("attrs.attrId")
                        .size(50);
        attrIdAgg
                .subAggregation(AggregationBuilders
                        .terms("attr_name_agg")
                        .field("attrs.attrName")
                        .size(1)
                );
        attrIdAgg
                .subAggregation(AggregationBuilders
                        .terms("attr_value_agg")
                        .field("attrs.attrValue")
                        .size(50)
                );

        NestedAggregationBuilder nestedAggregationBuilder =
                AggregationBuilders
                        .nested("attr_agg", "attrs")
                        .subAggregation(attrIdAgg);

        searchSourceBuilder.aggregation(nestedAggregationBuilder);

        //11.设置到查询中
        searchRequest.source(searchSourceBuilder);

        // 打印Query DSL
        try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
            // 将查询构建器转换为XContentBuilder
            searchSourceBuilder.toXContent(builder, ToXContent.EMPTY_PARAMS);
            // 打印DSL语句
            String dsl = Strings.toString(builder);
            logger.info("-----dsl:----" + dsl);
        } catch (IOException e) {
            // 处理可能的异常
            e.printStackTrace();
        }
        return searchRequest;
    }
    
        private ESResponseResult parseESResponse(ESRequestParam param, SearchResponse searchResponse) {
        RestStatus status = searchResponse.status();
        TimeValue took = searchResponse.getTook();
        Boolean terminatedEarly = searchResponse.isTerminatedEarly();
        boolean timedOut = searchResponse.isTimedOut();

        logger.info("timedOut:{}, terminatedEarly:{},took:{}, status:{}", timedOut, terminatedEarly, took.toString(), status.toString());
        if (timedOut || terminatedEarly != null && terminatedEarly.equals(Boolean.TRUE) || !RestStatus.OK.equals(status)) {
            logger.error("请求失败...");
            throw new RuntimeException("ES请求异常");
        }

        //1.分页信息
        ESResponseResult result = new ESResponseResult();
        SearchHits hits = searchResponse.getHits();
        long total = hits.getTotalHits().value;
        result.setTotal(total);
        result.setPageNum(param.getPageNum());
        int pages;
        if (total % param.getPageSize() == 0) {
            pages = Integer.parseInt(String.valueOf(total / param.getPageSize()));
        } else {
            pages = Integer.parseInt(String.valueOf(total / param.getPageSize() + 1));
        }
        result.setTotalPages(pages);
        if (pages > 100) {
            result.setTotalPages(100);
        }

        //2.商品列表解析
        List<EsProduct> products = new ArrayList<>();
        for (SearchHit hit : hits) {
            EsProduct product = new EsProduct();
            Map<String, Object> sourceAsMap = hit.getSourceAsMap();
            long id = Long.parseLong((String) sourceAsMap.get("id"));
            product.setId(id);
            product.setBrandId(Long.parseLong(sourceAsMap.get("brandId").toString()));
            product.setBrandName(sourceAsMap.get("brandName").toString());
            product.setProductCategoryId(Long.parseLong(sourceAsMap.get("categoryId").toString()));
            product.setProductCategoryName(sourceAsMap.get("categoryName").toString());
            product.setPic(sourceAsMap.get("pic").toString());
            product.setName(sourceAsMap.get("name").toString());
            product.setSubTitle(sourceAsMap.get("subTitle").toString());
            product.setKeywords(sourceAsMap.get("keywords").toString());
            product.setPrice(new BigDecimal(sourceAsMap.get("price").toString()));
            product.setSort(Integer.valueOf(hit.getSortValues()[0].toString()));

            //高亮处理
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            HighlightField name = highlightFields.get("name");
            Text[] fragments = name.fragments();
            String fragmentString = fragments[0].string();
            product.setName(fragmentString);

            HighlightField keywords = highlightFields.get("keywords");
            String keyword = keywords.fragments()[0].string();
            product.setKeywords(keyword);

            products.add(product);

        }
        result.setProducts(products);

        //聚合结果解析
        Aggregations aggregations = searchResponse.getAggregations();

        //1.品牌聚合结果
        List<ESResponseResult.BrandVo> brands = new ArrayList<>();
        Terms brandIdTerms = aggregations.get("brandId_aggs");
        for (Terms.Bucket bucket : brandIdTerms.getBuckets()) {
            ESResponseResult.BrandVo brandVo = new ESResponseResult.BrandVo();
            long brandId = Long.parseLong(bucket.getKey().toString());
            brandVo.setBrandId(brandId);
            Terms brandNameTerms = bucket.getAggregations().get("brandName_aggs");
            String brandName = brandNameTerms.getBuckets().get(0).getKey().toString();
            brandVo.setBrandName(brandName);
            Terms brandImgTerms = bucket.getAggregations().get("brandImg_aggs");
            String brandImg = brandImgTerms.getBuckets().get(0).getKey().toString();
            brandVo.setBrandImg(brandImg);

            brands.add(brandVo);
        }
        result.setBrands(brands);

        //2.分类聚合
        List<ESResponseResult.CategoryVo> categories = new ArrayList<>();
        Terms categoryIdTerms = aggregations.get("categoryId_aggs");
        for (Terms.Bucket bucket : categoryIdTerms.getBuckets()) {
            ESResponseResult.CategoryVo categoryVo = new ESResponseResult.CategoryVo();
            long categoryId = Long.parseLong(bucket.getKey().toString());
            categoryVo.setCategoryId(categoryId);
            Terms categoryNameTerms = bucket.getAggregations().get("categoryName_aggs");
            String categoryName = categoryNameTerms.getBuckets().get(0).getKey().toString();
            categoryVo.setCategoryName(categoryName);

            categories.add(categoryVo);
        }
        result.setCategorys(categories);

        //嵌套属性聚合结果
        List<ESResponseResult.AttrVo> attrs = new ArrayList<>();

        Nested nest = aggregations.get("attr_agg");
        Terms attrTerms = nest.getAggregations().get("attr_id_agg");
        for (Terms.Bucket bucket : attrTerms.getBuckets()) {
            ESResponseResult.AttrVo attrVo = new ESResponseResult.AttrVo();
            long attrId = Long.parseLong(bucket.getKey().toString());
            attrVo.setAttrId(attrId);
            Terms attrNameTerms = bucket.getAggregations().get("attr_name_agg");
            String attrName = attrNameTerms.getBuckets().get(0).getKey().toString();
            attrVo.setAttrName(attrName);

            Terms attrValueTerms = bucket.getAggregations().get("attr_value_agg");
            List<String> attrValues = new ArrayList<>();
            for (Terms.Bucket attrValBuck : attrValueTerms.getBuckets()) {
                attrValues.add(attrValBuck.getKey().toString());
            }

            attrVo.setAttrValue(attrValues);
            attrs.add(attrVo);
        }
        result.setAttrs(attrs);

        return result;
    }
}

使用Java API Client

maven依赖

<dependencies>        
    <dependency>
        <groupId>co.elastic.clients</groupId>
        <artifactId>elasticsearch-java</artifactId>
        <version>7.17.21</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.17.0</version>
    </dependency>
</dependencies>

测试类:基类

package com.polaris.es.testdemo;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;


public class ESJavaApiClientBase {
    static Logger logger = LoggerFactory.getLogger(ESJavaApiClientBase.class);

    private static ElasticsearchClient client;
    private static RestClient restClient;

    public static ElasticsearchTransport getTransport() {
        return transport;
    }

    private static ElasticsearchTransport transport;
    public static ElasticsearchClient getClient() {
        return client;
    }

    @BeforeAll
    public static void init(){
        //https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/connecting.html
        //https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/_basic_authentication.html
        final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY,
                new UsernamePasswordCredentials("elastic", "123456"));
        // Create the low-level client
        restClient = RestClient.builder(
                new HttpHost("192.168.43.7", 9200, "http"))
                .setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
                    public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
                        httpClientBuilder.disableAuthCaching();
                        return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
                    }
                }).build();
        // Create the transport with a Jackson mapper
        transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());
        // And create the API client
        client = new ElasticsearchClient(transport);
        logger.info("initialed....");
    }

    @AfterAll
    public static void afterAll(){

        // 完成后,关闭RestClient
        try {
            restClient.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        logger.info("finished...");
    }
}

测试类:子类,转换DSL为Java代码

public class ESJavaApiClientTest extends ESJavaApiClientBase {
    private final Logger logger = LoggerFactory.getLogger(ESJavaApiClientTest.class);
    
    @BeforeAll
    public static void initParam() {
        param = ESRequestParam.builder()
                .keyword("显示器")
                .categoryId(1L)
                .price("10_20000")
                .hasStock(1)
                .brandId(Collections.singletonList(1L))
                .attrs(Arrays.asList("1_120Hz", "2_Type-C"))
                .sort("saleCount_desc")
                .pageNum(0)
                .pageSize(50)
                .queryString("")
                .build();
    }
    
    @Test
    public void search() {
        //1.构建查询对象
        SearchRequest searchRequest = parseParamAndBuildSearchRequest(param);
        //2.查询
        SearchResponse<Product> searchResponse;
        try {
            searchResponse = getClient().search(searchRequest, Product.class);
            printSerializeDate("---searchResponse", searchResponse);
        } catch (ElasticsearchException e) {
            logger.error("ES请求失败...|rootCause:{}", e.error().rootCause(), e);
            throw new RuntimeException("ES请求异常");
        } catch (Exception e) {
            logger.error("请求失败...", e);
            throw new RuntimeException("ES请求异常");
        }
        //3.解析查询结果
        ESResponseResult result = parseESResponse(param, searchResponse);
        logger.info("---res:{}", JSON.toJSONString(result));
    }
    
    /**
     * <参考文档>
     * Java api client:
     * 官方示例 SearchingTest:
     * {@linkplain com.polaris.es.officialdemo.usage.SearchingTest}
     * <p>
     * 官方文档Searching for documents:
     * https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/searching.html
     * <p>
     * terms query 查询构造:
     * https://discuss.elastic.co/t/using-termsquery-in-java-api-client-7-17/304904
     * <p>
     * sort:
     * https://discuss.elastic.co/t/new-elasticsearch-8-java-api-sort-by/311352
     * <p>
     * how to use print dsl in es java api client
     * https://github.com/elastic/elasticsearch-java/issues/97
     * <p>
     * Elasticsearch Guide: 高亮highlighting:
     * https://www.elastic.co/guide/en/elasticsearch/reference/7.17/highlighting.html
     * <p>
     * 官方示例 AggregationsTest:
     * {@linkplain com.polaris.es.officialdemo.usage.AggregationsTest}
     *
     * @param param 请求参数
     * @return 构造好的searchRequest
     */
    private SearchRequest parseParamAndBuildSearchRequest(ESRequestParam param) {
        SearchRequest.Builder searchRequestBuilder = new SearchRequest.Builder();

        BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();
        //1.搜索文字
        String keyword = param.getKeyword();
        if (StringUtils.isNotBlank(keyword)) {
            Query byKeyword = MultiMatchQuery.of(m -> m
                    .fields(Arrays.asList("keywords", "subTitle", "name"))
                    .query(keyword)
            )._toQuery();
            boolQueryBuilder.must(byKeyword);
        }

        //2.品牌ids
        if (CollectionUtils.isNotEmpty(param.getBrandId())) {
            // brandId terms query
            List<FieldValue> fieldValues = new ArrayList<>();
            for (Long brandId : param.getBrandId()) {
                FieldValue fv = FieldValue.of(brandId);
                fieldValues.add(fv);
            }
            Query brandId = TermsQuery.of(q -> q
                    .field("brandId")
                    .terms(TermsQueryField.of(t -> t.value(fieldValues)))
            )._toQuery();
            boolQueryBuilder.filter(brandId);
        }

        //3.分类
        if (param.getCategoryId() != null) {
            boolQueryBuilder.filter(TermQuery.of(t -> t
                    .field("categoryId")
                    .value(param.getCategoryId())
            )._toQuery());
        }

        //4.排序
        if (StringUtils.isNotBlank(param.getSort())) {
            String[] split = param.getSort().split("_");
            searchRequestBuilder.sort(s -> s.
                    field(FieldSort
                            .of(f -> f
                                    .field(split[0])
                                    .order(split[1].equals("asc") ? SortOrder.Asc : SortOrder.Asc))
                    )
            );
        }

        //5.是否有货
        if (param.getHasStock() != null) {
            boolQueryBuilder
                    .filter(TermQuery.of(t -> t
                            .field("hasStock")
                            .value(param.getHasStock() == 1)
                    )._toQuery());
        }

        //6.价格范围过滤
        if (StringUtils.isNotBlank(param.getPrice())) {
            String[] s = param.getPrice().split("_");
            if (param.getPrice().startsWith("_")) {
                boolQueryBuilder.filter(RangeQuery.of(r -> r
                        .field("price")
                        .lte(JsonData.of(s[1]))
                )._toQuery());
            } else if (param.getPrice().endsWith("_")) {
                boolQueryBuilder.filter(RangeQuery.of(r -> r
                        .field("price")
                        .gte(JsonData.of(s[0]))
                )._toQuery());
            } else {
                boolQueryBuilder.filter(RangeQuery.of(r -> r
                        .field("price")
                        .gte(JsonData.of(s[0]))
                        .lte(JsonData.of(s[1]))
                )._toQuery());
            }
        }

        //7.属性查询:嵌套类型
        if (CollectionUtils.isNotEmpty(param.getAttrs())) {
            for (String attr : param.getAttrs()) {
                String[] s = attr.split("_");
                boolQueryBuilder.filter(
                        NestedQuery.of(n -> n
                                .path("attrs")
                                .query(q -> q
                                        .bool(b -> b
                                                .must(m -> m
                                                        .term(t -> t
                                                                .field("attrs.attrId")
                                                                .value(s[0]))
                                                )
                                                .must(m -> m
                                                        .term(t -> t
                                                                .field("attrs.attrValue")
                                                                .value(s[1])
                                                        )
                                                )
                                        )
                                )
                        )._toQuery());
            }
        }
        //8.分页
        if (param.getPageNum() != null) {
            searchRequestBuilder.from((param.getPageNum() - 1) * param.getPageSize());
            searchRequestBuilder.size(param.getPageSize());
        }

        //9.高亮
        searchRequestBuilder.highlight(h -> h
                .preTags("<strong style='color: red;'>")
                .postTags("</strong>")
                //这里不允许不设置任何东西
                //默认numberOfFragments:5, fragment_size:100
                .fields("name", hf -> hf.numberOfFragments(5))
                .fields("keywords", hf -> hf.numberOfFragments(5))
        );

        //10.聚合:下面其实也可以写在一起的
        //10.1 品牌-聚合,根据品牌id聚合,并通过子聚合获取品牌名字和品牌图片
        searchRequestBuilder.aggregations("brandId_aggs", ag -> ag
                .terms(term -> term
                        .field("brandId")
                        .size(50)
                )
                .aggregations("brandName_aggs", a -> a
                        .terms(t -> t
                                .field("brandName")
                                .size(1)
                        )
                )
                .aggregations("brandImg_aggs", a -> a
                        .terms(t -> t
                                .field("brandImg")
                                .size(1)
                        )
                )
        );

        //10.2 分类属性-聚合,根据分类属性id聚合,并通过子聚合获取分类名字
        searchRequestBuilder.aggregations("categoryId_aggs", ag -> ag
                .terms(term -> term
                        .field("categoryId")
                        .size(50)
                )
                .aggregations("categoryName_aggs", a -> a
                        .terms(t -> t
                                .field("categoryName")
                                .size(1)
                        )
                )
        );

        //10.3 嵌套属性聚合,根据嵌套属性id聚合,并通过子聚合获取嵌套属性名,及所有嵌套属性值
        searchRequestBuilder.aggregations("attr_aggs", nested -> nested
                .nested(n -> n
                        .path("attrs")
                ).aggregations("attrId_aggs", agg -> agg
                        .terms(t -> t
                                .field("attrs.attrId")
                                .size(50)
                        ).aggregations("attrName_aggs", a -> a
                                .terms(t -> t
                                        .field("attrs.attrName")
                                        .size(1)
                                )
                        )
                        .aggregations("attrVal_aggs", a -> a
                                .terms(t -> t
                                        .field("attrs.attrValue")
                                        .size(50)
                                )
                        )
                )
        );
        searchRequestBuilder.index("product")
                .query(q -> q.bool(boolQueryBuilder.build()));

        SearchRequest searchRequest = searchRequestBuilder.build();
        printSerializeDate("---searchRequest", searchRequest);
        return searchRequest;
    }

    private ESResponseResult parseESResponse(ESRequestParam param, SearchResponse<Product> searchResponse) {
        //结果解析:
        ESResponseResult result = new ESResponseResult();

        //1.分页信息,避免深分页问题,限制100页
        long total = searchResponse.hits().total().value();
        result.setTotal(total);
        result.setPageNum(param.getPageNum());
        int pages;
        if (total % param.getPageSize() == 0) {
            pages = Integer.parseInt(String.valueOf(total / param.getPageSize()));
        } else {
            pages = Integer.parseInt(String.valueOf(total / param.getPageSize() + 1));
        }
        result.setTotalPages(pages);
        if (pages > 100) {
            result.setTotalPages(100);
        }

        //2.商品列表解析
        List<Hit<Product>> hits = searchResponse.hits().hits();
        List<EsProduct> products = new ArrayList<>();
        for (Hit<Product> hit : hits) {
            Product source = hit.source();
            EsProduct product = new EsProduct();

            product.setId(source.getId());
            product.setBrandId(source.getBrandId());
            product.setBrandName(source.getBrandName());
            product.setProductCategoryId(source.getCategoryId());
            product.setProductCategoryName(source.getCategoryName());
            product.setPic(source.getPic());
            product.setName(source.getName());
            product.setSubTitle(source.getSubTitle());
            product.setKeywords(source.getKeywords());
            product.setPrice(source.getPrice());
            product.setStock(source.getStock());
            product.setSort(source.getSort());

            //高亮处理
            Map<String, List<String>> highlight = hit.highlight();
            product.setName(highlight.get("name").get(0));
            product.setKeywords(highlight.get("keywords").get(0));
            products.add(product);
        }
        result.setProducts(products);

        //3.聚合结果解析
        //1.品牌聚合结果
        List<ESResponseResult.BrandVo> brands = new ArrayList<>();

        //3.1品牌聚合结果
        List<LongTermsBucket> brandIdAggsBuckets = searchResponse.aggregations()
                .get("brandId_aggs")
                .lterms()//LongTermsAggregate
                .buckets()
                .array();
        for (LongTermsBucket brandIdAggsBucket : brandIdAggsBuckets) {
            ESResponseResult.BrandVo brandVo = new ESResponseResult.BrandVo();
            Long brandId = Long.parseLong(brandIdAggsBucket.key());
            brandVo.setBrandId(brandId);
            String brandName = brandIdAggsBucket
                    .aggregations()
                    .get("brandName_aggs")
                    .sterms()
                    .buckets()
                    .array()
                    .get(0)
                    .key()
                    .stringValue();
            brandVo.setBrandName(brandName);

            String brandImg = brandIdAggsBucket
                    .aggregations()
                    .get("brandImg_aggs")
                    .sterms()
                    .buckets()
                    .array()
                    .get(0)
                    .key()
                    .stringValue();
            brandVo.setBrandImg(brandImg);
            brands.add(brandVo);
        }
        result.setBrands(brands);

        //3.2分类聚合
        List<ESResponseResult.CategoryVo> categories = new ArrayList<>();
        List<LongTermsBucket> categoryIdAggBuckets = searchResponse
                .aggregations()
                .get("categoryId_aggs")
                .lterms()
                .buckets()
                .array();
        for (LongTermsBucket bucket : categoryIdAggBuckets) {
            ESResponseResult.CategoryVo categoryVo = new ESResponseResult.CategoryVo();
            long categoryId = Long.parseLong(bucket.key());
            categoryVo.setCategoryId(categoryId);

            String categoryName = bucket
                    .aggregations()
                    .get("categoryName_aggs")
                    .sterms()
                    .buckets()
                    .array()
                    .get(0)
                    .key()
                    .stringValue();
            categoryVo.setCategoryName(categoryName);
            categories.add(categoryVo);

        }
        result.setCategorys(categories);

        //3.3嵌套属性聚合结果
        List<ESResponseResult.AttrVo> attrs = new ArrayList<>();
        List<LongTermsBucket> attrAggBuckets = searchResponse
                .aggregations()
                .get("attr_aggs")
                .nested()
                .aggregations()
                .get("attrId_aggs")
                .lterms()
                .buckets()
                .array();

        for (LongTermsBucket bucket : attrAggBuckets) {
            ESResponseResult.AttrVo attrVo = new ESResponseResult.AttrVo();
            long attrId = Long.parseLong(bucket.key());
            attrVo.setAttrId(attrId);
            String attrName = bucket
                    .aggregations()
                    .get("attrName_aggs")
                    .sterms()
                    .buckets()
                    .array()
                    .get(0)
                    .key()
                    .stringValue();
            attrVo.setAttrName(attrName);

            List<StringTermsBucket> attrValAggBuckets = bucket
                    .aggregations()
                    .get("attrVal_aggs")
                    .sterms()
                    .buckets()
                    .array();
            List<String> attrValues = new ArrayList<>();
            for (StringTermsBucket valBuckets : attrValAggBuckets) {
                String attrVal = valBuckets.key().stringValue();
                attrValues.add(attrVal);
            }
            attrVo.setAttrValue(attrValues);
            attrs.add(attrVo);
        }

        result.setAttrs(attrs);
        return result;
    }

    /**
     * @param prefix 打印日志,设置前缀
     * @param object 需要序列号打印的对象
     */
    private void printSerializeDate(String prefix, JsonpSerializable object) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        JsonGenerator generator = getTransport().jsonpMapper().jsonProvider().createGenerator(baos);
        object.serialize(generator, getTransport().jsonpMapper());
        generator.close();
        logger.info(prefix + ":{}", baos);
    }
}

对比

Java API Client代码更简洁,使用lambda表达式构造,可读性更强,更容易将DSL转换成Java代码。

🔍️源码链接,点击查看👈️

  • 29
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值