第4章 检索模块.md 46 KB

谷粒随享

第4章 专辑检索

学习目标:

​ 1

1、检索业务需求

根据用户输入的检索条件,查询出对用的商品

  • 业务数据:包含专辑ID、专辑名称、专辑封面、主播名称、专辑声音数量、付费类型、分类ID、统计信息、专辑属性等
  • 过滤条件:关键字、分类、专辑标签等
  • 排序方式:热门、时间、播放量

1.1 封装实体类

商品上架,下架信息存在不同的数据库表中,所以我们将商品的上架-下架的字段统一封装到实体类中。

@Data
@Document(indexName = "albuminfo")
@JsonIgnoreProperties(ignoreUnknown = true)//目的:防止json字符串转成实体对象时因未识别字段报错
public class AlbumInfoIndex implements Serializable {

    private static final long serialVersionUID = 1L;

    // 专辑Id
    @Id
    private Long id;

    //  es 中能分词的字段,这个字段数据类型必须是 text!keyword 不分词! analyzer = "ik_max_word"
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String albumTitle;

    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String albumIntro;
    
    //  主播名称
    @Field(type = FieldType.Keyword)
    private String announcerName;

    //专辑封面
    @Field(type = FieldType.Keyword, index = false)
    private String coverUrl;

    //专辑包含声音总数
    @Field(type = FieldType.Long, index = false)
    private Integer includeTrackCount;

    //专辑是否完结:0-否;1-完结
    @Field(type = FieldType.Long, index = false)
    private String isFinished;

    //付费类型:免费、vip免费、付费
    @Field(type = FieldType.Keyword, index = false)
    private String payType;

    @Field(type = FieldType.Date,format = DateFormat.date_time, pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime; //

    @Field(type = FieldType.Long)
    private Long category1Id;

    @Field(type = FieldType.Long)
    private Long category2Id;

    @Field(type = FieldType.Long)
    private Long category3Id;

    //播放量
    @Field(type = FieldType.Integer)
    private Integer playStatNum = 0;

    //订阅量
    @Field(type = FieldType.Integer)
    private Integer subscribeStatNum = 0;

    //购买量
    @Field(type = FieldType.Integer)
    private Integer buyStatNum = 0;

    //评论数
    @Field(type = FieldType.Integer)
    private Integer commentStatNum = 0;

    //商品的热度!
    @Field(type = FieldType.Double)
    private Double hotScore = 0d;

    // 专辑属性值
    // Nested 支持嵌套查询
    @Field(type = FieldType.Nested)
    private List<AttributeValueIndex> attributeValueIndexList;

}

1.2 创建索引库

service-search模块中提供操作索引库持久层接口

package com.atguigu.tingshu.search.repository;

import com.atguigu.tingshu.model.search.AlbumInfoIndex;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface AlbumInfoIndexRepository extends ElasticsearchRepository<AlbumInfoIndex, Long> {
}

启动ServiceSearchApplication当扫描到持久层接口会自动创建索引库

image-20231011095104003

2、专辑上下架

2.1 功能实现分析

  1. 获取专辑信息(有)但是需要提供feign接口
  2. 根据专辑Id获取专辑属性信息(无)
  3. 获取分类信息(无)
  4. 获取用户信息(有)但是需要提供feign接口

2.1.1 查询专辑信息

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/23

service-album 中的AlbumInfoApiController控制器已有实现,只需要在service-album-client模块AlbumInfoFeignClien提供Feign远程调用方法即可。

package com.atguigu.tingshu.album.client;

import com.atguigu.tingshu.album.client.impl.AlbumDegradeFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.album.AlbumInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * <p>
 * 专辑模块远程调用Feign接口
 * </p>
 *
 * @author atguigu
 */
@FeignClient(value = "service-album", path = "/api/album", fallback = AlbumDegradeFeignClient.class)
public interface AlbumFeignClient {


    /**
     * 根据id 获取到专辑信息
     * @param id
     * @return
     */
    @GetMapping("/albumInfo/getAlbumInfo/{id}")
    public Result<AlbumInfo> getAlbumInfoById(@PathVariable Long id);

}

AlbumDegradeFeignClient服务降级类

package com.atguigu.tingshu.album.client.impl;


import com.atguigu.tingshu.album.client.AlbumFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.album.AlbumInfo;
import org.springframework.stereotype.Component;

@Component
public class AlbumDegradeFeignClient implements AlbumFeignClient {


