博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于Spring Data Elasticsearch的搜索微服务
阅读量:2389 次
发布时间:2019-05-10

本文共 14435 字,大约阅读时间需要 48 分钟。

Spring Data Elasticsearch使用

数据导入

module: leyou-search

数据结构

我们创建一个类,封装要保存到索引库的数据,并设置映射属性:

@Document(indexName = "goods", type = "docs", shards = 1, replicas = 0)//索引库goods 表名docs 分1片  0副本public class Goods {
@Id private Long id; // spuId //String 两种选择:Text,Keyword +@Field @Field(type = FieldType.Text, analyzer = "ik_max_word")//Text分词 private String all; // 所有需要被搜索的信息,包含标题,分类,甚至品牌 @Field(type = FieldType.Keyword, index = false) private String subTitle;// 卖点 private Long brandId;// 品牌id private Long cid1;// 1级分类id private Long cid2;// 2级分类id private Long cid3;// 3级分类id private Date createTime;// 创建时间 private List
price;// 价格 @Field(type = FieldType.Keyword, index = false) private String skus;// List
信息的json结构 private Map
specs;// 可搜索的规格参数,key是参数名,值是参数值}

商品微服务提供接口

索引库中的数据来自于数据库,我们不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以我们只能调用商品微服务提供的接口服务。

先思考我们需要的数据:

  • SPU信息
  • SKU信息
  • SPU的详情
  • 商品分类名称(拼接all字段)
  • 品牌名称
  • 规格参数

再思考我们需要哪些服务:

  • 第一:分批查询spu的服务,已经写过。
  • 第二:根据spuId查询sku的服务,已经写过
  • 第三:根据spuId查询SpuDetail的服务,已经写过
  • 第四:根据商品分类id,查询商品分类名称,没写过
  • 第五:根据商品品牌id,查询商品的品牌,没写过
  • 第六:规格参数接口

因此我们需要额外提供(在CategoryController中)查询商品分类名称的接口。

FeignClient

第一步:服务的提供方在leyou-item-interface中提供API接口,并编写接口声明:

  • 我们的服务提供方不仅提供实体类,还要提供api接口声明
  • 调用方不用自己编写接口方法声明,直接继承提供方给的Api接口即可,
    在这里插入图片描述

需要引入springMVC及leyou-common的依赖:

第二步:在调用方leyou-search中编写FeignClient,但不要写方法声明了,直接继承leyou-item-interface提供的api接口:

在这里插入图片描述

测试

在leyou-search中引入springtest依赖:

org.springframework.boot
spring-boot-starter-test
test

创建测试类:

在接口上按快捷键:Ctrl + Shift + T

在这里插入图片描述

测试代码:

@RunWith(SpringRunner.class)@SpringBootTest(classes = LeyouSearchApplication.class)public class CategoryClientTest {
@Autowired private CategoryClient categoryClient; @Test public void testQueryCategories() {
List
names = this.categoryClient.queryNamesByIds(Arrays.asList(1L, 2L, 3L)); names.forEach(System.out::println); }}

在这里插入图片描述

导入数据

在这里插入图片描述

在这里插入图片描述

实现基本搜索

当我们输入任何文本,点击搜索,就会跳转到搜索页search.html了:

并且将搜索关键字以请求参数携带过来:

在这里插入图片描述

后台提供搜索接口

在leyou-gateway中的CORS配置类中,添加允许信任域名:

在这里插入图片描述
并在leyou-gateway工程的Application.yml中添加网关映射:
在这里插入图片描述
在这里插入图片描述

首先分析几个问题:

  • 请求方式:Post

  • 请求路径:/search/page,不过前面的/search应该是网关的映射路径,因此真实映射路径page,代表分页查询

  • 请求参数:json格式,目前只有一个属性:key-搜索关键字,但是搜索结果页一定是带有分页查询的,所以将来肯定会有page属性,因此我们可以用一个对象来接收请求的json数据:

    public class SearchRequest {
    private String key;// 搜索条件 private Integer page;// 当前页 private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小 private static final Integer DEFAULT_PAGE = 1;// 默认页 public String getKey() {
    return key; } public void setKey(String key) {
    this.key = key; } public Integer getPage() {
    if(page == null){
    return DEFAULT_PAGE; } // 获取页码时做一些校验,不能小于1 return Math.max(DEFAULT_PAGE, page); } public void setPage(Integer page) {
    this.page = page; } public Integer getSize() {
    return DEFAULT_SIZE; }}
  • 返回结果:作为分页结果,一般都两个属性:当前页数据、总条数信息,我们可以使用之前定义的PageResult类

代码:

@RestController@RequestMappingpublic class SearchController {
@Autowired private SearchService searchService; /** * 搜索商品 * * @param request * @return */ @PostMapping("page") public ResponseEntity
> search(@RequestBody SearchRequest request) {
PageResult
result = this.searchService.search(request); if (result == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND); } return ResponseEntity.ok(result); }}

