feat(search): 增强笔记搜索功能支持类型筛选和多种排序方式
- 新增笔记类型筛选功能,支持图文和视频类型过滤 - 添加多种排序方式:最新发布、最多点赞、最多评论、最多收藏 - 实现综合排序逻辑,基于 function_score 查询优化搜索结果 - 增加搜索结果高亮显示标题功能 -重构查询构建逻辑,支持动态构建 bool 查询和 function_score 查询 - 添加 NoteSortTypeEnum 枚举类管理排序类型 - 优化搜索响应处理,支持高亮字段解析和数据格式化 - 更新 SearchNoteReqVO 模型,添加 type 和 sort 查询参数 - 改进异常处理逻辑,使用 IOException 替代通用 Exception - 移除旧的 stream 处理方式,采用循环遍历处理搜索结果
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
package com.hanserwei.hannote.search.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum NoteSortTypeEnum {
|
||||||
|
|
||||||
|
// 最新
|
||||||
|
LATEST(0),
|
||||||
|
// 最新点赞
|
||||||
|
MOST_LIKE(1),
|
||||||
|
// 最多评论
|
||||||
|
MOST_COMMENT(2),
|
||||||
|
// 最多收藏
|
||||||
|
MOST_COLLECT(3),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型 code 获取对应的枚举
|
||||||
|
*
|
||||||
|
* @param code 类型 code
|
||||||
|
* @return 枚举
|
||||||
|
*/
|
||||||
|
public static NoteSortTypeEnum valueOf(Integer code) {
|
||||||
|
for (NoteSortTypeEnum noteSortTypeEnum : NoteSortTypeEnum.values()) {
|
||||||
|
if (Objects.equals(code, noteSortTypeEnum.getCode())) {
|
||||||
|
return noteSortTypeEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,4 +19,14 @@ public class SearchNoteReqVO {
|
|||||||
@Min(value = 1, message = "页码不能小于 1")
|
@Min(value = 1, message = "页码不能小于 1")
|
||||||
private Integer pageNo = 1; // 默认值为第一页
|
private Integer pageNo = 1; // 默认值为第一页
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记类型:null:综合 / 0:图文 / 1:视频
|
||||||
|
*/
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序:null:不限 / 0:最新 / 1:最多点赞 / 2:最多评论 / 3:最多收藏
|
||||||
|
*/
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,18 @@ package com.hanserwei.hannote.search.service.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||||
|
import co.elastic.clients.elasticsearch._types.SortOptions;
|
||||||
import co.elastic.clients.elasticsearch._types.SortOrder;
|
import co.elastic.clients.elasticsearch._types.SortOrder;
|
||||||
import co.elastic.clients.elasticsearch._types.query_dsl.*;
|
import co.elastic.clients.elasticsearch._types.query_dsl.*;
|
||||||
import co.elastic.clients.elasticsearch.core.SearchRequest;
|
import co.elastic.clients.elasticsearch.core.SearchRequest;
|
||||||
import co.elastic.clients.elasticsearch.core.SearchResponse;
|
import co.elastic.clients.elasticsearch.core.SearchResponse;
|
||||||
|
import co.elastic.clients.elasticsearch.core.search.Highlight;
|
||||||
import co.elastic.clients.elasticsearch.core.search.HighlightField;
|
import co.elastic.clients.elasticsearch.core.search.HighlightField;
|
||||||
import co.elastic.clients.elasticsearch.core.search.Hit;
|
import co.elastic.clients.elasticsearch.core.search.Hit;
|
||||||
import co.elastic.clients.util.NamedValue;
|
import co.elastic.clients.util.NamedValue;
|
||||||
import com.hanserwei.framework.common.response.PageResponse;
|
import com.hanserwei.framework.common.response.PageResponse;
|
||||||
import com.hanserwei.framework.common.utils.NumberUtils;
|
import com.hanserwei.framework.common.utils.NumberUtils;
|
||||||
|
import com.hanserwei.hannote.search.enums.NoteSortTypeEnum;
|
||||||
import com.hanserwei.hannote.search.index.NoteIndex;
|
import com.hanserwei.hannote.search.index.NoteIndex;
|
||||||
import com.hanserwei.hannote.search.model.vo.SearchNoteReqVO;
|
import com.hanserwei.hannote.search.model.vo.SearchNoteReqVO;
|
||||||
import com.hanserwei.hannote.search.model.vo.SearchNoteRspVO;
|
import com.hanserwei.hannote.search.model.vo.SearchNoteRspVO;
|
||||||
@@ -19,10 +22,12 @@ import jakarta.annotation.Resource;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -37,121 +42,222 @@ public class NoteServiceImpl implements NoteService {
|
|||||||
String keyword = searchNoteReqVO.getKeyword();
|
String keyword = searchNoteReqVO.getKeyword();
|
||||||
// 当前页码
|
// 当前页码
|
||||||
Integer pageNo = searchNoteReqVO.getPageNo();
|
Integer pageNo = searchNoteReqVO.getPageNo();
|
||||||
|
// 笔记类型
|
||||||
|
Integer type = searchNoteReqVO.getType();
|
||||||
|
// 排序方式
|
||||||
|
Integer sort = searchNoteReqVO.getSort();
|
||||||
|
|
||||||
int pageSize = 10; // 每页展示数据量
|
// --- 2. 分页参数 ---
|
||||||
int from = (pageNo - 1) * pageSize; // 偏移量
|
int pageSize = 10;
|
||||||
|
int from = (pageNo - 1) * pageSize;
|
||||||
|
|
||||||
// 1. 构建基础 Multi-Match Query (原始查询条件)
|
//条件查询
|
||||||
|
// 创建查询条件
|
||||||
|
// "query": {
|
||||||
|
// "bool": {
|
||||||
|
// "must": [
|
||||||
|
// {
|
||||||
|
// "multi_match": {
|
||||||
|
// "query": "壁纸",
|
||||||
|
// "fields": [
|
||||||
|
// "title^2.0",
|
||||||
|
// "topic^1.0"
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "filter": [
|
||||||
|
// {
|
||||||
|
// "term": {
|
||||||
|
// "type": {
|
||||||
|
// "value": 0
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();
|
||||||
MultiMatchQuery multiMatchQuery = MultiMatchQuery.of(m -> m
|
MultiMatchQuery multiMatchQuery = MultiMatchQuery.of(m -> m
|
||||||
.query(keyword)
|
.query(keyword)
|
||||||
// 字段权重设置,与 RHL 的 .field(field, boost) 对应
|
// 新客户端推荐在字段名中直接附加权重
|
||||||
.fields(NoteIndex.FIELD_NOTE_TITLE + "^2.0", NoteIndex.FIELD_NOTE_TOPIC)
|
.fields(NoteIndex.FIELD_NOTE_TITLE + "^2.0", NoteIndex.FIELD_NOTE_TOPIC)
|
||||||
);
|
);
|
||||||
Query functionScoreQuery = Query.of(q -> q
|
boolQueryBuilder.must(multiMatchQuery);
|
||||||
.functionScore(fs -> fs
|
// 3.2. 构建 term (filter)
|
||||||
// 设置初始查询条件
|
if (Objects.nonNull(type)) {
|
||||||
.query(innerQuery -> innerQuery.multiMatch(multiMatchQuery))
|
boolQueryBuilder.filter(f -> f
|
||||||
.scoreMode(FunctionScoreMode.Sum)
|
.term(t -> t
|
||||||
.boostMode(FunctionBoostMode.Sum)
|
.field(NoteIndex.FIELD_NOTE_TYPE)
|
||||||
// 设置function数组
|
.value(type) // .value() 会自动处理 Integer, Long, String 等
|
||||||
.functions(List.of(
|
)
|
||||||
// 评分函数1
|
);
|
||||||
FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf
|
}
|
||||||
.field(NoteIndex.FIELD_NOTE_LIKE_TOTAL)
|
BoolQuery boolQuery = boolQueryBuilder.build();
|
||||||
.factor(0.5)
|
// --- 4. 构建排序 (Sort) 和 FunctionScore ---
|
||||||
.modifier(FieldValueFactorModifier.Sqrt)
|
Query finalQuery;
|
||||||
.missing(0.0))),
|
List<SortOptions> sortOptions = CollUtil.newArrayList();
|
||||||
// 评分函数2
|
NoteSortTypeEnum noteSortTypeEnum = NoteSortTypeEnum.valueOf(sort);
|
||||||
FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf
|
if (Objects.nonNull(noteSortTypeEnum)) {
|
||||||
.field(NoteIndex.FIELD_NOTE_COLLECT_TOTAL)
|
// 4.1. CASE 1: 按字段排序
|
||||||
.factor(0.3)
|
finalQuery = boolQuery._toQuery(); // 查询主体就是 bool 查询
|
||||||
.modifier(FieldValueFactorModifier.Sqrt)
|
|
||||||
.missing(0.0))),
|
switch (noteSortTypeEnum) {
|
||||||
// 评分函数3
|
// 按笔记发布时间降序
|
||||||
FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf
|
case LATEST -> sortOptions.add(SortOptions.of(s -> s
|
||||||
.field(NoteIndex.FIELD_NOTE_COMMENT_TOTAL)
|
.field(f -> f.field(NoteIndex.FIELD_NOTE_CREATE_TIME).order(SortOrder.Desc))
|
||||||
.factor(0.2)
|
));
|
||||||
.modifier(FieldValueFactorModifier.Sqrt)
|
// 按笔记点赞量降序
|
||||||
.missing(0.0)))
|
case MOST_LIKE -> sortOptions.add(SortOptions.of(s -> s
|
||||||
))
|
.field(f -> f.field(NoteIndex.FIELD_NOTE_LIKE_TOTAL).order(SortOrder.Desc))
|
||||||
)
|
));
|
||||||
);
|
// 按评论量降序
|
||||||
// 3. 构建 Highlight 配置
|
case MOST_COMMENT -> sortOptions.add(SortOptions.of(s -> s
|
||||||
|
.field(f -> f.field(NoteIndex.FIELD_NOTE_COMMENT_TOTAL).order(SortOrder.Desc))
|
||||||
|
));
|
||||||
|
// 按收藏量降序
|
||||||
|
case MOST_COLLECT -> sortOptions.add(SortOptions.of(s -> s
|
||||||
|
.field(f -> f.field(NoteIndex.FIELD_NOTE_COLLECT_TOTAL).order(SortOrder.Desc))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 4.2. CASE 2: 综合排序 (Function Score)
|
||||||
|
// 综合排序,按 _score 降序
|
||||||
|
sortOptions.add(SortOptions.of(s -> s.field(f -> f.field("_score").order(SortOrder.Desc))));
|
||||||
|
|
||||||
|
// 4.2.1. 构建 function_score 的 functions 列表
|
||||||
|
List<FunctionScore> functions = new ArrayList<>();
|
||||||
|
|
||||||
|
// Function 1: like_total
|
||||||
|
functions.add(FunctionScore.of(fs -> fs
|
||||||
|
.fieldValueFactor(fvf -> fvf
|
||||||
|
.field(NoteIndex.FIELD_NOTE_LIKE_TOTAL)
|
||||||
|
.factor(0.5) // 新版客户端使用 double
|
||||||
|
.modifier(FieldValueFactorModifier.Sqrt)
|
||||||
|
.missing(0.0) // missing 值也为 double
|
||||||
|
)
|
||||||
|
));
|
||||||
|
// 创建 FilterFunctionBuilder 数组
|
||||||
|
// "functions": [
|
||||||
|
// {
|
||||||
|
// "field_value_factor": {
|
||||||
|
// "field": "like_total",
|
||||||
|
// "factor": 0.5,
|
||||||
|
// "modifier": "sqrt",
|
||||||
|
// "missing": 0
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "field_value_factor": {
|
||||||
|
// "field": "collect_total",
|
||||||
|
// "factor": 0.3,
|
||||||
|
// "modifier": "sqrt",
|
||||||
|
// "missing": 0
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "field_value_factor": {
|
||||||
|
// "field": "comment_total",
|
||||||
|
// "factor": 0.2,
|
||||||
|
// "modifier": "sqrt",
|
||||||
|
// "missing": 0
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// Function 2: collect_total
|
||||||
|
functions.add(FunctionScore.of(fs -> fs
|
||||||
|
.fieldValueFactor(fvf -> fvf
|
||||||
|
.field(NoteIndex.FIELD_NOTE_COLLECT_TOTAL)
|
||||||
|
.factor(0.3)
|
||||||
|
.modifier(FieldValueFactorModifier.Sqrt)
|
||||||
|
.missing(0.0)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Function 3: comment_total
|
||||||
|
functions.add(FunctionScore.of(fs -> fs
|
||||||
|
.fieldValueFactor(fvf -> fvf
|
||||||
|
.field(NoteIndex.FIELD_NOTE_COMMENT_TOTAL)
|
||||||
|
.factor(0.2)
|
||||||
|
.modifier(FieldValueFactorModifier.Sqrt)
|
||||||
|
.missing(0.0)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
// 4.2.2. 构建 FunctionScoreQuery
|
||||||
|
FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.of(fsq -> fsq
|
||||||
|
.query(boolQuery._toQuery()) // 基础查询
|
||||||
|
.functions(functions) // 评分函数
|
||||||
|
.scoreMode(FunctionScoreMode.Sum) // 对应 score_mode
|
||||||
|
.boostMode(FunctionBoostMode.Sum) // 对应 boost_mode
|
||||||
|
);
|
||||||
|
|
||||||
|
finalQuery = functionScoreQuery._toQuery(); // 最终查询是 function_score
|
||||||
|
}
|
||||||
|
// --- 5. 构建高亮 (Highlight) ---
|
||||||
HighlightField titleHighlight = HighlightField.of(hf -> hf
|
HighlightField titleHighlight = HighlightField.of(hf -> hf
|
||||||
.preTags("<strong>")
|
.preTags("<strong>")
|
||||||
.postTags("</strong>")
|
.postTags("</strong>")
|
||||||
);
|
);
|
||||||
// 4. 构建最终的 SearchRequest
|
Highlight highlight = Highlight.of(h -> h.fields(NamedValue.of(NoteIndex.FIELD_NOTE_TITLE, titleHighlight)));
|
||||||
SearchRequest searchRequest = SearchRequest.of(r -> r
|
// --- 6. 构建最终的 SearchRequest ---
|
||||||
|
SearchRequest searchRequest = SearchRequest.of(s -> s
|
||||||
.index(NoteIndex.NAME)
|
.index(NoteIndex.NAME)
|
||||||
.query(functionScoreQuery) // 设置 function_score 查询
|
.query(finalQuery) // 设置查询
|
||||||
|
.sort(sortOptions) // 设置排序
|
||||||
// 排序:按 _score 降序
|
.from(from) // 设置分页
|
||||||
.sort(s -> s.score(d -> d.order(SortOrder.Desc)))
|
.size(pageSize) // 设置分页
|
||||||
|
.highlight(highlight) // 设置高亮
|
||||||
// 分页
|
|
||||||
.from(from)
|
|
||||||
.size(pageSize)
|
|
||||||
// 高亮
|
|
||||||
.highlight(h -> h
|
|
||||||
.fields(NamedValue.of(NoteIndex.FIELD_NOTE_TITLE, titleHighlight))
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
// 返参 VO 集合
|
// --- 7. 执行查询和解析响应 ---
|
||||||
List<SearchNoteRspVO> searchNoteRspVOS = null;
|
List<SearchNoteRspVO> searchNoteRspVOS = new ArrayList<>();
|
||||||
long total = 0;
|
long total = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info("==> NoteSearchRequest: {}", searchRequest.toString());
|
log.info("==> SearchRequest: {}", searchRequest.toString());
|
||||||
|
|
||||||
// ⭐️ 执行查询请求,并自动反序列化文档源到 SearchNoteRspVO
|
// 执行查询请求
|
||||||
SearchResponse<SearchNoteRspVO> searchResponse =
|
SearchResponse<SearchNoteRspVO> searchResponse = client.search(searchRequest, SearchNoteRspVO.class);
|
||||||
client.search(searchRequest, SearchNoteRspVO.class);
|
if (searchResponse.hits().total() != null) {
|
||||||
|
total = searchResponse.hits().total().value();
|
||||||
total = searchResponse.hits().total() != null ? searchResponse.hits().total().value() : 0;
|
}
|
||||||
log.info("==> 命中文档总数, hits: {}", total);
|
log.info("==> 命中文档总数, hits: {}", total);
|
||||||
|
List<Hit<SearchNoteRspVO>> hits = searchResponse.hits().hits();
|
||||||
|
for (Hit<SearchNoteRspVO> hit : hits) {
|
||||||
|
// 获取source
|
||||||
|
SearchNoteRspVO source = hit.source();
|
||||||
|
// 7.3. 获取高亮字段
|
||||||
|
String highlightedTitle = null;
|
||||||
|
Map<String, List<String>> highlightFields = hit.highlight();
|
||||||
|
if (highlightFields.containsKey(NoteIndex.FIELD_NOTE_TITLE)) {
|
||||||
|
highlightedTitle = highlightFields.get(NoteIndex.FIELD_NOTE_TITLE).getFirst();
|
||||||
|
}
|
||||||
|
if (source != null) {
|
||||||
|
Long noteId = source.getNoteId();
|
||||||
|
String cover = source.getCover();
|
||||||
|
String title = source.getTitle();
|
||||||
|
String highlightTitle = source.getHighlightTitle();
|
||||||
|
String avatar = source.getAvatar();
|
||||||
|
String nickname = source.getNickname();
|
||||||
|
LocalDateTime updateTime = source.getUpdateTime();
|
||||||
|
String likeTotal = source.getLikeTotal();
|
||||||
|
searchNoteRspVOS.add(SearchNoteRspVO.builder()
|
||||||
|
.noteId(noteId)
|
||||||
|
.cover(cover)
|
||||||
|
.title(title)
|
||||||
|
.highlightTitle(highlightTitle)
|
||||||
|
.avatar(avatar)
|
||||||
|
.nickname(nickname)
|
||||||
|
.updateTime(updateTime)
|
||||||
|
.highlightTitle(highlightedTitle)
|
||||||
|
.likeTotal(NumberUtils.formatNumberString(Long.parseLong(likeTotal)))
|
||||||
|
.build());
|
||||||
|
|
||||||
// ⭐️ 处理搜索结果:合并原始文档和高亮数据
|
}
|
||||||
searchNoteRspVOS = searchResponse.hits().hits().stream()
|
}
|
||||||
.map(this::processNoteHit)
|
} catch (IOException e) {
|
||||||
.filter(Objects::nonNull)
|
log.error("==> 搜索笔记异常: {}", e.getMessage());
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("==> 查询 Elasticsearch 异常: ", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return PageResponse.success(searchNoteRspVOS, pageNo, total);
|
return PageResponse.success(searchNoteRspVOS, pageNo, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 辅助方法:处理 Hit,合并 Source 和 Highlight 数据
|
|
||||||
*/
|
|
||||||
private SearchNoteRspVO processNoteHit(Hit<SearchNoteRspVO> hit) {
|
|
||||||
SearchNoteRspVO rspVO = hit.source();
|
|
||||||
|
|
||||||
if (rspVO == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. ⭐️ 处理高亮字段
|
|
||||||
Map<String, List<String>> highlights = hit.highlight();
|
|
||||||
|
|
||||||
if (CollUtil.isNotEmpty(highlights)) {
|
|
||||||
List<String> titleHighlights = highlights.get(NoteIndex.FIELD_NOTE_TITLE);
|
|
||||||
if (CollUtil.isNotEmpty(titleHighlights)) {
|
|
||||||
// 设置高亮标题
|
|
||||||
rspVO.setHighlightTitle(titleHighlights.getFirst());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 3. 确保 highlightTitle 有值 (如果没有高亮结果,使用原始 title)
|
|
||||||
if (rspVO.getHighlightTitle() == null) {
|
|
||||||
rspVO.setHighlightTitle(rspVO.getTitle());
|
|
||||||
}
|
|
||||||
// 4. 处理特殊格式化(如 RHL 代码中的点赞数格式化)
|
|
||||||
if (rspVO.getLikeTotal() != null) {
|
|
||||||
rspVO.setLikeTotal(NumberUtils.formatNumberString(Long.parseLong(rspVO.getLikeTotal())));
|
|
||||||
}
|
|
||||||
return rspVO;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user