跳至主要內容

云商城-海量数据搜索实现

soulballad实践项目SpringCloudAlibaba 云商城SpringCloudAlibaba约 4315 字大约 14 分钟

第4章 海量数据搜索实现

课程目标

1、商品搜索流程讲解

​ 1)搜索引擎技术分析

​ 2)搜索引擎流程实现

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

​ 1)工程搭建

​ 2)索引实时导入

​ 3)关键词搜索

3、商品统计条件搜索

​ 1)分类搜索实现

​ 2)品牌搜索实现

4、商品搜索条件筛选实现

​ 1)属性动态域分析

​ 2)属性动态域搜索

​ 3)价格区间搜索

5、商品搜索排序实现

​ 1)排序分析

​ 2)多种排序实现

6、商品搜索高亮实现

​ 1)高亮展示分析

​ 2)高亮搜索实现

环境说明:

1、es-head已装好,端口9100
2、ElasticSearch已装好
3、安装文档:参考资料->ES安装文档

1 商品搜索环境搭建

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

1.1 搜索分析

1603956077263

数据同步流程如下:

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

1603957982137

1.2 工程搭建

1.2.1 api工程搭建

1)工程搭建

创建搜索工程的api工程mall-search-api,坐标如下:

<groupId>com.gupaoedu.vip.mall</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>mall-search-api</artifactId>

pom.xml如下:

<?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>mall-api</artifactId>
        <groupId>com.gupaoedu.vip.mall</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>mall-search-api</artifactId>

    <dependencies>
        <!--ElasticSearch-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    </dependencies>
</project>

2)映射配置

1603959489085

我们需要将数据库数据查询出来,再存入ES中,但中间需要有一个和ES索引库对应的JavaBean,为了不影响原来程序对象,所以会创建一个新的JavaBean对象com.gupaoedu.vip.mall.search.model.SkuEs,代码如下:

@Data
@Document(indexName = "shopsearch",type = "skues")
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;
}

1.2.2 搜索工程搭建

搭建搜索工程,坐标如下:

<groupId>com.gupaoedu.vip.mall</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>mall-search-service</artifactId>

pom.xml:

<?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>mall-service</artifactId>
        <groupId>com.gupaoedu.vip.mall</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>mall-search-service</artifactId>
    <description>
        搜索服务
    </description>

    <dependencies>
        <!--mall-search-api-->
        <dependency>
            <groupId>com.gupaoedu.vip.mall</groupId>
            <artifactId>mall-search-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

bootstrap.yml:

server:
  port: 8084
spring:
  application:
    name: mall-search
  cloud:
    nacos:
      config:
        file-extension: yaml
        server-addr: 192.168.100.130:8848
      discovery:
        #Nacos的注册地址
        server-addr: 192.168.100.130:8848
  #Elasticsearch服务配置
  elasticsearch:
    rest:
      uris: http://192.168.100.130:9200
#日志配置
logging:
  pattern:
    console: "%msg%n"

启动类:com.gupaoedu.vip.mall.MallSearchApplication

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MallSearchApplication {

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

1.3 索引实时更新

当数据库中sku数据发生变更的时候,我们可以通过Canal微服务调用当前搜索微服务实现数据实时更新。

1.3.1 更新操作实现

1)Mapper

创建Dao接口需要继承ElasticsearchRepository<T,ID>接口,同时需要在启动类中添加扫描注解。

创建com.gupaoedu.vip.mall.search.mapper.SkuSearchMapper:

public interface SkuSearchMapper extends ElasticsearchRepository<SkuEs,String> {
}

在启动类上添加EnableElasticsearchRepositories注解:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableElasticsearchRepositories(basePackages = {"com.gupaoedu.vip.mall.search.mapper"})
public class MallSearchApplication {

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

2)Service

创建接口:com.gupaoedu.vip.mall.search.service.SkuSearchService,创建添加和删除方法:

public interface SkuSearchService {
    //添加索引
    void add(SkuEs skuEs);
    //删除索引
    void del(String id);
}

创建实现类:com.gupaoedu.vip.mall.search.service.impl.SkuSearchServiceImpl

@Service
public class SkuSearchServiceImpl implements SkuSearchService {

    @Autowired
    private SkuSearchMapper skuSearchMapper;

    /***
     * 单个导入ES
     * @param skuEs
     */
    @Override
    public void add(SkuEs skuEs) {
        //属性转换
        String skuAttribute = skuEs.getSkuAttribute();
        if(!StringUtils.isEmpty(skuAttribute)){
            skuEs.setAttrMap(JSON.parseObject(skuAttribute,Map.class));
        }
        skuSearchMapper.save(skuEs);
    }

    /***
     * 根据ID删除
     * @param id
     */
    @Override
    public void del(String id) {
        skuSearchMapper.deleteById(id);
    }
}

3)Controller

