javax.validation.constraints 中定义了非常多的校验注解,@Email、 @Future、 @NotBlank、 @Size 等,BindingResult 获取校验结果,
给Bean添加校验注解:javax.validation.constraints,并定义自己的message提示
比如
@NotBlank(message = "品牌名必须提交")
private String name;
开启校验功能@Valid,给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
public R save(@Valid @RequestBody BrandEntity brand,BindingResult result){
这个需要自己处理错误逻辑
// if(result.hasErrors()){
// Map map = new HashMap<>();
// //1、获取校验的错误结果
// result.getFieldErrors().forEach((item)->{
// //FieldError 获取到错误提示
// String message = item.getDefaultMessage();
// //获取错误的属性的名字
// String field = item.getField();
// map.put(field,message);
// });
//
// return R.error(400,"提交的数据不合法").put("data",map);
// }else {
//
// }
可以写一个统一的异常处理,编写异常处理类,使用@ControllerAdvice,使用@ExceptionHandler标注方法可以处理的异常。
/**
* 集中处理所有异常
*/
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
@ExceptionHandler(value= MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){
log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError)->{
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
log.error("错误:",throwable);
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*
*
*/
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
在@RestControllerAdvice中标注的包里的文件,都能使用这个异常,把BindingResult result去掉就可以了
public R save(@Valid @RequestBody BrandEntity brand){
对于多场景的复杂校验,比如新增需要校验,更新不需要校验的情况,需要分组
1)、 @NotBlank(message = “品牌名必须提交”,groups = {AddGroup.class,UpdateGroup.class}), 给校验注解标注什么情况需要进行校验
AddGroup和UpdateGroup是两个接口,
public interface AddGroup {
}
public interface UpdateGroup {
}
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {
UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {
AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交",groups = {
AddGroup.class,UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(groups = {
AddGroup.class})
@URL(message = "logo必须是一个合法的url地址",groups={
AddGroup.class,UpdateGroup.class})
private String logo;
校验的controller里也增加分组
public R save(@Validated({
AddGroup.class}) @RequestBody BrandEntity brand/*,BindingResult result*/){
默认没有指定分组的校验注解@NotBlank,在分组校验情况@Validated({AddGroup.class})下不生效,只会在@Validated生效;
也可以自定义校验,首先编写一个自定义的校验注解,然后编写一个自定义的校验器 ConstraintValidator,最后关联自定义的校验器和自定义的校验注解
@Documented
@Constraint(validatedBy = {
ListValueConstraintValidator.class })
@Target({
METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.atguigu.common.valid.ListValue.message}";
Class<?>[] groups() default {
};
Class<? extends Payload>[] payload() default {
};
int[] vals() default {
};
}
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
//判断是否校验成功
/**
*
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
使用
@NotNull(groups = {
AddGroup.class})
@ListValue(vals={
0,1},groups = {
AddGroup.class})
private Integer showStatus;
可以自定义错误信息,在resouces文件夹下,新建ValidationMessages.properties
com.atguigu.common.valid.ListValue.message=必须提交指定的值
规范每个接口返回数据的格式
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
//利用fastJson进行逆转
public <T>T getData(TypeReference<T> typeReference){
Object data = get("data");//默认map
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
public R setData(Object data){
put("data",data);
return this;
}
//利用fastJson进行逆转
public <T>T getData2(String key,TypeReference<T> typeReference){
Object data = get(key);//默认map
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode() {
return (Integer) this.get("code");
}
}
使用
1. return R.ok().put("page", page);
2.OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
return R.ok().setData(orderEntity);
3. FareVo data = fare.getData(new TypeReference<FareVo>() {
});
4. MemberAddressVo data = r.getData2("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
});
全文检索使用Elasticsearch,它可以快速地储存、 搜索和分析海量数据。Elastic 是 Lucene 的封装, 提供了 REST API 的操作接口, 开箱即用。
REST API: 天然的跨平台。
官方文档
Index(索引)
动词, 相当于 MySQL 中的 insert;
名词, 相当于 MySQL 中的 Database
Type(类型)
在 Index(索引) 中, 可以定义一个或多个类型。
类似于 MySQL 中的 Table; 每一种类型的数据放在一起;
Document(文档)
保存在某个索引(Index) 下, 某种类型(Type) 的一个数据(Document) , 文档是 JSON 格式的, Document 就像是 MySQL 中的某个 Table 里面的内容;
搜索速度快主要是因为使用了倒排索引机制
我这边没办法使用linux,所以没办法使用docker安装,分享一下win7安装Elasticsearch。Windows上安装ElasticSearch7
安装完成以后,http://localhost:9200/,显示
说明Elasticsearch运行成功,访问http://localhost:9100/,
说明elasticsearch-head-master运行成功,使用postman可以访问
为了和教程保持一致,准备使用kibana,刚开始下载的最新版7.15.0,结果打开就闪退,可能是版本太高,最后下载了6.8.1,解压以后,点击bin目录下的kibana.bat启动,如果启动页面是这样,说明已启动
访问http://localhost:5601/app/kibana,
在Dev tools页面测试
1、 _cat
GET /_cat/nodes: 查看所有节点
GET /_cat/health: 查看 es 健康状况
GET /_cat/master: 查看主节点
GET /_cat/indices: 查看所有索引 show databases;
2、 索引一个文档(保存)
保存一个数据, 保存在哪个索引的哪个类型下, 指定用哪个唯一标识
PUT customer/external/1; 在 customer 索引下的 external 类型下保存 1 号数据为
PUT customer/external/1
PUT 和 POST 都可以,
POST 新增。 如果不指定 id, 会自动生成 id。 指定 id 就会修改这个数据, 并新增版本号
PUT 可以新增可以修改。 PUT 必须指定 id; 由于 PUT 需要指定 id, 我们一般都用来做修改操作, 不指定 id 会报错
3、 查询文档
GET customer/external/1
{
"_index": "customer", //在哪个索引
"_type": "external", //在哪个类型
"_id": "1", //记录 id
"_version": 1, //版本号
"_seq_no": 0, //并发控制字段, 每次更新就会+1, 用来做乐观锁
"_primary_term": 1, //同上, 主分片重新分配, 如重启, 就会变化
"found": true,
"_source": {
//真正的内容
"name": "John Doe"
}
}
乐观锁
进行修改,需要带上if_primary_term和_seq_no,来判断是否能更改,?if_seq_no=0&if_primary_term=1,如果符合则更改成功,如果不符合,已经在提交之前,被其他人修改过,会报错
4、 更新文档
POST customer/external/1/_update
{
"doc":{
"name": "John Doew"
}
}
带_update 对比元数据如果一样就不进行任何操作。我这是提交了两次,第二次就出现这个结果,带_update,提交数据必须带"doc"
或者
POST customer/external/1
{
"name": "John Doe2"
}
或者
PUT customer/external/1
{
"name": "John Doe"
}
修改数据,版本号也会一直增加
put和post(不带_update),会一直更新,如果带_update 对比元数据如果一样就不进行任何操作。
看场景;
对于大并发更新, 不带 update;
对于大并发查询偶尔更新, 带 update; 对比更新, 重新计算分配规则。
更新同时增加属性
POST customer/external/1/_update
{
"doc": {
"name": "Jane Doe", "age": 20 }
}
PUT 和 POST 不带_update 也可以
5、 删除文档&索引
DELETE customer/external/1
DELETE customer
6、批量导入数据
使用bulk可以批量导入数据
POST customer/external/_bulk
{
"index":{
"_id":"1"}}
{
"name": "John Doe" }
{
"index":{
"_id":"2"}}
{
"name": "Jane Doe" }
语法
{
action: {
metadata }}\n
{
request body }\n
{
action: {
metadata }}\n
{
request body }\n
{
"index":{
"_id":"1"}}
{
"name": "John Doe" }
这两句属于一个操作,给新增数据指定id,保存数据位name
在kibana上测试,postman不能测试,
复杂实例
POST /_bulk
{
"delete": {
"_index": "website", "_type": "blog", "_id": "123" }}
{
"create": {
"_index": "website", "_type": "blog", "_id": "123" }}
{
"title": "My first blog post" }
{
"index": {
"_index": "website", "_type": "blog" }}
{
"title": "My second blog post" }
{
"update": {
"_index": "website", "_type": "blog", "_id": "123"} }
{
"doc" : {
"title" : "My updated blog post"} }
批量导入测试数据,可以从
accounts.json获取,
通过postman查询一下,发现已经有了bank的数据
1、 SearchAPI
ES 支持两种基本方式检索 :
1、一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
2、另一个是通过使用 REST request body 来发送它们(uri+请求体)
1) 、 检索信息
一切检索从_search 开始
请求参数方式检索
GET bank/_search?q=*&sort=account_number:asc
响应结果解释:
took - Elasticsearch 执行搜索的时间( 毫秒)
time_out - 告诉我们搜索是否超时
_shards - 告诉我们多少个分片被搜索了, 以及统计了成功/失败的搜索分片
hits - 搜索结果
hits.total - 搜索结果
hits.hits - 实际的搜索结果数组( 默认为前 10 的文档)
sort - 结果的排序 key( 键) ( 没有则按 score 排序)
score 和 max_score –相关性得分和最高得分( 全文检索用)
第二种方式,uri+请求体进行检索
GET bank/_search
{
"query": {
"match_all": {
}
},
"sort": [
{
"account_number": "asc"
},
{
"balance": "desc"
}
]
}
这种语法就是Query DSL
Query DSL
Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL( domain-specific language 领域特定语言) 。 这个被称为 Query DSL。
一个查询语句 的典型结构
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
如果是针对某个字段, 那么它的结构如下:
{
QUERY_NAME: {
FIELD_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
}
比如
GET bank/_search
{
"query": {
"match_all": {
}
},
"from": 0,
"size": 5,
"sort": [
{
"account_number": {
"order": "desc"
}
}
]
}
query 定义如何查询,
1. match_all 查询类型【代表查询所有的所有】 , es 中可以在 query 中组合非常多的查询类型完成复杂查询
2.除了 query 参数之外, 我们也可以传递其它的参数以改变查询结果。 如 sort, size
3.from+size 限定, 完成分页功能
4.sort 排序, 多字段排序, 会在前序字段相等时后续字段内部排序, 否则以前序为准
返回部分字段
通过"_source": 可以指定返回的部分字段
GET bank/_search
{
"query": {
"match_all": {
}
},
"from": 0,
"size": 5,
"_source": ["age","balance"]
}
match【 匹配查询】
可以使用match来进行匹配,作为查询条件,基本类型是全局匹配,字符串就是全局检索了
GET bank/_search
{
"query": {
"match": {
"account_number": "20"
}
}
}
GET bank/_search
{
"query": {
"match": {
"address": "Mill Avenue"
}
}
}
最终查询出 address 中包含 Mill 或者 Avenue或者 Mill Avenue的所有记录, 并给出相关性得分,分数高的匹配最高,就是之前说的倒排索引。
match_phrase【 短语匹配】
把match换成match_phrase就会将需要匹配的值当成一个整体单词( 不分词) 进行检索
multi_match【 多字段匹配】
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": ["state","address"]
}
}
}
这个会查询出state 或者 address 包含 mill的数据
bool【 复合查询】
复合语句可以合并 任何 其它查询语句, 包括复合语句, 复合语句之间可以互相嵌套, 可以表达非常复杂的逻辑。
must: 必须达到 must 列举的所有条件
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill" } },
{
"match": {
"gender": "M" } }
]
}
}
}
匹配address包含mill和gender为M的数据
must_not 必须不是指定的情况
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill" } },
{
"match": {
"gender": "M" } }
],
"should": [
{
"match": {
"address": "lane" }}
],
"must_not": [
{
"match": {
"email": "baluba.com" }}
]
}
}
}
这里边有个should,应该达到 should 列举的条件, 如果达到会增加相关文档的评分, 并不会改变查询的结果。 如果 query 中只有 should 且只有一种匹配规则, 那么 should 的条件就会被作为默认匹配条件而去改变查询结果
filter【结果过滤】
并不是所有的查询都需要产生分数, 特别是那些仅用于 “filtering”(过滤) 的文档。 为了不计算分数 Elasticsearch 会自动检查场景并且优化查询的执行
GET bank/_search
{
"query": {
"bool": {
"filter": {
"range": {
"balance": {
"gte": 10000,
"lte": 20000
}
}
}
}
}
}
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill"}}
],
"filter": {
"range": {
"balance": {
"gte": 10000,
"lte": 20000
}
}
}
}
}
}
term
和 match 一样。 匹配某个属性的值。 全文检索字段用 match, 其他非 text 字段匹配用 term
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"age": {
"value": "28"
}
}},
{
"match": {
"address": "990 Mill Road"
}}
]
}
}
}
GET bank/_search
{
"query": {
"match": {
"address.keyword": "990 Mill Road"
}
}
}
aggregations( 执行聚合)
聚合提供了从数据中分组和提取数据的能力。 最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。 在 Elasticsearch 中, 您有执行搜索返回 hits( 命中结果) , 并且同时返回聚合结果, 把一个响应中的所有 hits( 命中结果) 分隔开的能力。 这是非常强大且有效的,您可以执行查询和多个聚合, 并且在一次使用中得到各自的( 任何一个的) 返回结果, 使用一次简洁和简化的 API 来避免网络往返
1.搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄, 但不显示这些人的详情。
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"group_by_state": {
"terms": {
"field": "age"
}
},
"avg_age": {
"avg": {
"field": "age"
}
}
},
"size": 0
}
size: 0 不显示搜索数据
aggs: 执行聚合。 聚合语法如下
"aggs": {
"aggs_name 这次聚合的名字, 方便展示在结果集中": {
"AGG_TYPE 聚合的类型( avg,term,terms) ": {
}
}
},
2.按照年龄聚合, 并且请求这些年龄段的这些人的平均薪资
GET bank/account/_search
{
"query": {
"match_all": {
}
},
"aggs": {
"age_avg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"banlances_avg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 100
}
复杂: 查出所有年龄分布, 并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄段的总体平均薪资
GET bank/account/_search
{
"query": {
"match_all": {
}
},
"aggs": {
"age_agg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"gender_agg": {
"terms": {
"field": "gender.keyword",
"size": 100
},
"aggs": {
"balance_avg": {
"avg": {
"field": "balance"
}
}
}
},
"balance_avg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 1000
}
Mapping(映射)
Mapping 是用来定义一个文档( document) , 以及它所包含的属性( field) 是如何存储和索引的,比如, 使用 mapping 来定义:
1.哪些字符串属性应该被看做全文本属性(full text fields) 。
2. 哪些属性包含数字, 日期或者地理位置。
3. 文档中的所有属性是否都能被索引(_all 配置) 。
4.日期的格式。
5. 自定义映射规则来执行动态添加属性
新建的时候会猜测数据类型,比如
如果要修改数据类型,可以使用
PUT /my-index
{
"mappings": {
"properties": {
"age": {
"type": "integer" },
"email": {
"type": "keyword" },
"name": {
"type": "text" }
}
}
}
然后使用GET my-index/_mapping 查看
如果对现有索引添加新字段,需要使用
PUT /my-index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false
}
}
}
“index”: false设置这个字段不能被索引,不被索引就不会被检索到
对于已经存在的映射字段, 我们不能更新。 更新必须创建新的索引进行数据迁移。
数据迁移
通过GET my-index/_mapping ,查看my-index里的字段类型,
然后新建一个new_my-index,修改里边的字段类型,比如修改age为type
PUT /new_my-index
{
"mappings": {
"properties": {
"age": {
"type": "long"
},
"email": {
"type": "keyword"
},
"name": {
"type": "text"
},
"employee-id": {
"type": "keyword"
}
}
}
}
POST _reindex
{
"source": {
"index": "my-index"
},
"dest": {
"index": "new_my-index"
}
}
分词
一个 tokenizer( 分词器) 接收一个字符流, 将之分割为独立的 tokens( 词元, 通常是独立的单词) , 然后输出 tokens 流
使用ik分词器,IK分词器的安装与使用IK分词器创建索引
使用默认分词器
POST _analyze
{
"text": "我是中国人"
}
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
POST _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
Elasticsearch与spring boot整合,需要整合Elasticsearch-Rest-Client,官方文档
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
配置
@Bean
RestHighLevelClient client() {
RestClientBuilder builder = RestClient.builder(new HttpHost("192.168.56.10", 9200,
"http"));
return new RestHighLevelClient(builder);
}
测试
@Test
void test1() throws IOException {
Product product = new Product();
product.setSpuName("华为");
product.setId(10L);
IndexRequest request = new IndexRequest("product").id("20")
.source("spuName","华为","id",20L);
try {
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(request.toString());IndexResponse response2 = client.index(request, RequestOptions.DEFAULT);
} catch (ElasticsearchException e) {
if (e.status() == RestStatus.CONFLICT) {
}
}
}
spring boot与es的实战
商品上架和搜索功能,es对应的mapping
PUT gulimall_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"
}
}
}
}
}
}
@Configuration
public class GulimallElasticSearchConfig {
//全局通用设置项,单实例singleton,构建授权请求头,异步等信息
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
// builder.addHeader("Authorization","Bearer"+TOKEN);
// builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory.HeapBufferedResponseConsumerFactory(30*1024*1024*1024));
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient esRestClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.56.10", 9200, "http")));
return client;
}
}
EsConstant.java
public class EsConstant {
public static final String PRODUCT_INDEX = "gulimall_product";//sku数据在es中的索引
public static final Integer PRODUCT_PAGESIZE = 16;//每页显示数量
}
ElasticSearchSaveController.java 商品上架功能,把数据上传到es
@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSearchSaveController {
@Autowired
private ProductSaveService productSaveService;
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
boolean b = false;
try {
b = productSaveService.productStatusUp(skuEsModels);
} catch (Exception e) {
log.error("ElasticSaveController商品上架错误:{}", e);
}
if (!b) {
return R.ok();
} else {
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}
ProductSaveServiceImpl .java
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
//保存到es
//1.给es建立索引product(在Kibana中操作!)
//2.给es保存数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel model : skuEsModels) {
//构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(model.getSkuId().toString());
String s = JSON.toJSONString(model);
indexRequest.source(s, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//TODO 批量处理错误
boolean b = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.info("商品上架完成:{},返回数据:{},",collect,bulk.toString());
return b;
}
}
SearchController.java 商品检索功能,太多业务逻辑代码,重点看关于es的操作
@Controller
public class SearchController {
@Autowired
MallSearchService mallSearchService;
@GetMapping("/list.html")
public String lisgPage(SearchParamVo paramVo, Model model, HttpServletRequest request){
//根据页面传递的数据查询参数,去es中检索商品
String queryString = request.getQueryString();
paramVo.set_queryString(queryString);
SearchResult reslut = mallSearchService.search(paramVo);
model.addAttribute("result",reslut);
return "list";
}
}
MallSearchServiceImpl.java
@Service
public class MallSearchServiceImpl implements MallSearchService {
@Autowired
private RestHighLevelClient client;
@Autowired
ProductFeignService productFeignService;
@Override
public SearchResult search(SearchParamVo paramVo) {
//1.动态构建出查询需要的DSL语句
SearchResult result = null;
//准备检索请求
SearchRequest searchRequest = buildSearchRequest(paramVo);
try {
//执行检索请求
SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//分析封装响应数据
result = buildSearchResult(response, paramVo);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* 准备检索请求:
* 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分类,高亮,聚合分析
*
* @return
*/
private SearchRequest buildSearchRequest(SearchParamVo paramVo) {
//构建DSL语句
SearchSourceBuilder builder = new SearchSourceBuilder();
//查询:模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
//1.构建bool - query
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//1.1 must - 模糊匹配
if (!StringUtils.isEmpty(paramVo.getKeyword())) {
boolQuery.must(QueryBuilders.matchQuery("skuTitle", paramVo.getKeyword()));
}
//1.2 bool - fitler 按照三级分类id查询
if (paramVo.getCatalog3Id() != null) {
boolQuery.filter(QueryBuilders.termQuery("catalogId", paramVo.getCatalog3Id()));
}
//1.2 bool - filter 按照品牌id查询
if (paramVo.getBrandId() != null && paramVo.getBrandId().size() > 0) {
boolQuery.filter(QueryBuilders.termsQuery("brandId", paramVo.getBrandId()));
}
//1.2 bool - filter 按照指定属性进行查询,嵌入式查询,ScoreMode相关性得分
if (paramVo.getAttrs() != null && paramVo.getAttrs().size() > 0) {
for (String attrStr : paramVo.getAttrs()) {
//attrs=1_5寸:8寸&attrs=2_16G:8G
BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
String[] s = attrStr.split("_");
String attrId = s[0];//属性id
String[] attrValues = s[1].split(":");//检索的属性值
nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
//每一个必须得生成一个nested查询
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
boolQuery.filter(nestedQuery);
}
}
//1.2 bool - filter 按照库存进行查询 todo
builder.query(QueryBuilders.termsQuery("hasStock", paramVo.getHasStock() == 1));
//1.2 bool - filter 按照价格区间
if (!StringUtils.isEmpty(paramVo.getSkuPrice())) {
//1_500/_500/500_
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
String[] s = paramVo.getSkuPrice().split("_");
BigDecimal bigDecimal1 = new BigDecimal(s[0]);
if (s.length == 2) {
//gte大于等于,lte小于等于,gt大于,lt小于
//区间
BigDecimal bigDecimal2 = new BigDecimal(s[1]);
rangeQuery.gte(bigDecimal1).lte(bigDecimal2);
} else if (s.length == 1) {
//大于
if (paramVo.getSkuPrice().startsWith("_")) {
rangeQuery.lte(bigDecimal1);
}
//小于
if (paramVo.getSkuPrice().endsWith("_")) {
rangeQuery.gte(bigDecimal1);
}
}
boolQuery.filter(rangeQuery);
}
builder.query(boolQuery);
//排序,分类,高亮
//2.1 排序
if (!StringUtils.isEmpty(paramVo.getSort())) {
//sort = skuPrice_asc/desc
String sort = paramVo.getSort();
String[] s = sort.split("_");
SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
builder.sort(s[0], order);
}
//2.2 分页
//from = (pageNum-1)*pageSize
builder.from((paramVo.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
builder.size(EsConstant.PRODUCT_PAGESIZE);
//2.3 高亮
if (!StringUtils.isEmpty(paramVo.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("");
highlightBuilder.postTags("");
builder.highlighter(highlightBuilder);
}
//聚合分析
//1.品牌聚合
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
brand_agg.field("brandId").size(50);
//1.1品牌聚合的子聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
builder.aggregation(brand_agg);
//2.分类聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
builder.aggregation(catalog_agg);
//3.属性聚合(嵌入式聚合)
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
//聚合出当前所有的attr_id
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
//聚合分析当前attr_id对应的名字
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
//聚合分析当前attr_id对应的所有可能的属性值attrValue
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
attr_agg.subAggregation(attr_id_agg);
builder.aggregation(attr_agg);
String s = builder.toString();
System.out.println("DSL:" + s);
SearchRequest searchRequest = new SearchRequest(new String[]{
EsConstant.PRODUCT_INDEX}, builder);
return searchRequest;
}
/**
* 分析封装检索结果
*
* @return
*/
private SearchResult buildSearchResult(SearchResponse response, SearchParamVo paramVo) {
SearchResult result = new SearchResult();
//1.返回所有查询到的商品
SearchHits hits = response.getHits();
List<SkuEsModel> esModelList = new ArrayList<>();
if (hits.getHits() != null && hits.getHits().length > 0) {
for (SearchHit hit : hits.getHits()) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
if (!StringUtils.isEmpty(paramVo.getKeyword())) {
//设置高亮内容
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String string = skuTitle.getFragments()[0].string();
skuEsModel.setSkuTitle(string);
}
esModelList.add(skuEsModel);
}
}
result.setProducts(esModelList);
//2.当前所有商品涉及到的所有属性信息
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
ParsedNested attr_agg = response.getAggregations().get("attr_agg");
ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");//根据返回值确定数据类ParsedLongTerms,ParsedNested
for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
//1.得到属性的id
long attrId = bucket.getKeyAsNumber().longValue();
//2.得到属性的名字
String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
//3.得到属性的所有值
List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {
String keyAsString = ((Terms.Bucket) item).getKeyAsString();
return keyAsString;
}).collect(Collectors.toList());
attrVo.setAttrId(attrId);
attrVo.setAttrName(attrName);
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
//3.当前所有商品涉及到的所有品牌信息
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
for (Terms.Bucket bucket : brand_agg.getBuckets()) {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
//1.品牌id
long brandId = bucket.getKeyAsNumber().longValue();
//2.品牌名字
String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
//3.品牌图片
String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
brandVo.setBrandId(brandId);
brandVo.setBrandName(brandName);
brandVo.setBrandImg(brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
//4.当前所有商品涉及到的所有分类信息
ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
for (Terms.Bucket bucket : buckets) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
//得到分类id
String keyAsString = bucket.getKeyAsString();
catalogVo.setCatalogId(Long.parseLong(keyAsString));
//得到分类名
ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalog_name);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
//5.当前所有商品涉及到的所有分页信息
//页码
result.setPageNum(paramVo.getPageNum());
// result.setPageNum();
//总计录数
long total = hits.getTotalHits().value;
result.setTotal(total);
//总页码
Long totalPages = total % EsConstant.PRODUCT_PAGESIZE == 0 ? total / EsConstant.PRODUCT_PAGESIZE : (total / EsConstant.PRODUCT_PAGESIZE) + 1;
result.setTotalPage(totalPages);
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPages; i++) {
pageNavs.add(i);
}
result.setPageNavs(pageNavs);
//6.构建面包屑导航
if (paramVo.getAttrs() != null && paramVo.getAttrs().size() > 0) {
List<SearchResult.NavVo> collect = paramVo.getAttrs().stream().map(attr -> {
SearchResult.NavVo navVo = new SearchResult.NavVo();
//分析每个attr的参数值
String[] s = attr.split("_");
navVo.setNavValue(s[1]);
R r = productFeignService.attrInfo(Long.parseLong(s[0]));
result.getAttrIds().add(Long.parseLong(s[0]));
if (r.getCode() == 0) {
AttrResponseVo attrs = r.getData2("attr", new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(attrs.getAttrName());
} else {
navVo.setNavName(s[0]);
}
String replace = replaceQueryString(paramVo, attr, "attrs");
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
return navVo;
}).collect(Collectors.toList());
result.setNavs(collect);
}
//品牌、分类面包屑导航
if (paramVo.getBrandId() != null && paramVo.getBrandId().size() > 0) {
List<SearchResult.NavVo> navs = result.getNavs();
SearchResult.NavVo navVo = new SearchResult.NavVo();
navVo.setNavName("品牌");
//远程查询所有品牌
R r = productFeignService.brandsInfo(paramVo.getBrandId());
if (r.getCode() == 0) {
List<BrandVo> brands = r.getData2("brands", new TypeReference<List<BrandVo>>() {
});
StringBuffer buffer = new StringBuffer();
String replace = "";
for (BrandVo brandVo : brands){
buffer.append(brandVo.getName()+";");
replace = replaceQueryString(paramVo,brandVo.getBrandId()+"","brandId");
}
navVo.setNavValue(buffer.toString());
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
}
navs.add(navVo);
}
return result;
}
//取消了面包屑之后,跳转的位置(将请求地址的url替换,置空)
private String replaceQueryString(SearchParamVo paramVo, String value, String key) {
String encode = null;
try {
//编码
encode = URLEncoder.encode(value, "UTF-8");
encode = encode.replace("+", "%20");//对空格特殊处理(将空格变为%20)
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return paramVo.get_queryString().replace("&" + key + "=" + encode, "");
}
}