目 录CONTENT

文章目录

海量数据搜索实现

zhouzz
2024-09-29 / 0 评论 / 0 点赞 / 7 阅读 / 33075 字
温馨提示:
本文最后更新于 2024-09-29,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

1、商品搜索流程

商品搜索,包含了数据实时同步和搜索过程,这2个过程必须清楚才能做出更符合工作需要的搜索。

20240929172312.png

1)数据同步流程如下:

  • 1、商家通过程序将Sku添加到数据库
  • 2、后台通过 MQ 或者 Canal 实现实时将数据同步到ES搜索引擎
  • 3、用户可以在前台使用不同条件进行搜索,关键词、分类、价格区间、动态属性

​ 2)搜索引擎流程实现

@Data
public class SkuEs {

    @Id
    private String id;
    private String name;
    private Integer price;
    private Integer num;
    private String image;
    private String images;
    private Date createTime;
    private Date updateTime;
    private String spuId;
    private Integer categoryId;
    //Keyword:不分词
    @Field(type=FieldType.Keyword)
    private String categoryName;
    private Integer brandId;
    @Field(type=FieldType.Keyword)
    private String brandName;
    private String skuAttribute;
    private Integer status;

    //属性映射
    private Map<String,String> attrMap;
}

2、商品关键词搜索功能实现

2.1 关键字查询

用户输入关键词keywords后,将keywords关键词一起传入后台,需要根据商品名字进行搜索。以后也有可能根据别的条件查询,所以传入后台的数据可以用Map接收,响应页面的数据包含列表、分页等信息,可以用Map封装。

/***
 * 商品搜索
 * @param searchMap
 * @return
 */
@Override
public Map<String, Object> search(Map<String, Object> searchMap) {
    //条件封装
    NativeSearchQueryBuilder queryBuilder = queryBuilder(searchMap);
    //执行搜索
    Page<SkuEs> result = skuSearchMapper.search(queryBuilder.build());
    //结果集
    List<SkuEs> list = result.getContent();
    //中记录数
    long totalElements = result.getTotalElements();

    Map<String,Object> resultMap = new HashMap<String,Object>();
    resultMap.put("list",list);
    resultMap.put("totalElements",totalElements);
    return resultMap;
}

/***
 * 搜索条件组装
 */
public NativeSearchQueryBuilder queryBuilder(Map<String,Object> searchMap){
    //QueryBuilder构建
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    //条件判断
    if(searchMap!=null && searchMap.size()>0){
        //关键词
        Object keywords =searchMap.get("keywords");
        if(!StringUtils.isEmpty(keywords)){
            queryBuilder.withQuery(QueryBuilders.termQuery("name",keywords.toString()));
        }
    }
    return queryBuilder;
}

2.2 商品条件回显分析

我们每次执行搜索的时候,页面会显示不同搜索条件,例如:品牌、分类、属性,这些搜索条件都不是固定的,其实他们是没执行搜索的时候,符合搜索条件的商品所有品牌和所有分类,以及所有属性,把他们查询出来,然后页面显示。但是这些条件都没有重复的,也就是说要去重,去重一般采用分组查询即可,所以我们要想动态获取这样的搜索条件,我们需要在后台进行分组查询。

20240929213039.png

比如返回的结果如下,之后按照分类,品牌,动态属性列表在前端进行展示:

{
    "code": 200,
    "message": "操作成功",
    "data": {
        "categoryList": [
            "家电",
            "儿童",
            "图书"
        ],
        "brandList": [
            "艾尔利",
            "华为",
            "OPPO"
        ],
        "attrMap": {
            "网络": [
                "电信5G",
                "电信4G",
                "移动4G",
                "联通4G"
            ],
            "手机屏尺寸": [
                "5.0-5.9英寸",
                "6.0-6.2英寸"
            ],
            "像素": [
                "3000万以上",
                "800-3000万",
                "1200-1599万",
                "1600万以上"
            ],
            "价格范围": [
                "0-500元",
                "500-1000元",
                "1000-1500元",
                "1500-2000元",
                "2000-3000元",
                "3000元以上"
            ]
        },
        "list": [
            {
                "brandId": 11,
                "brandName": "华为",
                "categoryId": 11159,
                "categoryName": "软件研发",
                "createTime": 1603184062000,
                "id": "1318596430398562305",
                "image": "...",
                "images": "...",
                "name": "华为Mate40 Pro 128G",
                "num": 2,
                "price": 3,
                "spuId": "1318596430293704706",
                "status": 1,
                "updateTime": 1603184062000
            }
        ],
        "totalElements": 3
    }
}

2.3 品牌、分类条件查询

/***
 * 分组查询
 */