    @Override
    public Result<AlbumInfo> getAlbumInfoById(Long id) {
        return null;
    }
}

2.1.2 获取专辑属性(标签)列表

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/47

service-album 中的AlbumInfoApiController控制器添加方法

/**
 * 根据专辑Id 获取到专辑属性列表
 * @param albumId
 * @return
 */
@Operation(summary = "获取专辑属性值列表")
@GetMapping("/albumInfo/findAlbumAttributeValue/{albumId}")
public Result<List<AlbumAttributeValue>> getAlbumAttributeValueByAlbumId(@PathVariable Long albumId) {
    //	获取到专辑属性集合
    List<AlbumAttributeValue> list = albumInfoService.getAlbumAttributeValueByAlbumId(albumId);
    return Result.ok(list);
}

AlbumInfoService接口

/**
* 根据专辑Id 获取到专辑属性列表
* @param albumId
* @return
*/
List<AlbumAttributeValue> getAlbumAttributeValueByAlbumId(Long albumId)

AlbumInfoServiceImpl实现类

/**
 * 根据专辑Id 获取到专辑属性列表
 *
 * @param albumId
 * @return
 */
@Override
public List<AlbumAttributeValue> getAlbumAttributeValueByAlbumId(Long albumId) {
    LambdaQueryWrapper<AlbumAttributeValue> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(AlbumAttributeValue::getAlbumId, albumId);
    return albumAttributeValueMapper.selectList(queryWrapper);
}

service-album-client模块中AlbumFeignClient新增Feign远程调用方法

/**
 * 根据专辑Id 获取到专辑属性列表
 * @param albumId
 * @return
 */
@GetMapping("/albumInfo/findAlbumAttributeValue/{albumId}")
public Result<List<AlbumAttributeValue>> getAlbumAttributeValueByAlbumId(@PathVariable Long albumId);

AlbumDegradeFeignClient服务降级方法

@Override
public Result<List<AlbumAttributeValue>> getAlbumAttributeValueByAlbumId(Long albumId) {
	return null;
}

2.1.3 查询分类信息

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/49

根据三级分类Id查询分类数据,在BaseCategoryApiController 控制器中添加

/**
 * 根据三级分类Id 获取到分类信息
 * @param category3Id
 * @return
 */
@Operation(summary = "通过三级分类id查询分类信息")
@GetMapping("/category/getCategoryView/{category3Id}")
public Result<BaseCategoryView> getCategoryView(@PathVariable Long category3Id){
    // 调用服务层方法
    BaseCategoryView baseCategoryView = baseCategoryService.getCategoryView(category3Id);
    return Result.ok(baseCategoryView);
}

BaseCategoryService

/**
 * 根据三级分类ID查询分类信息
 * @param category3Id
 * @return
 */
BaseCategoryView getCategoryView(Long category3Id);

BaseCategoryServiceImpl

/**
 * 根据三级分类ID查询分类信息
 * @param category3Id
 * @return
 */
@Override
public BaseCategoryView getCategoryView(Long category3Id) {
    return baseCategoryViewMapper.selectById(category3Id);
}

service-album-client模块中AlbumFeignClient提供Feign远程调用接口及服务降级类

/**
 * 根据三级分类Id 获取到分类信息
 * @param category3Id
 * @return
 */
@GetMapping("/category/getCategoryView/{category3Id}")
public Result<BaseCategoryView> getCategoryView(@PathVariable Long category3Id);

AlbumDegradeFeignClient服务降级类

@Override
public Result<BaseCategoryView> getCategoryView(Long category3Id) {
    return null;
}

2.1.4 获取用户信息

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/51

service-user模块中控制器UserInfoApiController中新增方法

package com.atguigu.tingshu.user.api;

import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "用户管理接口")
@RestController
@RequestMapping("api/user")
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserInfoApiController {

	@Autowired
	private UserInfoService userInfoService;

	/**
	 * 根据用户Id获取用户信息
	 * @param userId
	 * @return
	 */
	@Operation(summary = "根据用户id获取用户信息")
	@GetMapping("/userInfo/getUserInfoVo/{userId}")
	public Result<UserInfoVo> getUserInfoVo(@PathVariable Long userId) {
		// 获取用户信息
		UserInfoVo userInfoVo = userInfoService.getUserInfoVoByUserId(userId);
		return Result.ok(userInfoVo);
	}
	
}

service-user-client模块中UserFeignClientFeign接口中新增远程调用接口与服务降级方法

package com.atguigu.tingshu.user.client;

import com.atguigu.tingshu.user.client.impl.UserDegradeFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * <p>
 * 用户模块远程调用Feign接口
 * </p>
 *
 * @author atguigu
 */
@FeignClient(value = "service-user", path = "/api/user", fallback = UserDegradeFeignClient.class)
public interface UserFeignClient {

    /**
     * 根据用户Id获取用户信息
     *
     * @param userId
     * @return
     */
    @GetMapping("/userInfo/getUserInfoVo/{userId}")
    public Result<UserInfoVo> getUserInfoVo(@PathVariable Long userId);
}

UserDegradeFeignClient服务降级类

package com.atguigu.tingshu.user.client.impl;


import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import org.springframework.stereotype.Component;

@Component
public class UserDegradeFeignClient implements UserFeignClient {

    @Override
    public Result<UserInfoVo> getUserInfoVo(Long userId) {
        return null;
    }
}

2.2 专辑上架

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/53

service-search模块中SearchApiController 控制器

package com.atguigu.tingshu.search.api;

import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.search.service.SearchService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "搜索专辑管理")
@RestController
@RequestMapping("api/search")
@SuppressWarnings({"unchecked", "rawtypes"})
public class SearchApiController {

    @Autowired
    private SearchService searchService;

    /**
     * 上架专辑
     *
     * @param albumId
     * @return
     */
    @Operation(summary = "上架专辑")
    @GetMapping("/albumInfo/upperAlbum/{albumId}")
    public Result upperAlbum(@PathVariable Long albumId) {
        //  调用服务层方法.
        searchService.upperAlbum(albumId);
        //  默认返回
        return Result.ok();
    }
}

SearchService 接口

package com.atguigu.tingshu.search.service;

public interface SearchService {

    /**
     * 上架专辑
     *
     * @param albumId
     */
    void upperAlbum(Long albumId);
}

SearchServiceImpl 实现类