service

@Servicepublic class SearchService {
@Autowired private GoodsRepository goodsRepository; public PageResult
search(SearchRequest request) {
String key = request.getKey(); // 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品 if (StringUtils.isBlank(key)) {
return null; } // 构建查询条件 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 1、对key进行全文检索查询 queryBuilder.withQuery(QueryBuilders.matchQuery("all", key).operator(Operator.AND)); // 2、通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle queryBuilder.withSourceFilter(new FetchSourceFilter( new String[]{
"id","skus","subTitle"}, null)); // 3、分页 // 准备分页参数 int page = request.getPage(); int size = request.getSize(); queryBuilder.withPageable(PageRequest.of(page - 1, size)); // 4、查询,获取结果 Page
goodsPage = this.goodsRepository.search(queryBuilder.build()); // 封装结果并返回 return new PageResult<>(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent()); }}

注意点:我们要设置SourceFilter,来选择要返回的结果,否则返回一堆没用的数据,影响查询效率。

测试

数据是查到了,但是因为我们只查询部分字段,所以结果json 数据中有很多null,这很不优雅。

解决办法很简单,在leyou-search的application.yml中添加一行配置,json处理时忽略空值:

spring:  jackson:    default-property-inclusion: non_null # 配置json处理时忽略空值

结果:

在这里插入图片描述

页面渲染

页面分页效果

后台返回的结果中,要包含total和totalPage,我们改造下刚才的接口:

在我们返回的PageResult对象中,其实是有totalPage字段的:

在这里插入图片描述

排序

搜索过滤

在这里插入图片描述

在这里插入图片描述

生成品牌和分类过滤

扩展返回的结果

我们新建一个类,继承PageResult,然后扩展两个新的属性:分类集合和品牌集合:

在这里插入图片描述

public class SearchResult extends PageResult
{
private List
> categories; private List
brands; public SearchResult() {
} public SearchResult(List
> categories, List
brands) { this.categories = categories; this.brands = brands; } public SearchResult(List
items, Long total, List
> categories, List
brands) { super(items, total); this.categories = categories; this.brands = brands; } public SearchResult(List
items, Long total, Integer totalPage, List
> categories, List
brands) { super(items, total, totalPage); this.categories = categories; this.brands = brands; } public List
> getCategories() { return categories; } public void setCategories(List
> categories) { this.categories = categories; } public List
getBrands() { return brands; } public void setBrands(List
brands) { this.brands = brands; }}

聚合商品分类和品牌

我们修改搜索的业务逻辑,对分类和品牌聚合。

因为索引库中只有id,所以我们根据id聚合,然后再根据id去查询完整数据。

所以,商品微服务需要提供一个接口:根据品牌id集合,批量查询品牌。

修改SearchService:

public SearchResult search(SearchRequest request) {
// 判断查询条件 if (StringUtils.isBlank(request.getKey())) {
// 返回默认结果集 return null; } // 初始化自定义查询构建器 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 添加查询条件 queryBuilder.withQuery(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND)); // 添加结果集过滤,只需要:id,subTitle, skus queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
"id", "subTitle", "skus"}, null)); // 获取分页参数 Integer page = request.getPage(); Integer size = request.getSize(); // 添加分页 queryBuilder.withPageable(PageRequest.of(page - 1, size)); String categoryAggName = "categories"; String brandAggName = "brands"; queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3")); queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId")); // 执行搜索,获取搜索的结果集 AggregatedPage
goodsPage = (AggregatedPage
)this.goodsReponsitory.search(queryBuilder.build()); // 解析聚合结果集 List
> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName)); List
brands = getBrandAggResult(goodsPage.getAggregation(brandAggName)); // 封装成需要的返回结果集 return new SearchResult(goodsPage.getContent(), goodsPage.getTotalElements(), goodsPage.getTotalPages(), categories, brands);}/** * 解析品牌聚合结果集 * @param aggregation * @return */private List
getBrandAggResult(Aggregation aggregation) { // 处理聚合结果集 LongTerms terms = (LongTerms)aggregation; // 获取所有的品牌id桶 List
buckets = terms.getBuckets(); // 定义一个品牌集合,搜集所有的品牌对象 List
brands = new ArrayList<>(); // 解析所有的id桶,查询品牌 buckets.forEach(bucket -> { Brand brand = this.brandClient.queryBrandById(bucket.getKeyAsNumber().longValue()); brands.add(brand); }); return brands; // 解析聚合结果集中的桶,把桶的集合转化成id的集合 // List
brandIds = terms.getBuckets().stream().map(bucket -> bucket.getKeyAsNumber().longValue()).collect(Collectors.toList()); // 根据ids查询品牌 //return brandIds.stream().map(id -> this.brandClient.queryBrandById(id)).collect(Collectors.toList()); // return terms.getBuckets().stream().map(bucket -> this.brandClient.queryBrandById(bucket.getKeyAsNumber().longValue())).collect(Collectors.toList());}/** * 解析分类 * @param aggregation * @return */private List
> getCategoryAggResult(Aggregation aggregation) { // 处理聚合结果集 LongTerms terms = (LongTerms)aggregation; // 获取所有的分类id桶 List
buckets = terms.getBuckets(); // 定义一个品牌集合,搜集所有的品牌对象 List
> categories = new ArrayList<>(); List
cids = new ArrayList<>(); // 解析所有的id桶,查询品牌 buckets.forEach(bucket -> { cids.add(bucket.getKeyAsNumber().longValue()); }); List
names = this.categoryClient.queryNamesByIds(cids); for (int i = 0; i < cids.size(); i++) { Map
map = new HashMap<>(); map.put("id", cids.get(i)); map.put("name", names.get(i)); categories.add(map); } return categories;}

生成规格参数过滤

谋而后动

有四个问题需要先思考清楚:

什么情况下显示有关规格参数的过滤?

如果用户尚未选择商品分类,或者聚合得到的分类数大于1,那么就没必要进行规格参数的聚合。因为不同分类的商品,其规格是不同的。

因此,我们在后台需要对聚合得到的商品分类数量进行判断,如果等于1,我们才继续进行规格参数的聚合

如何知道哪些规格需要过滤?

我们不能把数据库中的所有规格参数都拿来过滤。因为并不是所有的规格参数都可以用来过滤,参数的值是不确定的。

值的庆幸的是,我们在设计规格参数时,已经标记了某些规格可搜索,某些不可搜索。

因此,一旦商品分类确定,我们就可以根据商品分类查询到其对应的规格,从而知道哪些规格要进行搜索。

要过滤的参数,其可选值是如何获取的?

虽然数据库中有所有的规格参数,但是不能把一切数据都用来供用户选择。

与商品分类和品牌一样,应该是从用户搜索得到的结果中聚合,得到与结果品牌的规格参数可选值。

规格过滤的可选值,其数据格式怎样的?

我们直接看页面效果:

我们之前存储时已经将数据分段,恰好符合这里的需求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pLKDyUon-1586607445729)(assets/1526805322441.png)]

最终的完整代码

public SearchResult search(SearchRequest request) {
// 判断查询条件 if (StringUtils.isBlank(request.getKey())) {
// 返回默认结果集 return null; } // 初始化自定义查询构建器 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 添加查询条件 MatchQueryBuilder basicQuery = QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND); queryBuilder.withQuery(basicQuery); // 添加结果集过滤,只需要:id,subTitle, skus queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
"id", "subTitle", "skus"}, null)); // 获取分页参数 Integer page = request.getPage(); Integer size = request.getSize(); // 添加分页 queryBuilder.withPageable(PageRequest.of(page - 1, size)); String categoryAggName = "categories"; String brandAggName = "brands"; queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3")); queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId")); // 执行搜索,获取搜索的结果集 AggregatedPage
goodsPage = (AggregatedPage
)this.goodsReponsitory.search(queryBuilder.build()); // 解析聚合结果集 List
> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName)); List
brands = getBrandAggResult(goodsPage.getAggregation(brandAggName)); // 判断分类聚合的结果集大小,等于1则聚合 List
> specs = null; if (categories.size() == 1) { specs = getParamAggResult((Long)categories.get(0).get("id"), basicQuery); } // 封装成需要的返回结果集 return new SearchResult(goodsPage.getContent(), goodsPage.getTotalElements(), goodsPage.getTotalPages(), categories, brands, specs);}/** * 聚合出规格参数过滤条件 * @param id * @param basicQuery * @return */private List
> getParamAggResult(Long id, QueryBuilder basicQuery) { // 创建自定义查询构建器 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 基于基本的查询条件,聚合规格参数 queryBuilder.withQuery(basicQuery); // 查询要聚合的规格参数 List
params = this.specificationClient.queryParams(null, id, null, true); // 添加聚合 params.forEach(param -> { queryBuilder.addAggregation(AggregationBuilders.terms(param.getName()).field("specs." + param.getName() + ".keyword")); }); // 只需要聚合结果集,不需要查询结果集 queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{ }, null)); // 执行聚合查询 AggregatedPage
goodsPage = (AggregatedPage
)this.goodsReponsitory.search(queryBuilder.build()); // 定义一个集合,收集聚合结果集 List
> paramMapList = new ArrayList<>(); // 解析聚合查询的结果集 Map
aggregationMap = goodsPage.getAggregations().asMap(); for (Map.Entry
entry : aggregationMap.entrySet()) { Map
map = new HashMap<>(); // 放入规格参数名 map.put("k", entry.getKey()); // 收集规格参数值 List
options = new ArrayList<>(); // 解析每个聚合 StringTerms terms = (StringTerms)entry.getValue(); // 遍历每个聚合中桶,把桶中key放入收集规格参数的集合中 terms.getBuckets().forEach(bucket -> options.add(bucket.getKeyAsString())); map.put("options", options); paramMapList.add(map); } return paramMapList;}

在这里插入图片描述

页面渲染

  • 把过滤条件保存在search对象中(watch监控到search变化后就会发送到后台)
  • 在页面顶部展示已选择的过滤项
  • 把商品分类展示到顶部面包屑

保存过滤项(vue)

后台添加过滤条件

拓展请求对象

我们需要在请求类:SearchRequest中添加属性,接收过滤属性。过滤属性都是键值对格式,但是key不确定,所以用一个map来接收即可。

在这里插入图片描述

添加过滤条件

目前,我们的基本查询是这样的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ShqTbui-1586607445737)(assets/1533567897849.png)]

