记录专辑关键词和分类属性筛选、分页排序与高亮,以及首页分类和推荐数据接口。

本篇要点

  • 组合关键词与分类属性条件
  • 处理分页、排序和高亮
  • 梳理首页数据接口

今天内容

1、专辑检索接口

分析

DSL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# 根据关键词与分类属性Id 过滤
GET /albuminfo/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"albumTitle": "小说"
}
},
{
"match": {
"albumIntro": "小说"
}
}
],
"filter": [
{
"term": {
"category3Id": "1152"
}
}
]
}
}
}

# 根据属性与属性值Id 进行过滤
GET /albuminfo/_search
{
"query": {
"bool": {
"filter": [
{
"nested": {
"path": "attributeValueIndexList",
"query": {
"bool": {
"must": [
{
"term": {
"attributeValueIndexList.attributeId": {
"value": "15"
}
}
},
{
"term": {
"attributeValueIndexList.valueId": {
"value": "32"
}
}
}
]
}
}
}
}
]
}
}
}

# 分页-排序-高亮
GET /albuminfo/_search
{
"query": {
"match": {
"albumTitle": "小说"
}
},
"from": 0,
"size": 20,
"sort": [
{
"playStatNum": {
"order": "desc"
}
}
],
"highlight": {
"fields": {"albumTitle": {}},
"pre_tags": ["<font color='red'>"],
"post_tags": ["</font>"]
}
}
  • 封装两个实体类

– AlbumIndexQuery :接收前端传递过来数据

– AlbumSearchResponseVo:封装返回结果

  • 在service-search模块编写专辑检索接口

SearchApiController

1
2
3
4
5
6
7
8
9
10
//专辑检索的接口
@Operation(summary = "专辑搜索列表")
@PostMapping
public Result search(@RequestBody AlbumIndexQuery albumIndexQuery)
throws IOException {
//调用service
AlbumSearchResponseVo albumSearchResponseVo =
searchService.search(albumIndexQuery);
return Result.ok(albumSearchResponseVo);
}

(总记录数 + 每页显示记录数 -1)/ 每页显示记录数

service

基础方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//专辑检索的接口
@Override
public AlbumSearchResponseVo search(AlbumIndexQuery albumIndexQuery) throws Exception {
// 调用方法,构建dsl语句
SearchRequest request = this.buildQueryDsl(albumIndexQuery);
//调用 elasticsearchClient里面search执行检索
SearchResponse<AlbumInfoIndex> response =
elasticsearchClient.search(request, AlbumInfoIndex.class);

//把最终结果封装到AlbumSearchResponseVo
AlbumSearchResponseVo searchResponseVo = this.parseSearchResult(response);
//设置当前页
searchResponseVo.setPageNo(albumIndexQuery.getPageNo());
//每页记录数
searchResponseVo.setPageSize(albumIndexQuery.getPageSize());
//总页数
// 总页数 =(总记录数 + 每页显示记录数 -1)/ 每页显示记录数
long totalPages = (searchResponseVo.getTotal() + albumIndexQuery.getPageSize() - 1) / albumIndexQuery.getPageSize();
searchResponseVo.setTotalPages(totalPages);
//返回vo对象
return searchResponseVo;
}

构建DSL语句方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
    //构建dsl语句,返回SearchRequest对象