package com.atguigu.tingshu.search.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.model.album.AlbumAttributeValue;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.atguigu.tingshu.model.search.AlbumInfoIndex;
import com.atguigu.tingshu.model.search.AttributeValueIndex;
import com.atguigu.tingshu.search.repository.AlbumInfoIndexRepository;
import com.atguigu.tingshu.search.service.SearchService;
import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;


@Slf4j
@Service
@SuppressWarnings({"all"})
public class SearchServiceImpl implements SearchService {


    @Autowired
    private AlbumInfoIndexRepository albumInfoIndexRepository;

    @Autowired
    private UserFeignClient userFeignClient;

    @Autowired
    private AlbumFeignClient albumFeignClient;

    /**
     * 构建专辑索引库文档对象,新增专辑到索引库
     *
     * @param albumId
     */
    @Override
    public void upperAlbum(Long albumId) {
        AlbumInfoIndex albumInfoIndex = new AlbumInfoIndex();
        //1.远程调用专辑服务获取专辑信息 封装专辑基本信息
        AlbumInfo albumInfo = albumFeignClient.getAlbumInfoById(albumId).getData();
        Assert.notNull(albumInfo, "专辑为空");
        BeanUtil.copyProperties(albumInfo, albumInfoIndex);


        //2.远程调用专辑服务获取专辑属性列表 封装专辑属性列表
        List<AlbumAttributeValue> albumAttributeValueList = albumFeignClient.getAlbumAttributeValueByAlbumId(albumId).getData();
        Assert.notNull(albumAttributeValueList, "专辑标签为空");
        List<AttributeValueIndex> attributeValueIndexList = albumAttributeValueList.stream().map(albumAttributeValue -> {
            AttributeValueIndex attributeValueIndex = BeanUtil.copyProperties(albumAttributeValue, AttributeValueIndex.class);
            return attributeValueIndex;
        }).collect(Collectors.toList());
        albumInfoIndex.setAttributeValueIndexList(attributeValueIndexList);

        //3.远程调用专辑服务获取分类信息
        BaseCategoryView baseCategoryView = albumFeignClient.getCategoryView(albumInfo.getCategory3Id()).getData();
        Assert.notNull(baseCategoryView, "分类信息为空");
        albumInfoIndex.setCategory1Id(baseCategoryView.getCategory1Id());
        albumInfoIndex.setCategory2Id(baseCategoryView.getCategory2Id());
        albumInfoIndex.setCategory3Id(baseCategoryView.getCategory3Id());


        //4远程调用用户服务获取用户信息
        UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(albumInfo.getUserId()).getData();
        Assert.notNull(userInfoVo, "主播信息为空");
        albumInfoIndex.setAnnouncerName(userInfoVo.getNickname());

        //5.设置专辑统计量与得分,随机及方便测试
        int num1 = new Random().nextInt(1000);
        int num2 = new Random().nextInt(100);
        int num3 = new Random().nextInt(50);
        int num4 = new Random().nextInt(300);

        albumInfoIndex.setPlayStatNum(num1);
        albumInfoIndex.setSubscribeStatNum(num2);
        albumInfoIndex.setBuyStatNum(num3);
        albumInfoIndex.setCommentStatNum(num4);
        //  设置热度排名
        double hotScore = num1 * 0.2 + num2 * 0.3 + num3 * 0.4 + num4 * 0.1;
        albumInfoIndex.setHotScore(hotScore);

        //6.保存专辑文档对象到索引库
        albumInfoIndexRepository.save(albumInfoIndex);
    }
}

通过Knife4J接口地址 http://localhost:8502/doc.html 进行测试!

2.3 专辑下架

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/55

SearchApiController控制器

/**
 * 下架专辑
 * @param albumId
 * @return
 */
@Operation(summary = "下架专辑")
@GetMapping("/albumInfo/lowerAlbum/{albumId}")
public Result lowerAlbum(@PathVariable Long albumId) {
    searchService.lowerAlbum(albumId);
    return Result.ok();
}

SearchService接口

/**
 * 下架专辑
 * @param albumId
 */
void lowerAlbum(Long albumId);

SearchServiceImpl实现类

/**
 * 下架专辑
 * @param albumId
 */
@Override
public void lowerAlbum(Long albumId) {
    albumInfoIndexRepository.deleteById(albumId);
}

通过Knife4J接口地址 http://localhost:8502/doc.html 进行测试!

2.4 专辑自动上下架

需求:基于Kafka消息队列实现专辑自动上下架;主播在APP端对自己专辑进行进行上架或者下架除了修改

2.4.1 生产者:专辑服务

service-album模块中改造:AlbumInfoServiceImpl中保存专辑的saveAlbumInfo方法

@Autowired
private KafkaService kafkaService;


