微服务入门(二)JSR303、Elasticsearch

JSR303 数据校验

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<String,String> 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

全文检索使用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"
}

不带_update,可以一直提交post更新,版本会增加
在这里插入图片描述

或者

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
              }
            }
        }
      }
    }
}

在这里插入图片描述
也可以与must组合使用,对结果进行过滤

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"
        }}
      ]
    }
  }
}

在这里插入图片描述
精确匹配
查询字段加上keyword,就可以精确匹配

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": "我是中国人"
}

在这里插入图片描述
使用另一个分词器:ik_max_word

POST _analyze
{
  "analyzer": "ik_max_word",
  "text": "我是中国人"
}

在这里插入图片描述

Elasticsearch-Rest-Client

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("<b style='color:red'>");
            highlightBuilder.postTags("</b>");
            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, "");
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值