private SearchRequest buildQueryDsl(AlbumIndexQuery albumIndexQuery) {
//创建SearchRequestBuilder
SearchRequest.Builder requestBuilder = new SearchRequest.Builder();
//requestBuilder构建dsl语句
//创建BoolQuery对象
BoolQuery.Builder boolQuery = new BoolQuery.Builder();
//获取关键词
//{ bool - should - match - albumTitle:值}
String keyword = albumIndexQuery.getKeyword();
if(!StringUtils.isEmpty(keyword)) {
boolQuery.should(s->s.match(f->f.field("albumTitle").query(keyword)));
boolQuery.should(s->s.match(f->f.field("albumIntro").query(keyword)));

//高亮显示
requestBuilder.highlight(h->h.fields("albumTitle",
f->f.preTags("<span style=color:red>").postTags("</span>")));
}

//{ bool - filter - term - category3Id:值}
Long category3Id = albumIndexQuery.getCategory3Id();
if(!StringUtils.isEmpty(category3Id)) {
boolQuery.filter(s->s.term(f->f.field("category3Id").value(category3Id)));
}
// 一级分类Id
Long category1Id = albumIndexQuery.getCategory1Id();
if (!StringUtils.isEmpty(category1Id)) {
boolQuery.filter(f -> f.term(s -> s.field("category1Id").value(category1Id)));
}
// 二级分类Id
Long category2Id = albumIndexQuery.getCategory2Id();
if (!StringUtils.isEmpty(category2Id)) {
boolQuery.filter(f -> f.term(s -> s.field("category2Id").value(category2Id)));
}

// 属性id:属性值id
List<String> attributeList = albumIndexQuery.getAttributeList();
if(!CollectionUtils.isEmpty(attributeList)) {
for(String attribute : attributeList) {
//属性id:属性值id
String[] split = attribute.split(":");
if (null != split && split.length == 2) {
NestedQuery nestedQuery = NestedQuery.of(
f->f.path("attributeValueIndexList")
.query( q->q.bool(m->m.must(
t->t.term(
f1->f1.field("attributeValueIndexList.attributeId")
.value(split[0]))).must(
t->t.term(
f1->f1.field("attributeValueIndexList.valueId").value(split[1])
))
)));
boolQuery.filter(f->f.nested(nestedQuery));
}
}
}
//排序
//综合排序1 播放量2 发布时间3
// 1:asc 2:desc 3:desc
String order = albumIndexQuery.getOrder();
if(!StringUtils.isEmpty(order)) {
String[] split = order.split(":");
// 定义一个排序字段
String orderField = "";
// 定义一个排序规则
String sort = "";
// split[0] split[1]
if(split != null && split.length == 2) {
//根据 split[0] 1 2 3判断得到对应排序字段
switch (split[0]) {
case "1":
orderField="hotScore";
break;
case "2":
orderField = "playStatNum";
break;
case "3":
orderField = "createTime";
break;
}
//排序规则 asc desc
sort = split[1];
}
String finalSort = sort;
String finalOrderField = orderField;
//排序dsl语句
requestBuilder.sort(f->f.field(f1->f1.field(finalOrderField)
.order("asc".equals(finalSort)?SortOrder.Asc:SortOrder.Desc)));

} else {//默认排序方式
// 默认排序规则 _score
requestBuilder.sort(f->f.field(o->o.field("_score").order(SortOrder.Desc)));
}

// 字段选择
requestBuilder.source(s->s.filter(f->f.excludes("attributeValueIndexList")));

// 分页: (pageNo-1)*pageSize()
Integer from = (albumIndexQuery.getPageNo() - 1)*albumIndexQuery.getPageSize();
requestBuilder.from(from);
requestBuilder.size(albumIndexQuery.getPageSize());

//{albuminfo -- query -- bool -- boolQuery}
requestBuilder.index("albuminfo")
.query(f->f.bool(boolQuery.build()));

//requestBuilder的build返回SearchRequest
SearchRequest searchRequest = requestBuilder.build();
System.out.println("dsl: "+searchRequest.toString());
return searchRequest;
}