public void group(NativeSearchQueryBuilder queryBuilder,Map<String, Object> searchMap){
    //用户如果没有输入分类条件,则需要将分类搜索出来,作为条件提供给用户
    if(StringUtils.isEmpty(searchMap.get("category"))){
        queryBuilder.addAggregation(
                AggregationBuilders
                        .terms("categoryList")//别名,类似Map的key
                        .field("categoryName")//根据categoryName域进行分组
                        .size(100)      //分组结果显示100个
        );
    }
    //用户如果没有输入品牌条件,则需要将品牌搜索出来,作为条件提供给用户
    if(StringUtils.isEmpty(searchMap.get("brand"))){
        queryBuilder.addAggregation(
                AggregationBuilders
                        .terms("brandList")//别名,类似Map的key
                        .field("brandName")//根据brandName域进行分组
                        .size(100)      //分组结果显示100个
        );
    }
    //属性分组查询
    queryBuilder.addAggregation(
            AggregationBuilders
                    .terms("attrMap")//别名,类似Map的key
                    .field("skuAttribute")//根据skuAttribute域进行分组
                    .size(100000)      //分组结果显示100000个
    );
}

2.4 品牌、分类分组结果解析

/***
 * 分组结果解析
 */
public void parseGroup(Aggregations aggregations,Map<String,Object> resultMap){
    if(aggregations!=null){
        for (Aggregation aggregation : aggregations) {
            //强转ParsedStringTerms
            ParsedStringTerms terms = (ParsedStringTerms) aggregation;

            //循环结果集对象
            List<String> values = new ArrayList<String>();
            for (Terms.Bucket bucket : terms.getBuckets()) {
                values.add(bucket.getKeyAsString());
            }
            //名字
            String key = aggregation.getName();
            resultMap.put(key,values);
        }
    }
}

/****
 * 将属性信息合并成Map对象
 */
public void attrParse(Map<String,Object> searchMap){
    //先获取attrmaps
    Object attrmaps = searchMap.get("attrMap");
    if(attrmaps!=null){
        //集合数据
        List<String> groupList= (List<String>) attrmaps;
        //定义一个集合Map<String,Set<String>>,存储所有汇总数据
        Map<String,Set<String>> allMaps = new HashMap<String,Set<String>>();
        //循环集合
        for (String attr : groupList) {
            Map<String,String> attrMap = JSON.parseObject(attr,Map.class);
            for (Map.Entry<String, String> entry : attrMap.entrySet()) {
                //获取每条记录,将记录转成Map   就业薪资    学习费用
                String key = entry.getKey();
                Set<String> values = allMaps.get(key);
                if(values==null){
                    values = new HashSet<String>();
                }
                values.add(entry.getValue());
                //覆盖之前的数据
                allMaps.put(key,values);
            }
        }
        //覆盖之前的attrmaps
        searchMap.put("attrMap",allMaps);
    }
}

2.5 调用实现分组查询

/****
 * 关键词搜索
 * @param searchMap
 *              关键词:keywords->name
 * @return
 */
@Override
public Map<String, Object> search(Map<String, Object> searchMap) {
    //QueryBuilder->构建搜索条件
    NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap);
    //分组搜索调用
    group(queryBuilder,searchMap);
    //1.设置高亮信息   关键词前(后)面的标签、设置高亮域
    HighlightBuilder.Field field = new HighlightBuilder
            .Field("name")  //根据指定的域进行高亮查询
            .preTags("<span style=\"color:red;\">")     //关键词高亮前缀
            .postTags("</span>")   //高亮关键词后缀
            .fragmentSize(100);     //碎片长度
    queryBuilder.withHighlightFields(field);
    //2.将非高亮数据替换成高亮数据
    AggregatedPage<SkuEs> page = elasticsearchRestTemplate.queryForPage(queryBuilder.build(), SkuEs.class,null);
    //获取结果集:集合列表、总记录数
    Map<String,Object> resultMap = new HashMap<String,Object>();
    //分组数据解析
    parseGroup(page.getAggregations(),resultMap);
    //动态属性解析
    attrParse(resultMap);
    List<SkuEs> list = page.getContent();
    resultMap.put("list",list);
    resultMap.put("totalElements",page.getTotalElements());
    return resultMap;
}

3.商品搜索条件筛选

3.1 条件搜索分析

20240929224054.png

用户在前端执行条件搜索的时候,有可能会选择分类、品牌、价格、属性,每次选择条件传入后台,后台按照指定参数进行条件查询,我们这里约定一个传参数的规则:

1、分类参数:category
2、品牌参数:brand
3、价格参数:price
4、属性参数:attr_属性名:属性值
5、分页参数:page

3.2 分类/品牌/价格查询

/***
 * 搜索条件组装
 */
public NativeSearchQueryBuilder queryBuilder(Map<String,Object> searchMap){
    //QueryBuilder构建
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

    //多条件组合查询对象
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

    //条件判断
    if(searchMap!=null && searchMap.size()>0){
        //关键词
        Object keywords =searchMap.get("keywords");
        if(!StringUtils.isEmpty(keywords)){
            boolQuery.must(QueryBuilders.termQuery("name",keywords.toString()));
        }

        //分类
        Object category =searchMap.get("category");
        if(!StringUtils.isEmpty(category)){
            boolQuery.must(QueryBuilders.termQuery("categoryName",category.toString()));
        }

        //品牌
        Object brand =searchMap.get("brand");
        if(!StringUtils.isEmpty(brand)){
            boolQuery.must(QueryBuilders.termQuery("brandName",brand.toString()));
        }

        //价格
        Object price =searchMap.get("price");
        if(!StringUtils.isEmpty(price)){
            //去掉元和以上,并按-分割
            String[] prices = price.toString()
                    .replace("元","")
                    .replace("以上","")
                    .split("-");
            //price>x
            boolQuery.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0])));
            //price<=y
            if(prices.length==2){
                boolQuery.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1])));
            }
        }
    }
    return queryBuilder.withQuery(boolQuery);
}