创建com.gupaoedu.vip.mall.search.controller.SkuSearchController

@RestController
@RequestMapping(value = "/search")
public class SkuSearchController {

    @Autowired
    private SkuSearchService skuSearchService;

    /****
     * 单个商品导入
     */
    @PostMapping(value = "/add")
    public RespResult add(@RequestBody SkuEs skuEs){
        skuSearchService.add(skuEs);
        return RespResult.ok();
    }

    /***
     * 根据ID删除
     * @param id
     * @return
     */
    @DeleteMapping(value = "/del/{id}")
    public RespResult del(@PathVariable("id")String id){
        skuSearchService.del(id);
        return RespResult.ok();
    }
}

4)Feign

mall-search-api中创建com.gupaoedu.vip.mall.search.feign.SkuSearchFeign

@FeignClient(value = "mall-search")
public interface SkuSearchFeign {

    /****
     * 单个导入
     * @param skuEs
     * @return
     */
    @PostMapping(value = "/search/add")
    RespResult add(@RequestBody SkuEs skuEs);

    /***
     * 根据ID删除
     * @param id
     * @return
     */
    @DeleteMapping(value = "/search/del/{id}")
    RespResult del(@PathVariable("id")String id);
}

1.3.2 实时更新实现

1.3.2.1 映射配置

实时更新,我们需要在Canal微服务中调用上面2个接口,首先我们要先将数据库中通过Canal推送的数据转成指定的JavaBean,需要用到JPA注解配置映射。

mall-api中引入JPA:

<!--JPA-->
<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>persistence-api</artifactId>
    <version>1.0</version>
    <scope>compile</scope>
</dependency>

mall-goods-api中的Sku中配置映射如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "sku")
@Table
public class Sku implements Serializable{

    @TableId(type = IdType.ASSIGN_ID)
    private String id;
    private String name;
    private Integer price;
    private Integer num;
    private String image;
    private String images;
    @Column(name = "create_time")
    private Date createTime;
    @Column(name = "update_time")
    private Date updateTime;
    @Column(name = "spu_id")
    private String spuId;
    @Column(name = "category_id")
    private Integer categoryId;
    @Column(name = "category_name")
    private String categoryName;
    @Column(name = "brand_id")
    private Integer brandId;
    @Column(name = "brand_name")
    private String brandName;
    @Column(name = "sku_attribute")
    private String skuAttribute;
    private Integer status;
}
1.3.2.2 监听更新

监听更新需要调用search的接口,所以需要引入依赖包,在mall-canal-service添加如下依赖:

<!--searchapi-->
<dependency>
    <groupId>com.gupaoedu.vip.mall</groupId>
    <artifactId>mall-search-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

mall-canal-service的启动类上添加Feign注解@EnableFeignClients配置:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients(basePackages = {"com.gupaoedu.vip.mall.goods.feign","com.gupaoedu.vip.mall.search.feign"})
public class MallCanalApplication {

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

mall-canal-service中创建com.gupaoedu.vip.canal.listener.SkuHandler

@CanalTable(value = "sku")
@Component
public class SkuHandler implements EntryHandler<Sku>{

    @Autowired
    private SkuSearchFeign skuSearchFeign;

    /***
     * 增加产品
     * @param sku
     */
    @Override
    public void insert(Sku sku) {
        if(sku.getStatus().intValue()==1){
            //导入索引
            skuSearchFeign.add(JSON.parseObject(JSON.toJSONString(sku), SkuEs.class));
        }
    }

    /***
     * 修改
     * @param before
     * @param after
     */
    @Override
    public void update(Sku before, Sku after) {
        if(after.getStatus().intValue()==2){
            //导入索引
            skuSearchFeign.del(after.getId());
        }else{
            skuSearchFeign.add(JSON.parseObject(JSON.toJSONString(after), SkuEs.class));
        }
    }

    /***
     *
     * @param sku
     */
    @Override
    public void delete(Sku sku) {
        skuSearchFeign.del(sku.getId());
    }
}

修改数据库数据,同时打开http://192.168.100.130:9100/,效果如下:

1603961500034

2 商品关键词搜索

2.1 搜索分析

1603964027575

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

2.2 搜索实现

1)Service

修改com.gupaoedu.vip.mall.search.service.SkuSearchService添加搜索方法:

/***
 * 商品搜索
 * @param searchMap
 * @return
 */
Map<String,Object> search(Map<String, Object> searchMap);

修改com.gupaoedu.vip.mall.search.service.impl.SkuSearchServiceImpl添加搜索实现:

/***
 * 商品搜索
 * @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)Controller

修改com.gupaoedu.vip.mall.search.controller.SkuSearchController添加搜控制方法:

/****
 * 商品搜索
 */