封装返回结果方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//把es查询返回SearchResponse处理,封装到AlbumSearchResponseVo
private AlbumSearchResponseVo
parseSearchResult(SearchResponse<AlbumInfoIndex> response) {
//创建vo对象
AlbumSearchResponseVo searchResponseVo = new AlbumSearchResponseVo();
//获取总记录数
HitsMetadata<AlbumInfoIndex> hits = response.hits();
long totalValue = hits.total().value();
//设置到vo对象里面
searchResponseVo.setTotal(totalValue);
//获取数据集合
List<Hit<AlbumInfoIndex>> subHist = hits.hits();
if(!CollectionUtils.isEmpty(subHist)) {
List<AlbumInfoIndexVo> list = subHist.stream().map(albumInfoIndexHit -> {
AlbumInfoIndexVo albumInfoIndexVo = new AlbumInfoIndexVo();
//获取返回具体值部分
AlbumInfoIndex albumInfoIndex = albumInfoIndexHit.source();
BeanUtils.copyProperties(albumInfoIndex, albumInfoIndexVo);
// 判断用户是否根据关键词进行检索.
if (null != albumInfoIndexHit.highlight().get("albumTitle")){
// 获取高亮数据
String albumTitle = albumInfoIndexHit
.highlight().get("albumTitle").get(0);
// 赋值高亮数据
albumInfoIndexVo.setAlbumTitle(albumTitle);
}
return albumInfoIndexVo;
}).collect(Collectors.toList());

//设置数据到vo里面 List<AlbumInfoIndexVo>
searchResponseVo.setList(list);
}
return searchResponseVo;
}

2、首页数据接口

分析

image-20251028140746585

接口分析

image-20251028141216419

根据一级分类id查询前7个置顶三级分类

实现过程分析

1
2
3
4
5
6
7
8
9
10
# 根据一级分类id查询前7个置顶三级分类

# 第一步 根据一级分类id查询下面所有二级分类id
SELECT bc.id FROM base_category2 bc WHERE bc.category1_id=1;
# 101 102 103

# 第二步 根据第一步查询出来所有二级分类id,查询下面三级分类数据,
# 有条件是否置顶 is_top=1,
SELECT * FROM base_category3 bc3 WHERE bc3.category2_id IN(101,102,103)
AND bc3.is_top=1 LIMIT 7

在service-album模块BaseCategoryApiController

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据一级分类Id 查询置顶频道页的三级分类列表
* @param category1Id
* @return
*/
@Operation(summary = "获取一级分类下置顶到频道页的三级分类列表")
@GetMapping("findTopBaseCategory3/{category1Id}")
public Result<List<BaseCategory3>>
findTopBaseCategory3(@PathVariable Long category1Id) {
List<BaseCategory3> list =
baseCategoryService.selectTopBaseCategory3(category1Id);
return Result.ok(list);
}

在service-album模块service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//根据一级分类Id 查询置顶频道页的三级分类列表
@Override
public List<BaseCategory3> selectTopBaseCategory3(Long category1Id) {

// # 第一步 根据一级分类id查询下面所有二级分类id
// SELECT bc.id FROM base_category2 bc WHERE bc.category1_id=1;
// # 101 102 103
LambdaQueryWrapper<BaseCategory2> wrapper2 = new LambdaQueryWrapper<>();
wrapper2.eq(BaseCategory2::getCategory1Id, category1Id);
List<BaseCategory2> baseCategory2List =
baseCategory2Mapper.selectList(wrapper2);

// baseCategory2List二级分类所有数据集合,获取所有二级分类id,构建新集合
List<Long> category2IdList = baseCategory2List.stream()
.map(BaseCategory2::getId)
.collect(Collectors.toList());

// # 第二步 根据第一步查询出来所有二级分类id,查询下面三级分类数据,
// # 有条件是否置顶 is_top=1,
// SELECT * FROM base_category3 bc3 WHERE bc3.category2_id IN(101,102,103)
// AND bc3.is_top=1 LIMIT 7
LambdaQueryWrapper<BaseCategory3> wrapper3 = new LambdaQueryWrapper<>();
wrapper3.in(BaseCategory3::getCategory2Id, category2IdList);
wrapper3.eq(BaseCategory3::getIsTop,1);
wrapper3.last(" limit 7");
List<BaseCategory3> baseCategory3List = baseCategory3Mapper.selectList(wrapper3);
return baseCategory3List;
}