/**
 * 保存专辑
 *
 * @param albumInfoVo
 * @param userId
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void saveAlbumInfo(AlbumInfoVo albumInfoVo, Long userId) {

    //....省略代码
    
    //同时将新上架状态公开的专辑存入ES 发送上架消息
    if ("1".equals(albumInfo.getIsOpen())){
        kafkaService.sendMessage(KafkaConstant.QUEUE_ALBUM_UPPER,String.valueOf(albumInfo.getId()  ));
    }
}

更新专辑方法添加

/**
 * 保存专辑方法
 *
 * @param albumInfoVo
 * @param userId      -- 可以暂时写个固定值
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void saveAlbumInfo(AlbumInfoVo albumInfoVo, Long userId) {
    //....省略代码

    //TODO如果是开放专辑自动将专辑同步到ES索引库中
    if ("1".equals(albumInfo.getIsOpen())) {
        kafkaService.sendMessage(KafkaConstant.QUEUE_ALBUM_UPPER, albumInfo.getId().toString());
    } else {
        kafkaService.sendMessage(KafkaConstant.QUEUE_ALBUM_LOWER, albumInfo.getId().toString());
    }
}

删除专辑方法添加

/**
 * 删除专辑
 * @param id
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void removeAlbumInfoById(Long id) {
    //	删除专辑表的数据 album_info
    this.removeById(id);
    //	删除专辑属性信息
    albumAttributeValueMapper.delete(new LambdaQueryWrapper<AlbumAttributeValue>().eq(AlbumAttributeValue::getAlbumId, id));
    //	删除专辑对应的统计数据
    albumStatMapper.delete(new LambdaQueryWrapper<AlbumStat>().eq(AlbumStat::getAlbumId, id));

    //TODO 同步删除索引库中专辑文档
    kafkaService.sendMessage(KafkaConstant.QUEUE_ALBUM_LOWER, id.toString());
}

2.4.2 消费者:搜索服务

监听:在service-search微服务中添加监听方法

package com.atguigu.tingshu.search.receiver;

import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.search.service.SearchService;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

/**
 * @author: atguigu
 * @create: 2023-09-19 22:20
 */
@Slf4j
@Component
public class SearchReceiver {


    @Autowired
    private SearchService searchService;

    /**
     * 监听专辑上架
     *
     * @param consumerRecord
     */
    @KafkaListener(topics = KafkaConstant.QUEUE_ALBUM_UPPER)
    public void upperGoods(ConsumerRecord<String, String> consumerRecord) {
        //  获取到发送的消息
        Long albumId = Long.parseLong(consumerRecord.value());
        if (null != albumId) {
            searchService.upperAlbum(albumId);
        }
    }

    /**
     * 监听专辑下架
     *
     * @param consumerRecord
     */
    @KafkaListener(topics = KafkaConstant.QUEUE_ALBUM_LOWER)
    public void lowerGoods(ConsumerRecord<String, String> consumerRecord) {
        //  获取到发送的消息
        Long albumId = Long.parseLong(consumerRecord.value());
        if (null != albumId) {
            searchService.lowerAlbum(albumId);
        }
    }
}

3、专辑检索

3.1 封装条件及响应对象

根据用户可能输入的检索条件,所有条件封装到一个实体对象中 AlbumIndexQuery

package com.atguigu.tingshu.query.search;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.util.List;

@Data
@Schema(description = "专辑信息搜索")
public class AlbumIndexQuery {

	@Schema(description = "关键字")
	private String keyword;

	@Schema(description = "一级分类")
	private Long category1Id;

	@Schema(description = "二级分类")
	private Long category2Id;

	@Schema(description = "三级分类")
	private Long category3Id;

	@Schema(description = "属性(属性id:属性值id)")
	private List<String> attributeList;

	// order=1:asc  排序规则   0:asc
	@Schema(description = "排序(综合排序[1:desc] 播放量[2:desc] 发布时间[3:desc];asc:升序 desc:降序)")
	private String order = "";// 1:综合排序 2:播放量 3:最近更新

	private Integer pageNo = 1;//分页信息
	private Integer pageSize = 10;
}

将检索的结果统一封装到一个实体类便于显示检索内容 AlbumSearchResponseVo

package com.atguigu.tingshu.vo.search;

import lombok.Data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Data
public class AlbumSearchResponseVo implements Serializable {

    //检索出来的商品信息
    private List<AlbumInfoIndexVo> list = new ArrayList<>();
    private Long total;//总记录数
    private Integer pageSize;//每页显示的内容
    private Integer pageNo;//当前页面
    private Long totalPages;

}

3.2 检索实现

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/57

SearchApiController控制器

/**
 * 根据关键词检索
 * @param queryVo
 * @return
 * @throws IOException
 */
@Operation(summary = "专辑搜索列表")
@PostMapping("/albumInfo")
public Result search(@RequestBody AlbumIndexQuery queryVo) throws IOException {
    //  调用服务层方法.
    AlbumSearchResponseVo albumSearchResponseVo = searchService.search(queryVo);
    return Result.ok(albumSearchResponseVo);
}

SearchService接口

/**
  * 根据关键词检索
  * @param albumIndexQuery
  * @return
  */
AlbumSearchResponseVo search(AlbumIndexQuery queryVo);

SearchServiceImpl实现类

@Autowired
private ElasticsearchClient elasticsearchClient;


/**
 * 根据多条件进行检索过滤专辑数据
 *
 * @param queryVo
 * @return
 */
