电商项目-基于ElasticSearch实现商品搜索功能(三)

本系列文章主要介绍基于 Spring Data Elasticsearch 实现商品搜索的后端代码,介绍代码逻辑和代码实现。

主要实现功能:根据搜索关键字查询、条件筛选、规格过滤、价格区间搜索、搜索查询分页、搜索查询排序、高亮查询。

主要应用技术:canal,Eureka,微服务架构(Microservices Architecture),Spring Data Elasticsearch

一、 搜索分页

1 分页分析

基于spring data ElasticSearch 对于查询结果进行分页操作。

页面需要实现分页搜索,所以我们后台每次查询的时候,需要实现分页。用户页面每次会传入当前页和每页查询多少条数据,当然如果不传入每页显示多少条数据,设置默认查询条即可。

2 分页实现

分页使用PageRequest.of( pageNo- 1, pageSize);实现,第1个参数表示第N页,从0开始,第2个参数表示每页显示多少条,实现代码如下:

代码如下:

java 复制代码
    @Override
    public Map search(Map<String, String> searchMap) throws Exception {
        Map<String, Object> resultMap = new HashMap<>();
​
        //有条件才查询Es
        if (null != searchMap) {
            //组合条件对象
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            //0:关键词
            if (!StringUtils.isEmpty(searchMap.get("keywords"))) {
                boolQuery.must(QueryBuilders.matchQuery("name", searchMap.get("keywords")).operator(Operator.AND));
            }
            //1:条件 品牌
            if (!StringUtils.isEmpty(searchMap.get("brand"))) {
                boolQuery.filter(QueryBuilders.termQuery("brandName", searchMap.get("brand")));
            }
​
            //2:条件 规格
            for (String key : searchMap.keySet()) {
                if (key.startsWith("spec_")) {
                    String value = searchMap.get(key).replace("%2B", "+");
                    boolQuery.filter(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword",value));
                }
            }
            //3:条件 价格
            if (!StringUtils.isEmpty(searchMap.get("price"))) {
                String[] p = searchMap.get("price").split("-");
                boolQuery.filter(QueryBuilders.rangeQuery("price").gte(p[0]));
                if (p.length == 2) {
                    boolQuery.filter(QueryBuilders.rangeQuery("price").lte(p[1]));
                }
            }
​
            //4. 原生搜索实现类
            NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
            nativeSearchQueryBuilder.withQuery(boolQuery);
​
            //6. 品牌聚合(分组)查询
            String skuBrand = "skuBrand";
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuBrand).field("brandName"));
​
            //7. 规格聚合(分组)查询
            String skuSpec = "skuSpec";
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuSpec).field("spec.keyword"));
​
​
            String pageNum = searchMap.get("pageNum");
            if (null == pageNum) {
                pageNum = "1";
            }
            //9: 分页
            nativeSearchQueryBuilder.withPageable(PageRequest.of(Integer.parseInt(pageNum) - 1, Page.pageSize));
​
            //10: 执行查询, 返回结果对象
            AggregatedPage<SkuInfo> aggregatedPage = esTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() {
                @Override
                public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
​
                    List<T> list = new ArrayList<>();
​
                    SearchHits hits = searchResponse.getHits();
                    if (null != hits) {
                        for (SearchHit hit : hits) {
                            SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);
​
                            
                            list.add((T) skuInfo);
                        }
                    }
                    return new AggregatedPageImpl<T>(list, pageable, hits.getTotalHits(), searchResponse.getAggregations());
                }
            });
​
            //11. 总条数
            resultMap.put("total", aggregatedPage.getTotalElements());
            //12. 总页数
            resultMap.put("totalPages", aggregatedPage.getTotalPages());
            //13. 查询结果集合
            resultMap.put("rows", aggregatedPage.getContent());
​
            //14. 获取品牌聚合结果
            StringTerms brandTerms = (StringTerms) aggregatedPage.getAggregation(skuBrand);
            List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
            resultMap.put("brandList", brandList);
​
            //15. 获取规格聚合结果
            StringTerms specTerms = (StringTerms) aggregatedPage.getAggregation(skuSpec);
            List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
            resultMap.put("specList", specList(specList));
​
            //16. 返回当前页
            resultMap.put("pageNum", pageNum);
​
            return resultMap;
        }
​
        return null;
    }

3测试

使用postman测试分页:

二、 搜索排序

1 排序分析

排序这里总共有根据价格排序、根据评价排序、根据新品排序、根据销量排序,排序要想实现只需要告知排序的域以及排序方式即可实现。

价格排序:只需要根据价格高低排序即可,降序价格高->低,升序价格低->高

评价排序:评价分为好评、中评、差评,可以在数据库中设计3个列,用来记录好评、中评、差评的量,每次排序的时候,好评的比例来排序,当然还要有条数限制,评价条数需要超过N条。

新品排序:直接根据商品的发布时间或者更新时间排序。

销量排序:销量排序除了销售数量外,还应该要有时间段限制。

2 排序代码实现

这里我们不单独针对某个功能实现排序,我们只需要在后台接收2个参数,分别是排序域名字和排序方式,代码如下:

java 复制代码
   @Override
    public Map search(Map<String, String> searchMap) throws Exception {
        Map<String, Object> resultMap = new HashMap<>();
​
        //有条件才查询Es
        if (null != searchMap) {
            //组合条件对象
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            //0:关键词
            if (!StringUtils.isEmpty(searchMap.get("keywords"))) {
                boolQuery.must(QueryBuilders.matchQuery("name", searchMap.get("keywords")).operator(Operator.AND));
            }
            //1:条件 品牌
            if (!StringUtils.isEmpty(searchMap.get("brand"))) {
                boolQuery.filter(QueryBuilders.termQuery("brandName", searchMap.get("brand")));
            }
​
            //2:条件 规格
            for (String key : searchMap.keySet()) {
                if (key.startsWith("spec_")) {
                    String value = searchMap.get(key).replace("%2B", "+");
                    boolQuery.filter(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword",value));
                }
            }
            //3:条件 价格
            if (!StringUtils.isEmpty(searchMap.get("price"))) {
                String[] p = searchMap.get("price").split("-");
                boolQuery.filter(QueryBuilders.rangeQuery("price").gte(p[0]));
                if (p.length == 2) {
                    boolQuery.filter(QueryBuilders.rangeQuery("price").lte(p[1]));
                }
            }
​
            //4. 原生搜索实现类
            NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
            nativeSearchQueryBuilder.withQuery(boolQuery);
​
            //6. 品牌聚合(分组)查询
            String skuBrand = "skuBrand";
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuBrand).field("brandName"));
​
            //7. 规格聚合(分组)查询
            String skuSpec = "skuSpec";
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuSpec).field("spec.keyword"));
​
            //8: 排序
            if (!StringUtils.isEmpty(searchMap.get("sortField"))) {
                if ("ASC".equals(searchMap.get("sortRule"))) {
                    nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(searchMap.get("sortField")).order(SortOrder.ASC));
                } else {
​
                    nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(searchMap.get("sortField")).order(SortOrder.DESC));
                }
​
            }
​
            String pageNum = searchMap.get("pageNum");
            if (null == pageNum) {
                pageNum = "1";
            }
            //9: 分页
            nativeSearchQueryBuilder.withPageable(PageRequest.of(Integer.parseInt(pageNum) - 1, Page.pageSize));
​
            //10: 执行查询, 返回结果对象
            AggregatedPage<SkuInfo> aggregatedPage = esTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() {
                @Override
                public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
​
                    List<T> list = new ArrayList<>();
​
                    SearchHits hits = searchResponse.getHits();
                    if (null != hits) {
                        for (SearchHit hit : hits) {
                            SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);
                            
                            list.add((T) skuInfo);
                        }
                    }
                    return new AggregatedPageImpl<T>(list, pageable, hits.getTotalHits(), searchResponse.getAggregations());
                }
            });
​
            //11. 总条数
            resultMap.put("total", aggregatedPage.getTotalElements());
            //12. 总页数
            resultMap.put("totalPages", aggregatedPage.getTotalPages());
            //13. 查询结果集合
            resultMap.put("rows", aggregatedPage.getContent());
​
            //14. 获取品牌聚合结果
            StringTerms brandTerms = (StringTerms) aggregatedPage.getAggregation(skuBrand);
            List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
            resultMap.put("brandList", brandList);
​
            //15. 获取规格聚合结果
            StringTerms specTerms = (StringTerms) aggregatedPage.getAggregation(skuSpec);
            List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
            resultMap.put("specList", specList(specList));
​
            //16. 返回当前页
            resultMap.put("pageNum", pageNum);
​
            return resultMap;
        }
​
        return null;
    }

3测试

使用postman测试

根据价格降序:

javascript 复制代码
{"keywords":"手机","pageNum":"1","sortRule":"DESC","sortField":"price"}

根据价格升序:

javascript 复制代码
{"keywords":"手机","pageNum":"1","sortRule":"ASC","sortField":"price"}
相关推荐
暖和_白开水2 小时前
数据分析agent(十):loguru日志输出优化 和es 启动流程
java·elasticsearch·数据分析
一次旅行1 天前
【效率秘籍】Shell 函数+别名组合拳,让 80% 的重复命令变 1 个字母
大数据·elasticsearch·搜索引擎
暖和_白开水1 天前
数据分析agent(九):es_client_manager.py 和分词器ik_max_word问题
elasticsearch·数据分析·c#
码农学院1 天前
Elasticsearch构建汽车零配件智能搜索与匹配系统
大数据·elasticsearch·汽车
Elastic 中国社区官方博客2 天前
不到 5 分钟完成本地部署:Jina embedding 模型现已支持本地部署
大数据·人工智能·elasticsearch·搜索引擎·embedding·jina
二进制流水搬运工2 天前
入职第一天拉了23个仓库,我写了个脚本解放双手
大数据·elasticsearch·搜索引擎
Elasticsearch2 天前
在不到一小时内将 Datadog Kubernetes 仪表板迁移到 Elastic Observability
elasticsearch
.柒宇.2 天前
Elasticsearch 核心概念与系统架构详解
elasticsearch·系统架构
风行無痕2 天前
单机Elasticsearch 8.19.18安全升级到9.4.3的过程
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客2 天前
使用重新设计的 AutoOps 更快地进行 Elasticsearch 问题排查
大数据·运维·前端·人工智能·elasticsearch·搜索引擎·全文检索