远程调用定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@FeignClient(value = "service-album", fallback = CategoryDegradeFeignClient.class)
public interface CategoryFeignClient {

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

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

首页数据接口

在service-search模块SearchApiController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 根据一级分类Id获取数据
* @param category1Id
* @return
*/
@Operation(summary = "获取频道页数据")
@GetMapping("channel/{category1Id}")
public Result channel(@PathVariable Long category1Id) {
// 调用服务层方法
List<Map<String, Object>> mapList = searchService.channel(category1Id);
return Result.ok(mapList);
}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//首页数据
@Override
public List<Map<String, Object>> channel(Long category1Id) throws Exception {
//1 远程调用:根据一级分类id 获取下面前7个置顶的三级分类id
//前7个三级分类id集合形式
Result<List<BaseCategory3>> topBaseCategory3Result =
categoryFeignClient.findTopBaseCategory3(category1Id);
List<BaseCategory3> baseCategory3List = topBaseCategory3Result.getData();

//List<BaseCategory3> -- Map<三级分类id,三级分类对象>
Map<Long, BaseCategory3> category3Map = baseCategory3List.stream().collect(
Collectors.toMap(BaseCategory3::getId,
BaseCategory3 -> BaseCategory3));

// 获取到三级分类Id 集合
List<Long> category3IdList =
baseCategory3List.stream().map(BaseCategory3::getId)
.collect(Collectors.toList());

// List<Long> -- List<FieldValue>
List<FieldValue> idValueList =
category3IdList.stream().map(id -> FieldValue.of(id))
.collect(Collectors.toList());

//2 编写DSL语句查询es
SearchRequest.Builder requestBuilder = new SearchRequest.Builder();

requestBuilder.index("albuminfo").query(
q->q.terms(
f->f.field("category3Id")
.terms(
new TermsQueryField.Builder()
.value(idValueList).build())
));

requestBuilder.aggregations("groupByCategory3IdAgg",
a->a.terms(f->f.field("category3Id").size(10))
.aggregations("topTenHotScoreAgg",
a1->a1.topHits(
s->s.size(6)
.sort(o->o.field(o1->o1.field("hotScore")
.order(SortOrder.Desc)))))
);

SearchResponse<AlbumInfoIndex> response =
elasticsearchClient.search(requestBuilder.build(),
AlbumInfoIndex.class);

//3 从es查询返回结果获取需要数据,封装到map集合
// 声明集合
List<Map<String, Object>> result = new ArrayList<>();
// 从聚合中获取数据
Aggregate groupByCategory3IdAgg =
response.aggregations().get("groupByCategory3IdAgg");
groupByCategory3IdAgg.lterms().buckets().array().forEach(item ->{
// 创建集合数据
List<AlbumInfoIndex> albumInfoIndexList = new ArrayList<>();
// 获取三级分类Id 对象
long category3Id = item.key();
// 获取要置顶的集合数据
Aggregate topTenHotScoreAgg = item.aggregations().get("topTenHotScoreAgg");
// 循环遍历获取聚合中的数据
topTenHotScoreAgg.topHits().hits().hits().forEach(hit->{
// 获取到source 的json 字符串数据
String json = hit.source().toString();
// 将json 字符串转换为AlbumInfoIndex 对象
AlbumInfoIndex albumInfoIndex = JSON.parseObject(json, AlbumInfoIndex.class);
// 将对象添加到集合中
albumInfoIndexList.add(albumInfoIndex);
});

// 声明一个map 集合数据
Map<String, Object> map = new HashMap<>();

// 存储根据三级分类Id要找到的三级分类
map.put("baseCategory3",category3Map.get(category3Id));

// 存储所有的专辑集合数据
map.put("list",albumInfoIndexList);

// 将map 添加到集合中
result.add(map);
});

return result;
}