Elasticsearch-Rest-Client
1. 通过 9300: TCP
- spring-data-elasticsearch:transport-api.jar;
- springboot版本不同,ransport-api.jar不同,不能适配es版本
- 7.x已经不建议使用,8以后就要废弃
2. 通过 9200: HTTP
- jestClient: 非官方,更新慢;
- RestTemplate:模拟HTTP请求,ES很多操作需要自己封装,麻烦;
- HttpClient:同上;
- Elasticsearch-Rest-Client:官方RestClient,封装了ES操作,API层次分明,上手简单;
最终选择Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client); https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
创建 Elasticsearch 检索服务模块
gulimall-search
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>gulimall</artifactId> <groupId>com.ylc.gulimall</groupId> <version>0.0.1-SNAPSHOT</version> <relativePath></relativePath> </parent> <artifactId>gulimall-search</artifactId> <name>gulimall-search</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.7.RELEASE</spring-boot.version> </properties> <dependencies> <dependency> <groupId>com.ylc.gulimall</groupId> <artifactId>gulimall-coupon</artifactId> <version>0.0.1-SNAPSHOT</version> <exclusions> <exclusion> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
父pom
<!-- 这里的属性会被子模块继承 --> <properties> ... <elasticsearch.version>7.4.2</elasticsearch.version> </properties> <!-- 子模块继承父模块之后,提供作用:锁定版本 + 子模块不用再写 version --> <dependencyManagement> <dependencies> ... <!-- 重写覆盖 spring-boot-dependencies 中的依赖版本 --> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>${elasticsearch.version}</version> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>${elasticsearch.version}</version> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-client</artifactId> <version>${elasticsearch.version}</version> </dependency> </dependencies> </dependencyManagement>
YML
spring: application: name: gulimall-search cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 server: port: 13000
主启动类
package com.ylc.gulimallsearch; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class GulimallSearchApplication { public static void main(String[] args) { SpringApplication.run(GulimallSearchApplication.class, args); } }
编写配置类
package com.ylc.gulimallsearch.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MallElasticSearchConfig { /** * 配置请求选项 * 参考:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-low-usage-requests.html#java-rest-low-usage-request-options */ public static final RequestOptions COMMON_OPTIONS; static { RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder(); COMMON_OPTIONS = builder.build(); } @Bean public RestHighLevelClient esRestClient() { return new RestHighLevelClient( RestClient.builder( new HttpHost("192.168.195.100", 9200, "http"))); } }
编写测试类
package com.ylc.gulimallsearch; import org.elasticsearch.client.RestHighLevelClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class GulimallSearchApplicationTests { @Autowired RestHighLevelClient client; @Test void contextLoads() { System.out.println(client); } }
测试成功
测试存储数据
@Test void indexData() throws IOException { IndexRequest indexRequest = new IndexRequest("users"); indexRequest.id("1"); User user=new User(); user.setAge(22); user.setName("ylc"); user.setGender("男"); String json= JSON.toJSONString(user); indexRequest.source(json, XContentType.JSON); //执行操作 IndexResponse index =client.index(indexRequest, MallElasticSearchConfig.COMMON_OPTIONS); System.out.println(index); } @Data public class User { private String name; private String gender; private Integer age; }
查看结果
GET users/_search
整合复杂检索
参考: Search API
/** * 检索地址中带有 mill 的人员年龄分布和平均薪资 */ @Test void searchData() throws IOException { // 1. 创建检索请求 SearchRequest searchRequest = new SearchRequest(); // 指定索引 searchRequest.indices("bank"); // 指定 DSL 检索条件 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 1.1 构建检索条件 address 包含 mill searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill")); // 1.2 按照年龄值分布进行聚合 TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10); searchSourceBuilder.aggregation(ageAgg); // 1.3 计算平均薪资 AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance"); searchSourceBuilder.aggregation(balanceAvg); System.out.println("检索条件:" + searchSourceBuilder.toString()); searchRequest.source(searchSourceBuilder); // 2. 执行检索, 获得响应 SearchResponse searchResponse = client.search(searchRequest, MallElasticSearchConfig.COMMON_OPTIONS); // 3. 分析结果 // 3.1 获取所有查到的记录 SearchHits hits = searchResponse.getHits(); SearchHit[] searchHits = hits.getHits(); for (SearchHit hit : searchHits) { // 数据字符串 String jsonString = hit.getSourceAsString(); System.out.println(jsonString); // 可以通过 json 转换成实体类对象 // Account account = JSON.parseObject(jsonString, Account.class); } // 3.2 获取检索的分析信息(聚合数据等) Aggregations aggregations = searchResponse.getAggregations(); // for (Aggregation aggregation : aggregations.asList()) { // System.out.println("当前聚合名:" + aggregation.getName()); // } Terms ageAgg1 = aggregations.get("ageAgg"); for (Terms.Bucket bucket : ageAgg1.getBuckets()) { String keyAsString = bucket.getKeyAsString(); System.out.println("年龄:" + keyAsString + " 岁的有 " + bucket.getDocCount() + " 人"); } Avg balanceAvg1 = aggregations.get("balanceAvg"); System.out.println("平均薪资: " + balanceAvg1.getValue()); }
检索业务
只有上架的商品存储到Elasticsearch中才能被检索
1. 需要保存 sku 信息
- 当搜索商品名时,查询的是 sku 的标题 sku_title;
- 可能通过 sku 的标题、销量、价格区间检索
2. 需要保存品牌、分类等信息
- 点击分类,检索分类下的所有信息
- 点击品牌,检索品牌下的商品信息
3. 需要保存 spu 信息
- 选择规格,检索共有这些规格的商品
方案1-空间换时间
{ skuId:1 spuId:11 skyTitile:华为xx price:999 saleCount:99 attrs:[ {尺寸:5存}, {CPU:高通945}, {分辨率:全高清} ] } # 缺点:会产生冗余字段,对于相同类型的商品,attrs 属性字段会重复,空间占用大 # 好处:方便检索
方案2-时间换空间
sku索引 { skuId:1 spuId:11 } attr索引 { spuId:11 attrs:[ {尺寸:5寸}, {CPU:高通945}, {分辨率:全高清} ] } # 缺点:选择公共属性attr时,会检索当前属性的所有商品分类,然后再查询当前商品分类的所有可能属性; # 导致耗时长。 # 好处:空间利用率高
最终结构
PUT product { "mappings": { "properties": { "skuId": { "type": "long" }, "spuId": { "type": "keyword" }, "skuTitle": { "type": "text", "analyzer": "ik_smart" }, "skuPrice": { "type": "keyword" }, "skuImg": { "type": "keyword", "index": false, "doc_values": false }, "saleCount":{ "type":"long" }, "hasStock": { "type": "boolean" }, "hotScore": { "type": "long" }, "brandId": { "type": "long" }, "catalogId": { "type": "long" }, "brandName": { "type": "keyword", "index": false, "doc_values": false }, "brandImg":{ "type": "keyword", "index": false, "doc_values": false }, "catalogName": { "type": "keyword", "index": false, "doc_values": false }, "attrs": { "type": "nested", "properties": { "attrId": {"type": "long" }, "attrName": { "type": "keyword", "index": false, "doc_values": false }, "attrValue": { "type": "keyword" } } } } } }
结构说明
"mappings": { "properties": { "skuId": { "type": "long" }, "spuId": { "type": "keyword" }, # 精确检索,不分词 "skuTitle": { "type": "text", # 全文检索 "analyzer": "ik_smart" # 分词器 }, "skuPrice": { "type": "keyword" }, "skuImg": { "type": "keyword", "index": false, # false 不可被检索 "doc_values": false # false 不可被聚合 }, "saleCount":{ "type":"long" }, # 商品销量 "hasStock": { "type": "boolean" }, # 商品是否有库存 "hotScore": { "type": "long" }, # 商品热度评分 "brandId": { "type": "long" }, # 品牌id "catalogId": { "type": "long" }, # 分类id "brandName": { # 品牌名,只用来查看,不用来检索和聚合 "type": "keyword", "index": false, "doc_values": false }, "brandImg":{ # 品牌图片,只用来查看,不用来检索和聚合 "type": "keyword", "index": false, "doc_values": false }, "catalogName": { # 分类名,只用来查看,不用来检索和聚合 "type": "keyword", "index": false, "doc_values": false }, "attrs": { # 属性对象 "type": "nested", # 嵌入式,内部属性 "properties": { "attrId": {"type": "long" }, "attrName": { # 属性名 "type": "keyword", "index": false, "doc_values": false }, "attrValue": { "type": "keyword" } # 属性值 } } } }
关于 nested 类型
- Object 数据类型的数组会被扁平化处理为一个简单的键与值的列表,即对象的相同属性会放到同一个数组中,在检索时会出现错误。参考官网:How arrays of objects are flattened
- 对于 Object 类型的数组,要使用 nested 字段类型。参考官网:Using nested fields for arrays of objects
对于这种情况于是就会用到nested类型
商品上架及ElasticSearch检索
利用es检索商品信息,被检索的只能是已经上架的商品,因此商品需要先上架,上架前需要检查商品的库存信息,没有库存不能够上架,符合要求的拿到商品id再上架,最后再把上架的商品插入es的索引结构中,才能够被检索到。
根据es事先建立好的商品信息索引结构在原有SkuInfoEntity类中还多了一些字段,还有一些字段名称不相同,因此在SkuInfoEntity类的基础上建立了SkuesModel类
获取该商品spuId所对应的skuId信息
- 首先发送远程调用查看库存模块是否还有库存
- 根据skuid去库存表
wms_ware_sku查询,返回商品skuId和是否有库存信息的bool值,用Map接收 - 根据商品的skuId集合,获取到对应的Sku实体信息
SkuInfoEntity,把SkuInfoEntity复制到SkuesModel集合,根据skuId查询到品牌的相关信息后赋值,并把不相同无法复制的字段手动设置值。 - 同时获取商品的属性集合,并赋值给SkuesModel.attr
- 最后将封装好的
SkuesModel数据发送给es保存
- 远程调用es检索模块,把
SkuesModel数据批量保存到es索引中 - 根据返回结果,成功再更新商品的上架状态,事务保持统一




