乐优商城项目---day12-基本搜索

5.6.聚合

5.6.1.聚合为桶

桶就是分组,比如这里我们按照品牌brand进行分组:这个聚合查询还是比较复杂的,相对于前面的排序和分页查询,有很多子接。

聚合查询:

 @Test
    public void testJuhe(){
        NativeSearchQueryBuilder queryBuilder=new NativeSearchQueryBuilder();
        //不查询结果集合
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""},null));
        //设置查询类型为词条查询,查询名称为,brands,查询字段为brand
        queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand"));
        //执行查询,执行查询并转换类型为AggregatedPage
        AggregatedPage<Item> page= (AggregatedPage<Item>) this.repository.search(queryBuilder.build());
        //获取查询结果中brads的结果集合并转换为字符串的词条
        StringTerms brand = (StringTerms) page.getAggregation("brands");
        //获取桶,之后遍历得到相应的key和value
        List<StringTerms.Bucket> buckets = brand.getBuckets();
        for (StringTerms.Bucket bucket : buckets) {
            System.out.println(bucket.getKeyAsString());
            System.out.println(bucket.getDocCount());
        }
    }

完成测试:

嵌套子查询加求平均值

  @Test
    public void avgAndTest(){
        NativeSearchQueryBuilder queryBuilder=new NativeSearchQueryBuilder();
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""},null));
        queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand").subAggregation(AggregationBuilders.avg("price_avg").field("price")));
       AggregatedPage<Item> aggregatedPage= (AggregatedPage<Item>) this.repository.search(queryBuilder.build());
        StringTerms brands = (StringTerms) aggregatedPage.getAggregation("brands");
        List<StringTerms.Bucket> buckets = brands.getBuckets();
        for (StringTerms.Bucket bucket : buckets) {
            System.out.println(bucket.getKeyAsString());
            System.out.println(bucket.getDocCount());
            InternalAvg price_avg = (InternalAvg) bucket.getAggregations().asMap().get("price_avg");
            System.out.println("平均价格为:"+price_avg.getValue());
        }
    }

完成测试:

-------------------------------------------------------------------------------------------------------

1.索引库数据导入

昨天我们学习了Elasticsearch的基本应用。今天就学以致用,搭建搜索微服务,实现搜索功能。

1.1.创建搜索服务

创建模块,之后添加依赖

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.search</groupId>
    <artifactId>leyou-search</artifactId>
    <dependencies>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- elasticsearch -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <!-- eureka -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.item</groupId>
            <artifactId>leyou-item-interface</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.leyou.common</groupId>
            <artifactId>leyou-common</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

    </dependencies>

</project>

添加对应的配置文件

server:
  port: 8083
spring:
  application:
    name: search-service
  data:
    elasticsearch:
      cluster-name: elasticsearch
      cluster-nodes: 192.168.1.99:9300
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期

添加引导类

@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class LeyouSearchAppliction {

    public static void main(String[] args) {
        SpringApplication.run(LeyouSearchAppliction.class);

    }
}

1.2.索引库数据格式分析

接下来,我们需要商品数据导入索引库,便于用户搜索。

那么问题来了,我们有SPU和SKU,到底如何保存到索引库?

1.2.1.以结果为导向

因为要用搜索引擎去搜索

这些过滤条件也都需要存储到索引库中,包括:

商品分类、品牌、可用来搜索的规格参数等

综上所述,我们需要的数据格式有:

spuId、SkuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数

1.2.3.最终的数据结构

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

@Document(indexName = "goods", type = "docs", shards = 1, replicas = 0)
public class Goods {
    @Id
    private Long id; // spuId
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    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<Long> price;// 价格
    @Field(type = FieldType.Keyword, index = false)
    private String skus;// List<sku>信息的json结构
    private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值
}