@GetMapping
public RespResult<Map<String,Object>> search(@RequestParam Map<String,Object> searchMap){
    Map<String,Object> result = skuSearchService.search(searchMap);
    return RespResult.ok(result);
}

测试结果如下:http://localhost:8084/search?keywords=华为

{
    "code": 20000,
    "data": {
        "list": [
            {
                "attrMap": {
                    "学习费用": "2万",
                    "就业薪资": "10K起"
                },
                "brandId": 11,
                "brandName": "华为",
                "categoryId": 11159,
                "categoryName": "软件研发",
                "createTime": 1603184062000,
                "id": "1318596430398562305",
                "image": "...",
                "images": "...",
                "name": "华为Mate40 Pro 128G",
                "num": 2,
                "price": 3,
                "skuAttribute": "{\"就业薪资\":\"10K起\",\"学习费用\":\"2万\"}",
                "spuId": "1318596430293704706",
                "status": 1,
                "updateTime": 1603184062000
            }
        ],
        "totalElements": 3
    },
    "message": "操作成功"
}

3 商品搜索条件回显

3.1 条件回显分析

1603977400312

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

3.2 品牌、分类条件查询

1)分组设置

com.gupaoedu.vip.mall.search.service.impl.SkuSearchServiceImpl中添加方法

/***
 * 分组搜索
 */
public void group(NativeSearchQueryBuilder builder,Map<String,Object> searchMap){
    //前端没有传入分类参数的时候查询分类集合作为搜索条件
    if(StringUtils.isEmpty(searchMap.get("category"))){
        //分类分组
        builder.addAggregation(AggregationBuilders
                .terms("categoryList")  //查询的数据对应别名
                .field("categoryName")         //根据categoryName分组
                .size(100));                    //分组查询100条
    }
    //前端没有传入品牌参数的时候查询品牌集合作为搜索条件
    if(StringUtils.isEmpty(searchMap.get("brand"))){
        //品牌分组
        builder.addAggregation(AggregationBuilders
                .terms("brandList")     //查询的数据对应别名
                .field("brandName")            //根据brandName分组
                .size(100));                    //分组查询100条
    }
}

2)分组结果解析

com.gupaoedu.vip.mall.search.service.impl.SkuSearchServiceImpl中添加方法

/****
 * 取出所有分组数据
 * @param aggregations
 * @return
 */
public Map<String,Object> parseGroup(Aggregations aggregations) {
    //所有分组数据
    Map<String,Object> groups = new HashMap<String,Object>();
    for (Aggregation aggregation : aggregations) {
        //强转成ParsedStringTerms
        ParsedStringTerms pst= (ParsedStringTerms) aggregation;

        //定义一个集合存储
        List<String> values = new ArrayList<String>();
        for (Terms.Bucket bucket : pst.getBuckets()) {
            //单个对象值
            values.add(bucket.getKeyAsString());
        }
        //存入到Map中
        groups.put(pst.getName(),values);
    }
    return groups;
}

3)调用实现分组查询

com.gupaoedu.vip.mall.search.service.impl.SkuSearchServiceImplsearch方法中添加上面2个方法的调用:

1603979180556

请求测试:http://localhost:8084/search,结果如下:

{
    "code": 20000,
    "data": {
        "categoryList": [
            "软件研发",
            "手机"
        ],
        "brandList": [
            "华为",
            "中兴"
        ],
        "list": [
            {
                "attrMap": {
                    "学习费用": "2万",
                    "就业薪资": "10K起"
                },
                "brandId": 11,
                "brandName": "华为",
                "categoryId": 11159,
                "categoryName": "软件研发",
                "createTime": 1603183717000,
                "id": "1318594982227025922",
                "image": "...",
                "images": "...",
                "name": "华为Mate40 Pro 32G",
                "num": 44,
                "price": 3,
                "skuAttribute": "{\"就业薪资\":\"10K起\",\"学习费用\":\"2万\"}",
                "spuId": "1318594982147334146",
                "status": 1,
                "updateTime": 1603183717000
            }
        ],
        "totalElements": 11
    },
    "message": "操作成功"
}

3.3 属性回显

3.3.1 属性条件分析

1603979420789

属性条件其实就是当前搜索的所有商品属性信息,所以我们可以把所有属性信息全部查询出来,然后把属性名作为key,属性值用集合存起来,就是我们页面要的属性条件了。

3.3.2 属性条件查询

1)属性分组查询

修改com.gupaoedu.vip.mall.search.service.impl.SkuSearchServiceImplgroup方法,添加分组查询,代码如下:

1603980460734

上图代码如下:

//属性分组查询
builder.addAggregation(AggregationBuilders
        .terms("attrmaps")      //查询的数据对应别名
        .field("skuAttribute")         //根据brandName分组
        .size(100000));                 //分组查询100000条

2)属性数据分析

1603982830066

上图代码的思路如下:

1、将所有属性信息传过来,并取出存到groupList中
2、循环groupList
3、把所有相同属性名的结果集合收集到一起
4、收集完成后,替换原来groupMap中的属性数据

完整代码如下:

/****
 * 属性解析
 */
public void attrParse(Map<String,Object> groupMap){
    //获取属性分组数据
    Object attrgroup = groupMap.get("attrmaps");
    if(attrgroup!=null){
        //强转成List
        List<String> groupList = (List<String>) attrgroup;

        //所有属性名拥有的属性集合
        Map<String,Set<String>> allMaps = new HashMap<String,Set<String>>();

        for (String attr : groupList) {
            //将当前attr解析成Map
            Map<String,String> attrMap = JSON.parseObject(attr,Map.class);

            for (Map.Entry<String, String> entry : attrMap.entrySet()) {
                //判断allMaps中是否存在当前attr的属性,不存在则添加一个
                Set<String> values = allMaps.get(entry.getKey());
                if(values==null){
                    values=new HashSet<String>();
                }
                //存在,则取出来,再添加当前值
                values.add(entry.getValue());
                //将修改后的值添加到allMaps中
                allMaps.put(entry.getKey(),values);
            }
        }

        //重置groupMap的值
        groupMap.put("attrmaps",allMaps);
    }
}

3)执行调用

com.gupaoedu.vip.mall.search.service.impl.SkuSearchServiceImpl#search中执行调用,如下代码:

1603983174708

4 商品搜索条件筛选实现

4.1 条件搜索分析

1603984003326

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

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

4.2 条件筛选查询

4.2.1 分类/品牌/价格查询

分别获取category,brand,price的值,并根据这三个只分别实现分类过滤、品牌过滤、价格过滤,其中价格过滤传入的数据以-分割,实现代码如下:

修改com.gupaoedu.vip.mall.search.service.impl.SkuSearchServiceImpl#queryBuilder添加代码如下图红色部分:

1603986415824

完整代码如下:

/***
 * 搜索条件组装
 */
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);
}

4.2.2 分页查询

编写分页实现,先获取当前页,再在com.gupaoedu.vip.mall.search.service.impl.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)分页调用

1603987385962

4.2.3 属性查询

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

com.gupaoedu.vip.mall.search.service.impl.SkuSearchServiceImpl#queryBuilder中添加如下代码 即可:

1603988145907

上图代码如下:

//属性查询
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));
    }
}

5 商品搜索排序实现

1603988397006

5.1 排序搜索分析

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

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

例如:根据价格升序

sfield=price
sm=ASC

例如:新品排序

sfield=updateTime
sm=DESC

5.2 排序实现

com.gupaoedu.vip.mall.search.service.impl.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())));  //排序方式
}

6 商品搜索高亮实现

6.1 高亮原理分析

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

1603991251079

6.2 高亮实现

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

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

1)高亮配置

com.gupaoedu.vip.mall.search.service.impl.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();
    }
}

注入对象ElasticsearchRestTemplate

@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;

将之前的搜索换成用elasticsearchRestTemplate来实现搜索:

1603991647876

测试关键词搜索,效果如下:

{
    "code": 20000,
    "data": {
        "attrmaps": {
            "学习费用": [
                "2万"
            ],
            "就业薪资": [
                "10K起"
            ]
        },
        "categoryList": [
            "软件研发"
        ],
        "brandList": [
            "华为"
        ],
        "list": [
            {
                "attrMap": {
                    "学习费用": "2万",
                    "就业薪资": "10K起"
                },
                "brandId": 11,
                "brandName": "华为",
                "categoryId": 11159,
                "categoryName": "软件研发",
                "createTime": 1603184062000,
                "id": "1318596430398562305",
                "image": "....",
                "images": "....",
                "name": "<span style=\"color:red\">华为</span>Mate40 Pro 128G",
                "num": 122,
                "price": 3,
                "skuAttribute": "{\"就业薪资\":\"10K起\",\"学习费用\":\"2万\"}",
                "spuId": "1318596430293704706",
                "status": 1,
                "updateTime": 1603184062000
            },
            {
                "attrMap": {
                    "学习费用": "2万",
                    "就业薪资": "10K起"
                },
                "brandId": 11,
                "brandName": "华为",
                "categoryId": 11159,
                "categoryName": "软件研发",
                "createTime": 1603183717000,
                "id": "1318594982227025922",
                "image": "....",
                "images": "....",
                "name": "<span style=\"color:red\">华为</span>Mate40 Pro 32G",
                "num": 122,
                "price": 3,
                "skuAttribute": "{\"就业薪资\":\"10K起\",\"学习费用\":\"2万\"}",
                "spuId": "1318594982147334146",
                "status": 1,
                "updateTime": 1603183717000
            }
        ],
        "totalElements": 3
    },
    "message": "操作成功"
}
上次编辑于:
贡献者: soulballad