@Override
public AlbumSearchResponseVo search(AlbumIndexQuery queryVo) {
    try {
        //1.封装检索请求对象
        SearchRequest searchRequest = this.buildDSL(queryVo);
        //2.执行检索
        SearchResponse<AlbumInfoIndex> searchResponse = elasticsearchClient.search(searchRequest, AlbumInfoIndex.class);
        //3.解析ES响应数据
        return this.parseResult(searchResponse, queryVo);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

//专辑索引库名称
private static final String INDEX_NAME = "albuminfo";

/**
 * 基于检索条件对象封装检索请求对象
 *
 * @param queryVo
 * @return
 */
@Override
public SearchRequest buildDSL(AlbumIndexQuery queryVo) {
    //1.创建请求构建器对象
    SearchRequest.Builder searchRequestBuiler = new SearchRequest.Builder().index(INDEX_NAME);
    //1.1 设置query 查询过滤条件
    BoolQuery.Builder allBoolQueryBuilder = new BoolQuery.Builder();
    //1.1.1 设置关键字查询
    String keyword = queryVo.getKeyword();
    if (StringUtils.isNotBlank(keyword)) {
        BoolQuery.Builder keyWordBoolBuilder = new BoolQuery.Builder();
        keyWordBoolBuilder.should(s -> s.match(m -> m.field("albumTitle").query(keyword)));
        keyWordBoolBuilder.should(s -> s.match(m -> m.field("albumIntro").query(keyword)));
        keyWordBoolBuilder.should(s -> s.match(m -> m.field("announcerName").query(keyword)));
        allBoolQueryBuilder.must(keyWordBoolBuilder.build()._toQuery());
    }
    //1.1.2 设置分类过滤条件
    Long category1Id = queryVo.getCategory1Id();
    if (category1Id != null) {
        allBoolQueryBuilder.filter(f -> f.term(t -> t.field("category1Id").value(category1Id)));
    }
    Long category2Id = queryVo.getCategory2Id();
    if (category2Id != null) {
        allBoolQueryBuilder.filter(f -> f.term(t -> t.field("category2Id").value(category2Id)));
    }
    Long category3Id = queryVo.getCategory3Id();
    if (category3Id != null) {
        allBoolQueryBuilder.filter(f -> f.term(t -> t.field("category3Id").value(category3Id)));
    }
    //1.1.2 设置专辑属性过滤条件
    List<String> attributeList = queryVo.getAttributeList();
    if (!CollectionUtils.isEmpty(attributeList)) {
        //以:进行分割,分割后应该是2个元素,属性id:属性值id (以-分割的字符串)
        for (String attribute : attributeList) {
            String[] split = attribute.split(":");
            if (split != null && split.length == 2) {
                Query nestedQuery = NestedQuery.of(n ->
                        n.path("attributeValueIndexList")
                                .query(q ->
                                        q.bool(b ->
                                                b.must(m -> m.term(t -> t.field("attributeValueIndexList.attributeId").value(split[0])))
                                                        .must(m -> m.term(t -> t.field("attributeValueIndexList.valueId").value(split[1])))
                                        )))._toQuery();
                allBoolQueryBuilder.filter(nestedQuery);
            }
        }
    }
    searchRequestBuiler.query(allBoolQueryBuilder.build()._toQuery());

    //1.2 设置分页
    Integer pageNo = queryVo.getPageNo();
    Integer pageSize = queryVo.getPageSize();
    Integer from = (pageNo - 1) * pageSize;
    searchRequestBuiler.from(from).size(pageSize);


    //1.3 设置排序
    String order = queryVo.getOrder();
    String orderField = "";
    String sort = "";
    if (StringUtils.isNotBlank(order)) {
        String[] split = order.split(":");
        if (split != null && split.length == 2) {
            switch (split[0]) {
                case "1":
                    orderField = "hotScore";
                    break;
                case "2":
                    orderField = "playStatNum";
                    break;
                case "3":
                    orderField = "createTime";
                    break;
            }
            sort = split[1];
        }
        String finalOrderField = orderField;
        String finalSort = sort;
        searchRequestBuiler.sort(s -> s.field(f -> f.field(finalOrderField).order("asc".equals(finalSort) ? SortOrder.Asc : SortOrder.Desc)));
    }

    //1.4 设置高亮
    if (StringUtils.isNotBlank(keyword)) {
        searchRequestBuiler.highlight(h -> h.fields("albumTitle", f -> f.preTags("<font color='red'>").postTags("</font>")));
    }

    //1.5 设置查询返回索引库字段
    searchRequestBuiler.source(s -> s.filter(f -> f.excludes("attributeValueIndexList", "hotScore")));

    return searchRequestBuiler.build();
}

查询结果集封装

/**
 * 解析ES响应结果
 *
 * @param searchResponse
 * @param queryVo
 * @return
 */
@Override
public AlbumSearchResponseVo parseResult(SearchResponse<AlbumInfoIndex> searchResponse, AlbumIndexQuery queryVo) {
    AlbumSearchResponseVo vo = new AlbumSearchResponseVo();
    //1.封装分页信息
    vo.setPageNo(queryVo.getPageNo());
    Integer pageSize = queryVo.getPageSize();
    vo.setPageSize(pageSize);

    long total = searchResponse.hits().total().value();
    vo.setTotal(total);

    long totalPage = total % pageSize == 0 ? total / pageSize : total / pageSize + 1;
    vo.setTotalPages(totalPage);

    //2.封装业务数据
    List<Hit<AlbumInfoIndex>> hitList = searchResponse.hits().hits();
    if (!CollectionUtils.isEmpty(hitList)) {
        List<AlbumInfoIndexVo> albumInfoIndexVoList = hitList.stream().map(hit -> {
            AlbumInfoIndexVo albumInfoIndexVo = new AlbumInfoIndexVo();
            AlbumInfoIndex albumInfoIndex = hit.source();
            BeanUtils.copyProperties(albumInfoIndex, albumInfoIndexVo);
            //处理高亮
            Map<String, List<String>> highlight = hit.highlight();
            if (!CollectionUtils.isEmpty(highlight)) {
                String albumTitle = highlight.get("albumTitle").get(0);
                albumInfoIndexVo.setAlbumTitle(albumTitle);
            }
            return albumInfoIndexVo;
        }).collect(Collectors.toList());
        vo.setList(albumInfoIndexVoList);
    }
    return vo;
}

4、热门专辑检索

当进入首页,会自动发出三个请求

  • 查询所有(1、2、3)分类列表 例如:音乐、有声书、娱乐等
  • 根据一级分类下包含所有三级分类列表。例如:音乐分类下的 催眠音乐、放松音乐
  • 根据一级分类查询所有三级分类下包含的热门专辑(热度前6)

4.1 查询一级分类下三级分类列表

image-20231011153747113

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/59

service-album 模块 BaseCategoryApiController 控制器中添加

/**
 * 根据一级分类Id 查询置顶频道页的三级分类列表
 * @param category1Id
 * @return
 */
@Operation(summary = "获取一级分类下置顶到频道页的三级分类列表")
@GetMapping("/category/findTopBaseCategory3/{category1Id}")
public Result<List<BaseCategory3>> getTopBaseCategory3(@PathVariable Long category1Id) {
    //	获取三级分类列表
    List<BaseCategory3> baseCategory3List = baseCategoryService.getTopBaseCategory3ByCategory1Id(category1Id);
    //	返回数据
    return Result.ok(baseCategory3List);
}

BaseCategoryService接口:

/**
 * 根据一级分类Id 查询置顶频道页的三级分类列表
 *
 * @param category1Id
 * @return
 */
List<BaseCategory3> getTopBaseCategory3ByCategory1Id(Long category1Id);

BaseCategoryServiceImpl实现类

/**
 * 根据一级分类Id 查询置顶频道页的三级分类列表
 *
 * @param category1Id
 * @return
 */
@Override
public List<BaseCategory3> getTopBaseCategory3ByCategory1Id(Long category1Id) {
    //1.根据一级分类ID查询视图列表
    LambdaQueryWrapper<BaseCategoryView> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(BaseCategoryView::getCategory1Id, category1Id);
    List<BaseCategoryView> baseCategoryViewList = baseCategoryViewMapper.selectList(queryWrapper);

    //2.获取三级分类ID集合
    if (CollectionUtil.isNotEmpty(baseCategoryViewList)) {
        List<Long> category3IdList = baseCategoryViewList.stream().map(BaseCategoryView::getCategory3Id).collect(Collectors.toList());
        return baseCategory3Mapper.selectBatchIds(category3IdList);
    }
    return null;
}

service-album-client模块中AlbumFeignClient提供Feign接口

/**
 * 根据一级分类Id 查询置顶频道页的三级分类列表
 * @param category1Id
 * @return
 */
@GetMapping("/category/findTopBaseCategory3/{category1Id}")
public Result<List<BaseCategory3>> getTopBaseCategory3(@PathVariable Long category1Id);

AlbumDegradeFeignClient服务降级类

@Override
public Result<List<BaseCategory3>> getTopBaseCategory3(Long category1Id) {
    return null;
}

4.2 根据一级分类Id 查询频道数据

获取频道页数据时,页面需要将存储一个 List 集合,map 中 需要存储

该接口是获取一级分类下,置顶到频道页的三级分类(base_category3)的热门专辑数据

map.put("baseCategory3", category3IdToMap.get(category3Id));
map.put("list", albumInfoIndexList);

image-20230921152112908

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/61

/**
 * 根据一级分类Id获取置顶数据
 *
 * @param category1Id
 * @return
 */
@Operation(summary = "获取频道页数据")
@GetMapping("/albumInfo/channel/{category1Id}")
public Result channel(@PathVariable Long category1Id) {
    //  调用服务层方法
    List<Map<String, Object>> mapList = searchService.channel(category1Id);
    return Result.ok(mapList);
}

接口:

/**
* 根据一级分类Id 获取置顶数据
* @param category1Id
* @return
*/
List<Map<String, Object>> channel(Long category1Id);

实现类:

基本DSL语句:

GET /albuminfo/_search
{
  "aggregations": {
    "groupByCategory3IdAgg": {
      "aggregations": {
        "topTenHotScoreAgg": {
          "top_hits": {
            "size": 6,
            "sort": [
              {
                "hotScore": {
                  "order": "desc"
                }
              }
            ]
          }
        }
      },
      "terms": {
        "field": "category3Id"
      }
    }
  },
  "query": {
    "terms": {
      "category3Id": [
        1001,
        1002,
        1003,
        1004,
        1005,
        1006,
        1007
      ]
    }
  }
}
@Override
public List<Map<String, Object>> channel(Long category1Id) {
    try {
        //  根据一级分类Id获取到置顶数据集合
        Result<List<BaseCategory3>> baseCategory3ListResult = albumFeignClient.getTopBaseCategory3(category1Id);
        List<BaseCategory3> baseCategory3List = baseCategory3ListResult.getData();
        Assert.notNull(baseCategory3List, "分类集合不能为空");

        //  建立对应关系 key=三级分类Id value=三级分类对象
        Map<Long, BaseCategory3> category3IdToMap = baseCategory3List.stream().collect(Collectors.toMap(BaseCategory3::getId, baseCategory3 -> baseCategory3));


        //  获取置顶的三级分类Id 列表
        List<Long> idList = baseCategory3List.stream().map(BaseCategory3::getId).collect(Collectors.toList());

        //  调用查询方法.
        List<FieldValue> valueList = idList.stream().map(id -> FieldValue.of(id)).collect(Collectors.toList());
        SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(s -> s
                        .index("albuminfo")
                        .size(0)
                        .query(q -> q.terms(t -> t.field("category3Id").terms(new TermsQueryField.Builder().value(valueList).build())))
                        .aggregations("groupByCategory3IdAgg", a -> a
                                .terms(t -> t.field("category3Id")
                                )
                                .aggregations("topTenHotScoreAgg", a1 -> a1
                                        .topHits(t -> t
                                                .size(6)
                                                .sort(sort -> sort.field(f -> f.field("hotScore").order(SortOrder.Desc)))))
                        )
                , AlbumInfoIndex.class);

        List<Map<String, Object>> result = new ArrayList<>();
        //  从聚合中获取数据
        Aggregate groupByCategory3IdAgg = response.aggregations().get("groupByCategory3IdAgg");
        groupByCategory3IdAgg.lterms().buckets().array().forEach(item -> {
            List<AlbumInfoIndex> albumInfoIndexList = new ArrayList<>();
            Long category3Id = item.key();
            Aggregate topTenHotScoreAgg = item.aggregations().get("topTenHotScoreAgg");
            topTenHotScoreAgg.topHits().hits().hits().forEach(hit -> {
                AlbumInfoIndex albumInfoIndex = JSONObject.parseObject(hit.source().toString(), AlbumInfoIndex.class);
                albumInfoIndexList.add(albumInfoIndex);
            });
            Map<String, Object> map = new HashMap<>();
            map.put("baseCategory3", category3IdToMap.get(category3Id));
            map.put("list", albumInfoIndexList);
            result.add(map);
        });
        return result;
    } catch (IOException e) {
        log.error("[检索服务]查询分类下热门专辑异常:{}", e);
        throw new RuntimeException(e);
    }
}

4.3 根据一级分类Id获取全部分类信息

YAP接口地址:http://192.168.200.6:3000/project/11/interface/api/63

点击全部的时候,加载所有的一级分类信息数据:

BaseCategoryApiController控制器

/**
 * 根据一级分类Id 获取全部数据
 * @param category1Id
 * @return
 */
@Operation(summary = "根据一级分类id获取全部分类信息")
@GetMapping("/category/getBaseCategoryList/{category1Id}")
public Result<JSONObject> getBaseCategoryList(@PathVariable Long category1Id){
   JSONObject jsonObject = baseCategoryService.getAllCategoryList(category1Id);
   return Result.ok(jsonObject);
}

BaseCategoryService接口:

/**
 * 根据一级分类Id 获取数据
 * @param category1Id
 * @return
 */
JSONObject getAllCategoryList(Long category1Id);

BaseCategoryServiceImpl实现类:

@Override
public JSONObject getAllCategoryList(Long category1Id) {
   BaseCategory1 baseCategory1 = baseCategory1Mapper.selectById(category1Id);
   // 声明一级分类对象
   JSONObject category1 = new JSONObject();
   category1.put("categoryId", category1Id);
   category1.put("categoryName", baseCategory1.getName());

   //获取全部分类信息
   List<BaseCategoryView> baseCategoryViewList = baseCategoryViewMapper.selectList(new LambdaQueryWrapper<BaseCategoryView>().eq(BaseCategoryView::getCategory1Id, category1Id));

   //根据二级分类ID分组转换数据
   Map<Long, List<BaseCategoryView>> category2Map = baseCategoryViewList.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory2Id));
   List<JSONObject> category2Child = new ArrayList<>();
   for(Map.Entry<Long, List<BaseCategoryView>> entry2 : category2Map.entrySet()) {
      //二级分类ID
      Long category2Id = entry2.getKey();
      //二级分类对应的全部数据(三级数据)
      List<BaseCategoryView> category3List = entry2.getValue();

      // 声明二级分类对象
      JSONObject category2 = new JSONObject();
      category2.put("categoryId", category2Id);
      category2.put("categoryName", category3List.get(0).getCategory2Name());

      // 循环三级分类数据
      List<JSONObject> category3Child = new ArrayList<>();
      category3List.stream().forEach(category3View -> {
         JSONObject category3 = new JSONObject();
         category3.put("categoryId", category3View.getCategory3Id());
         category3.put("categoryName", category3View.getCategory3Name());
         category3Child.add(category3);
      });
      category2Child.add(category2);
      // 将三级数据放入二级里面
      category2.put("categoryChild", category3Child);
   }
   // 将二级数据放入一级里面
   category1.put("categoryChild", category2Child);
   return category1;
}