3.3 分页查询

编写分页实现,先获取当前页,再在SkuSearchServiceImpl#queryBuilder调用该方法。

1)获取分页方法

/***
 * 当前页
 */
public int currentPage(Map<String,Object> searchMap){
    try {
        Object currentPage = searchMap.get("page");
        return Integer.valueOf(currentPage.toString())> 0 ? Integer.valueOf(currentPage.toString()) -1 : 0;
    } catch (NumberFormatException e) {
        return 0;
    }
}

2)分页调用

//分页查询
builder.withPageable(PageRequest.of(currentPage(searchMap),10));

3.4 属性查询

属性查询,每次传到后台的参数都是以attr_开始,我们可以遍历传过来的参数searchMap,判断是否是以attr_开始的参数,如果是,则查询属性。

SkuSearchServiceImpl#queryBuilder中添加如下代码 即可:

上图代码如下:

//属性查询
for (Map.Entry<String, Object> entry : searchMap.entrySet()) {
    if(entry.getKey().startsWith("attr_")){
        //获取结果
        String key = entry.getKey().replaceFirst("attr_","");
        String value = entry.getValue().toString();
        //执行查询
        boolQuery.must(QueryBuilders.termQuery("attrMap."+key+".keyword",value));
    }
}

4、商品搜索排序实现

4.1 排序搜索分析

排序搜索有多种排序方式,我们可以把排序升序、降序当做一个参数,把排序的域当做一个参数,无论是哪种排序方式,只需要把这两个参数传到后端服务即可。

排序规则有 销量、新品、评价、价格,但是这里排序字段只有单个一种排序,不会出现销量升序和价格升序这种组合排序。

我们定义一下传参数规则:

1、排序域:sfield
2、排序方式:sm

例如:根据价格升序

sfield=price
sm=ASC

例如:新品排序

sfield=updateTime
sm=DESC

4.2 排序实现

SkuSearchServiceImpl#queryBuilder中添加如下daima 即可:

//排序
Object sfield = searchMap.get("sfield");
Object sm = searchMap.get("sm");
if(!StringUtils.isEmpty(sfield) && !StringUtils.isEmpty(sm)){
    queryBuilder.withSort(
            SortBuilders.fieldSort(sfield.toString())   //排序域
            .order(SortOrder.valueOf(sm.toString())));  //排序方式
}

5、商品搜索高亮实现

5.1 高亮原理分析

高亮是指搜索商品的时候,商品列表中如何和你搜索的关键词相同,那么它会高亮展示,也就是变色展示,如下京东搜索,其实就是给关键词增加了样式,所以是红色,ES搜索引擎也是一样,也可以实现关键词高亮展示,原理和京东搜索高亮原理一样。

5.2 高亮实现

高亮搜索实现有2个步骤:

1、配置高亮域以及对应的样式
2、从结果集中取出高亮数据,并将非高亮数据换成高亮数据

1)高亮配置

SkuSearchServiceImpl#search中添加如下高亮代码:

//高亮配置
HighlightBuilder.Field field = new HighlightBuilder.
        Field("name").                      //指定的高亮域
        preTags("<span style=\"color:red\">").   //前缀
        postTags("</span>").                      //后缀
        fragmentSize(100);
//添加高亮域
queryBuilder.withHighlightFields(field);

2)结果映射转换

创建一个结果映射转换对象com.gupaoedu.vip.mall.search.es.HighlightResultMapper,其实主要是将非高亮转换成高亮数据,代码如下:

public class HighlightResultMapper extends DefaultResultMapper{
    /***
     * 处理结果集
     */
    @Override
    public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
        //所有数据
        for (SearchHit hit : response.getHits()) {
            //当前单条数据
            Map<String, Object> sourceMap = hit.getSourceAsMap();
            //高亮数据
            for (Map.Entry<String, HighlightField> entry : hit.getHighlightFields().entrySet()) {
                String key = entry.getKey();
                if (sourceMap.containsKey(key)) {
                    Text[] fragments = entry.getValue().getFragments();
                    sourceMap.put(key, transTextArrayToString(fragments));
                }
            }
            hit.sourceRef(new ByteBufferReference(ByteBuffer.wrap(JSONObject.toJSONString(sourceMap).getBytes())));
        }
        return super.mapResults(response, clazz, pageable);
    }

    /***
     * 拼接数据碎片
     */
    private String transTextArrayToString(Text[] fragments) {
        if (null == fragments) {
            return "";
        }
        StringBuffer buffer = new StringBuffer();
        for (Text fragment : fragments) {
            buffer.append(fragment.string());
        }
        return buffer.toString();
    }
}

6.小结

0

评论区