一些特殊字段解释:

  • all:用来进行全文检索的字段,里面包含标题、商品分类信息

  • price:价格数组,是所有sku的价格集合。方便根据价格进行筛选过滤

  • skus:用于页面展示的sku信息,不索引,不搜索。包含skuId、image、price、title字段

  • specs:所有规格参数的集合。key是参数名,值是参数值。

    例如:我们在specs中存储 内存:4G,6G,颜色为红色,转为json就是:

    {
        "specs":{
            "内存":[4G,6G],
            "颜色":"红色"
        }
    }

    当存储到索引库时,elasticsearch会处理为两个字段:

    • specs.内存:[4G,6G]

    • specs.颜色:红色

    另外, 对于字符串类型,还会额外存储一个字段,这个字段不会分词,用作聚合。

    • specs.颜色.keyword:红色

接下来就是创建微服务接口:创建微服务接口的时候要创建远程的feign客户端进行远程调用,首先创建API接口

BrandApi

@RequestMapping("brand")
public interface BrandApi {
    /**
     * 根据bid查询品牌名称
     */
    @GetMapping("{id}")
    public String queryNameById(@PathVariable("id")Long id);

}

CategoryApi

@RequestMapping("category")
public interface CategoryApi {

    /**
     * 根据ids查询分类名称
     */
    @GetMapping
    public List<String> queryNameByids(@RequestParam("ids")List<Long> ids);

}

GoodsApi

@RequestMapping("/goods")
public interface GoodsApi {

    @RequestMapping("spu/page")
    public PageResult<SpuBo> queryGoods(
            @RequestParam(value = "key",required = false) String key,
            @RequestParam(value = "saleable",required = false) String saleable,
            @RequestParam(value = "page",defaultValue = "1") Integer page,
            @RequestParam(value = "rows",defaultValue = "5") Integer rows
    );


    @GetMapping("spu/detail/{spuId}")
    public SpuDetail queryBySpuId(@PathVariable("spuId")Long spuId);

    @GetMapping("sku/list")
    public List<Sku> querBySpu(@RequestParam("id")Long spuId);

}

SpecificationApi

@RequestMapping("spec")
public interface SpecificationApi {


    @GetMapping("groups/{cid}")
    public List<SpecGroup> querySpecGroyo(@PathVariable("cid") Long cid);

    @GetMapping("params")
    public List<SpecParam> querySpecParam(@RequestParam(value = "gid",required = false) Long gid,
                                                          @RequestParam(value = "cid",required = false)Long cid,
                                                          @RequestParam(value = "generic",required = false)Boolean generic,
                                                          @RequestParam(value = "searching",required = false)Boolean searching);


}

接下来就是在对应的Search微服务中创建对应的FeignClinet进行远程的调用,直接继承该接口就可以了,

BrandClient

@FeignClient(value = "item-service")
public interface BrandClient  extends BrandApi {

}

CategoryClient

@FeignClient(value = "item-service")
public interface CategoryClient extends CategoryApi {


}

SpecificationClient

@FeignClient(value = "item-service")
public interface SpecificationClient extends SpecificationApi {



}

GoodsClient

@FeignClient(value = "item-service")
public interface GoodsClient  extends GoodsApi {


}

接下来就是进行远程接口调用的测试,进行一个Test类的编写测试

测试成功:这个测试的时候要把相应的微服务全部启动起来才可以

接下来这个从Spu构建为Goods的模型比较难,业务逻辑比较复杂,但是拆开来看的话还是容易拆的

1,首先要把Spu构建成goods,因为传过来的参数是Spu,固可以得到spu里面的一些属性和字段,这个里面比较难处理的就是Sku和规格参数,其中这个地方处理Sku的地方是很巧妙的,因为一个Spu下面可以有多个sku,而多个Sku又有多个价格,这个就涉及到一个Spu下也存在多个价格的可能,解决的办法:

①针对一个Spu有多个Sku的解决方案,首先定义一个prices的集合,用来存放多个sku的价格,在其次,可以根据spu的Id查询出Sku的集合,将该单个Sku转换为map,统一存放到一个skuListMap中,这样在相应goods的时候,在把这个map的key,value结构在转换成json字符串响应出去,前台就可以解析处理了。

②规格参数的的特殊参数和通用参数,这个处理起来也是比较绕的,因为规格参数在SpuDetail这个 表里,而根据spuid查询出来的规格参数,只有能获取规格参数名字,而不能获取到规格参数的值,这个处理的办法,首先将SpuDetail进行Map反序列化处理,这样就可以得到kv结构,可以根据paramId,拿到对应的规格参数值,在定义一个paramMap,因为规格参数通过SpuId可以查询处理,得到一个集合,在遍历集合,将规格参数名,通过paramId,可以获取到parm的名字,而spuDetail里面的规格参数值,也可以通过paramId获取到,这样就可以用paramID作为中介,将规格参数名,和规格参数值,结合到一起了:


    public Goods goodsBuilds(Spu spu) throws IOException {
        //创建要返回的Goods对象 第一步必须要创建Goods对象这个后面是为了给Goods对象里面设置属性及其相应的值
        Goods goods=new Goods();
        //查询品牌      根据Spu的ID去查询出来对应的品牌
        Brand brand = this.brandClient.queryNameById(spu.getBrandId());
        //查询分类         根据Spu的ID去查询出来对应的分类名称
        List<String> categoryList = this.categoryClient.queryNameByids(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
        //查询SPu下的所有 Sku     根据SpuID去查询出来对应的下属的Sku的集合
        List<Sku> skus = this.goodsClient.querBySpu(spu.getId());
        //因为一个Spu下面可能有多个Sku,所以多个Sku,就可能有多个价格,定义价格price
        List<Long> prices=new ArrayList<>();
        //定义一个SkuMpa的集合,目的是把sku转成对应的固定的四个属性,之后在同一放到一起
        List<Map<String,Object>> skuMapList=new ArrayList<>();
        //遍历每个skus
        skus.forEach(sku -> {
            //把每个sku的价格放进去
            prices.add(sku.getPrice());
            //开始把每个Sku转换成Map
            Map<String,Object> skuMap=new HashMap<>();
            skuMap.put("id",sku.getId());
            skuMap.put("title",sku.getTitle());
            skuMap.put("price",sku.getPrice());
            skuMap.put("image", StringUtils.isNotBlank(sku.getImages())?StringUtils.split(sku.getImages(),",")[0]:"");
            skuMapList.add(skuMap);
        });
        //查询出所有的规格参数
        List<SpecParam> specParamList = this.specificationClient.querySpecParam(null, spu.getCid3(), null, true);
        //查询出所有的SpuDetail,获取规格参数值
        SpuDetail spuDetail = this.goodsClient.queryBySpuId(spu.getId());
        //获取通用的规格参数反序列化为Map对象  将获取的通用规格参数,反序为Map对象
        Map<Long,Object> genericScap=MAPPER.readValue(spuDetail.getGenericSpec(),new TypeReference<Map<Long,Object>>(){});
       //获取特殊规格参数反序列化为Map对象
        Map<Long,Object> specialSpecMap=MAPPER.readValue(spuDetail.getSpecialSpec(),new TypeReference<Map<Long,List<Object>>>(){});
        //定义map接收{规格参数名,规格参数值}
        Map<String,Object> paramap=new HashMap<>();
        specParamList.forEach(specParam -> {
            //检验是否是通用的规格参数
            if (specParam.getGeneric()){
                String value = genericScap.get(specParam.getId()).toString();
                //判断是否是数值类型
                if (specParam.getNumeric()){
                    //如果是数值类型,判断落在那个区间
                    value=chooseSegment(value,specParam);
                }
                paramap.put(specParam.getName(),value);
            }else{
                paramap.put(specParam.getName(),specialSpecMap.get(specParam.getId()));
            }
        });
        //设置参数
        goods.setId(spu.getId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setBrandId(spu.getBrandId());
        goods.setCreateTime(spu.getCreateTime());
        goods.setSubTitle(spu.getSubTitle());
        goods.setAll(spu.getTitle()+brand.getName()+StringUtils.join(categoryList," "));
        goods.setPrice(prices);
        goods.setSkus(MAPPER.writeValueAsString(skuMapList));
        goods.setSpecs(paramap);
        return goods;
    }
    private String chooseSegment(String value, SpecParam p) {
        double val = NumberUtils.toDouble(value);
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = NumberUtils.toDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if(segs.length == 2){
                end = NumberUtils.toDouble(segs[1]);
            }
            // 判断是否在范围内
            if(val >= begin && val < end){
                if(segs.length == 1){
                    result = segs[0] + p.getUnit() + "以上";
                }else if(begin == 0){
                    result = segs[1] + p.getUnit() + "以下";
                }else{
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

开始构建模型:

   @Test
    public void TestContext(){
        this.elasticsearchTemplate.createIndex(Goods.class);
        this.elasticsearchTemplate.putMapping(Goods.class);
        Integer page=1;
        Integer rows=100;
        do {
            PageResult<SpuBo> pageResult = this.goodsClient.queryGoods(null, true, page, rows);
            List<Goods> goodsList = pageResult.getItems().stream().map(spuBo -> {
                try {
                    return this.searchService.goodsBuilds(spuBo);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }).collect(Collectors.toList());
            this.goodsRepository.saveAll(goodsList);
            rows=pageResult.getItems().size();
            page++;

        }while (rows==100);

    }

构建模型成功:

2.实现基本搜索

2.1.页面分析

2.1.1.页面跳转

在首页的顶部,有一个输入框:

前台页面的AJAX发生的请求:

 var vm = new Vue({
        el: "#searchApp",
        data: {
            search:{
                key:"", // 搜索页面的关键字
            },
            goodList:[]
        }, created() {
            // 判断是否有请求参数
            if (!location.search) {
                return;
            }
            // 将请求参数转为对象
            const search = ly.parse(location.search.substring(1));
            // 记录在data的search对象中
            this.search = search;

            // 发起请求,根据条件搜索
            this.loadData();
        },
        methods: {
            loadData(){
                // ly.http.post("/search/page", ly.stringify(this.search)).then(resp=>{
                ly.http.post("/search/page", this.search).then(resp=>{
                    console.log(resp);
            });
            }
        },
        components:{
            lyTop: () => import("./js/pages/top.js")
        }
    });

 检查:

开始编写后台接口:

Controller:

@RestController
@RequestMapping
public class SearchController {

    @Autowired
    private SearchService searchService;

    @PostMapping("page")
    public ResponseEntity<PageResult<Goods>> queryByPage(@RequestBody SearchRequest searchRequest){
        PageResult<Goods> goodsList= this.searchService.queryByPage(searchRequest);
        if (goodsList.getItems().size()==0|| CollectionUtils.isEmpty(goodsList.getItems())){
            return ResponseEntity.badRequest().build();
        }
        return ResponseEntity.ok(goodsList);
    }

}

Service:

    public PageResult<Goods> queryByPage(SearchRequest searchRequest) {
        // 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品
        String key = searchRequest.getKey();
        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、分页准备分页参数
        Integer page = searchRequest.getPage();
        Integer size = searchRequest.getSize();
        queryBuilder.withPageable(PageRequest.of(page-1,size));
        // 4、查询,获取结果
        Page<Goods> goods = this.goodsRepository.search(queryBuilder.build());
        // 5.封装结果并返回
        return new PageResult<>(goods.getTotalElements(),goods.getTotalPages(),goods.getContent());

    }
}

完成测试:

今日总结:

发布了143 篇原创文章 · 获赞 16 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/zgz102928/article/details/104491196