5、关键字自动补全功能

image-20231005103017179

5.1 新增关键字

创建索引库:

package com.atguigu.tingshu.search.repository;

import com.atguigu.tingshu.model.search.SuggestIndex;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface SuggestIndexRepository extends ElasticsearchRepository<SuggestIndex, String> {
}

service-search模块SearchServiceImpl.upperAlbum上架方法中追加内容:

@Autowired
private SuggestIndexRepository suggestIndexRepository;



//  更新关键字智能提示库
//  专辑标题
SuggestIndex titleSuggestIndex = new SuggestIndex();
titleSuggestIndex.setId(UUID.randomUUID().toString().replaceAll("-", ""));
titleSuggestIndex.setTitle(albumInfoIndex.getAlbumTitle());
titleSuggestIndex.setKeyword(new Completion(new String[]{albumInfoIndex.getAlbumTitle()}));
titleSuggestIndex.setKeywordPinyin(new Completion(new String[]{PinYinUtils.toHanyuPinyin(albumInfoIndex.getAlbumTitle())}));
titleSuggestIndex.setKeywordSequence(new Completion(new String[]{PinYinUtils.getFirstLetter(albumInfoIndex.getAlbumTitle())}));
suggestIndexRepository.save(titleSuggestIndex);

// 专辑主播
SuggestIndex announcerSuggestIndex = new SuggestIndex();
announcerSuggestIndex.setId(UUID.randomUUID().toString().replaceAll("-", ""));
announcerSuggestIndex.setTitle(albumInfoIndex.getAnnouncerName());
announcerSuggestIndex.setKeyword(new Completion(new String[]{albumInfoIndex.getAnnouncerName()}));
announcerSuggestIndex.setKeywordPinyin(new Completion(new String[]{PinYinUtils.toHanyuPinyin(albumInfoIndex.getAnnouncerName())}));
announcerSuggestIndex.setKeywordSequence(new Completion(new String[]{PinYinUtils.getFirstLetter(albumInfoIndex.getAnnouncerName())}));
suggestIndexRepository.save(announcerSuggestIndex);