现在,我们要把页面传递的过滤条件也加入进去。

因此不能在使用普通的查询,而是要用到BooleanQuery,基本结构是这样的:

GET /heima/_search{
"query":{
"bool":{
"must":{
"match": {
"title": "小米手机",operator:"and"}}, "filter":{
"range":{
"price":{
"gt":2000.00,"lt":3800.00}} } } }}

所以,我们对原来的基本查询进行改造:(SearchService中的search方法)

因为比较复杂,我们将其封装到一个方法中:

/**     * 构建bool查询构建器     * @param request     * @return     */private BoolQueryBuilder buildBooleanQueryBuilder(SearchRequest request) {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 添加基本查询条件 boolQueryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND)); // 添加过滤条件 if (CollectionUtils.isEmpty(request.getFilter())){
return boolQueryBuilder; } for (Map.Entry
entry : request.getFilter().entrySet()) {
String key = entry.getKey(); // 如果过滤条件是“品牌”, 过滤的字段名:brandId if (StringUtils.equals("品牌", key)) {
key = "brandId"; } else if (StringUtils.equals("分类", key)) {
// 如果是“分类”,过滤字段名:cid3 key = "cid3"; } else {
// 如果是规格参数名,过滤字段名:specs.key.keyword key = "specs." + key + ".keyword"; } boolQueryBuilder.filter(QueryBuilders.termQuery(key, entry.getValue())); } return boolQueryBuilder;}

其它不变。

页面测试

我们先不点击过滤条件,直接搜索手机:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wpbFqrdG-1586607445739)(assets/1526910966728.png)]

总共184条

页面展示选择的过滤项

当用户选择一个商品分类以后,我们应该在过滤模块的上方展示一个面包屑,把三级商品分类都显示出来。

在这里插入图片描述

用户选择的商品分类就存放在search.filter中,但是里面只有第三级分类的id:cid3

我们需要根据它查询出所有三级分类的id及名称

搜索系统需要优化的点:

  • 查询规格参数部分可以添加缓存
  • 聚合计算interval变化频率极低,所以可以设计为定时任务计算(周期为天),然后缓存起来。
  • elasticsearch本身有查询缓存,可以不进行优化
  • 商品图片应该采用缩略图,减少流量,提高页面加载速度
  • 图片采用延迟加载
  • 图片还可以采用CDN服务器
  • sku信息应该在页面异步加载,而不是放到索引库

转载地址:http://cxxab.baihongyu.com/

你可能感兴趣的文章
Linux psacct文档
查看>>
使用setuptools自动安装python模块
查看>>
python IDE环境
查看>>
传说中的windows加固 -.... -
查看>>
windows目录监控软件
查看>>
Virus Bulletin malware分析杂志以及paper
查看>>
Security Considerations for AppLocker
查看>>
Oracle Forensics t00ls
查看>>
JetLeak Vulnerability: Remote Leakage Of Shared Buffers In Jetty Web Server [CVE-2015-2080]
查看>>
zZ-ModSecurity Framework支持Web应用安全核心规则集
查看>>
zz-LDAP详解
查看>>
zZ-google-perftools 加速MySQL – TCMalloc
查看>>
apache 防DDOS脚本
查看>>
使用syslog-ng 和stunnel 创建集中式安全日志服务器
查看>>
网友将电视剧潜伏当职场教科书 研究办公室政治
查看>>
graudit
查看>>
使用Hudson和FindBugs进行持续集成和代码检查
查看>>
New Tool: The PenTesters Framework (PTF) Released
查看>>
Detecting and Defending against PowerShell Shells
查看>>
NagVis实物监控工具
查看>>