5.2 关键字自动提示

相关dsl 语句:

GET /suggestinfo/_search
{
  "suggest": {
    "suggestionKeyword": {
      "prefix": "韩",
      "completion": {
        "field": "keyword",
        "size": 10,
        "skip_duplicates": true
      }
    },
    "suggestionKeywordSequence": {
      "prefix": "韩",
      "completion": {
        "field": "keywordSequence",
        "size": 10,
        "skip_duplicates": true
      }
    },
    "suggestionKeywordPinyin": {
      "prefix": "韩",
      "completion": {
        "field": "keywordPinyin",
        "size": 10,
        "skip_duplicates": true
      }
    }
  }
}

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/65

SearchApiController控制器

/**
 * 自动补全功能
 * @param keyword
 * @return
 */
@Operation(summary = "关键字自动补全")
@GetMapping("/albumInfo/completeSuggest/{keyword}")
public Result completeSuggest(@PathVariable String keyword) {
    //  根据关键词查询补全
    List<String> list = searchService.completeSuggest(keyword);
    //  返回数据
    return Result.ok(list);
}

SearchService接口

/**
* 根据关键字自动补全功能
* @param keyword
* @return
*/
List<String> completeSuggest(String keyword);

SearchServiceImpl实现类:

@SneakyThrows
@Override
public List<String> completeSuggest(String keyword) {
  //  创建Suggester 对象
  Suggester suggester = new Suggester.Builder()
    .suggesters("suggestionKeyword", s ->
                s.prefix(keyword)
                .completion(c -> c.field("keyword")
                            .size(10)
                            .skipDuplicates(true)))
    .suggesters("suggestionKeywordSequence", s ->
                s.prefix(keyword)
                .completion(c -> c.field("keywordSequence")
                            .size(10)
                            .skipDuplicates(true)))
    .suggesters("suggestionKeywordPinyin", s ->
                s.prefix(keyword)
                .completion(c -> c.field("keywordPinyin")
                            .size(10)
                            .skipDuplicates(true))).build();
  //  打印对象
  System.out.println(suggester.toString());
  SearchResponse<SuggestIndex> response = elasticsearchClient.search(s ->
                                                                     s.index("suggestinfo")
                                                                     .suggest(suggester),
                                                                     SuggestIndex.class);

  //  处理关键字搜索结构
  HashSet<String> titleSet = new HashSet<>();
  titleSet.addAll(this.parseSearchResult(response, "suggestionKeyword"));
  titleSet.addAll(this.parseSearchResult(response, "suggestionKeywordPinyin"));
  titleSet.addAll(this.parseSearchResult(response, "suggestionKeywordSequence"));

  //  判断
  if (titleSet.size() < 10) {
    SearchResponse<SuggestIndex> responseSuggest = elasticsearchClient.search(s ->
                                                                              s.index("suggestinfo")
                                                                              .size(10)
                                                                              .query(q -> q.match(m -> m.field("title").query(keyword))), SuggestIndex.class);
    List<Hit<SuggestIndex>> hits = responseSuggest.hits().hits();
    //  循环遍历
    for (Hit<SuggestIndex> hit : hits) {
      titleSet.add(hit.source().getTitle());
      //  计算智能推荐总个数最大10个
      int total = titleSet.size();
      if (total>=10) break;
    }
  }
  //  返回这个集合
  return new ArrayList<>(titleSet);
}
/**
 * 处理聚合结果集
 * @param response
 * @param suggestName
 * @return
 */
private List<String> parseSearchResult(SearchResponse<SuggestIndex> response, String suggestName) {
    //  创建集合
    List<String> suggestList = new ArrayList<>();
    Map<String, List<Suggestion<SuggestIndex>>> groupBySuggestionListAggMap = response.suggest();
    groupBySuggestionListAggMap.get(suggestName).forEach(item -> {
        CompletionSuggest<SuggestIndex> completionSuggest =  item.completion();
        completionSuggest.options().forEach(it -> {
            SuggestIndex suggestIndex = it.source();
            suggestList.add(suggestIndex.getTitle());
        });
    });
    //  返回集合列表
    return suggestList;
}