diff --git a/han-note-search/src/main/java/com/hanserwei/hannote/search/enums/NoteSortTypeEnum.java b/han-note-search/src/main/java/com/hanserwei/hannote/search/enums/NoteSortTypeEnum.java new file mode 100644 index 0000000..a346459 --- /dev/null +++ b/han-note-search/src/main/java/com/hanserwei/hannote/search/enums/NoteSortTypeEnum.java @@ -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; + } + +} \ No newline at end of file diff --git a/han-note-search/src/main/java/com/hanserwei/hannote/search/model/vo/SearchNoteReqVO.java b/han-note-search/src/main/java/com/hanserwei/hannote/search/model/vo/SearchNoteReqVO.java index 4407ad7..efd6cff 100644 --- a/han-note-search/src/main/java/com/hanserwei/hannote/search/model/vo/SearchNoteReqVO.java +++ b/han-note-search/src/main/java/com/hanserwei/hannote/search/model/vo/SearchNoteReqVO.java @@ -19,4 +19,14 @@ public class SearchNoteReqVO { @Min(value = 1, message = "页码不能小于 1") private Integer pageNo = 1; // 默认值为第一页 + /** + * 笔记类型:null:综合 / 0:图文 / 1:视频 + */ + private Integer type; + + /** + * 排序:null:不限 / 0:最新 / 1:最多点赞 / 2:最多评论 / 3:最多收藏 + */ + private Integer sort; + } \ No newline at end of file diff --git a/han-note-search/src/main/java/com/hanserwei/hannote/search/service/impl/NoteServiceImpl.java b/han-note-search/src/main/java/com/hanserwei/hannote/search/service/impl/NoteServiceImpl.java index a6c288a..877c561 100644 --- a/han-note-search/src/main/java/com/hanserwei/hannote/search/service/impl/NoteServiceImpl.java +++ b/han-note-search/src/main/java/com/hanserwei/hannote/search/service/impl/NoteServiceImpl.java @@ -2,15 +2,18 @@ package com.hanserwei.hannote.search.service.impl; import cn.hutool.core.collection.CollUtil; 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.query_dsl.*; import co.elastic.clients.elasticsearch.core.SearchRequest; 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.Hit; import co.elastic.clients.util.NamedValue; import com.hanserwei.framework.common.response.PageResponse; 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.model.vo.SearchNoteReqVO; import com.hanserwei.hannote.search.model.vo.SearchNoteRspVO; @@ -19,10 +22,12 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; @Service @Slf4j @@ -37,121 +42,222 @@ public class NoteServiceImpl implements NoteService { String keyword = searchNoteReqVO.getKeyword(); // 当前页码 Integer pageNo = searchNoteReqVO.getPageNo(); + // 笔记类型 + Integer type = searchNoteReqVO.getType(); + // 排序方式 + Integer sort = searchNoteReqVO.getSort(); - int pageSize = 10; // 每页展示数据量 - int from = (pageNo - 1) * pageSize; // 偏移量 + // --- 2. 分页参数 --- + 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 .query(keyword) - // 字段权重设置,与 RHL 的 .field(field, boost) 对应 + // 新客户端推荐在字段名中直接附加权重 .fields(NoteIndex.FIELD_NOTE_TITLE + "^2.0", NoteIndex.FIELD_NOTE_TOPIC) ); - Query functionScoreQuery = Query.of(q -> q - .functionScore(fs -> fs - // 设置初始查询条件 - .query(innerQuery -> innerQuery.multiMatch(multiMatchQuery)) - .scoreMode(FunctionScoreMode.Sum) - .boostMode(FunctionBoostMode.Sum) - // 设置function数组 - .functions(List.of( - // 评分函数1 - FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf - .field(NoteIndex.FIELD_NOTE_LIKE_TOTAL) - .factor(0.5) - .modifier(FieldValueFactorModifier.Sqrt) - .missing(0.0))), - // 评分函数2 - FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf - .field(NoteIndex.FIELD_NOTE_COLLECT_TOTAL) - .factor(0.3) - .modifier(FieldValueFactorModifier.Sqrt) - .missing(0.0))), - // 评分函数3 - FunctionScore.of(f -> f.fieldValueFactor(fvf -> fvf - .field(NoteIndex.FIELD_NOTE_COMMENT_TOTAL) - .factor(0.2) - .modifier(FieldValueFactorModifier.Sqrt) - .missing(0.0))) - )) - ) - ); - // 3. 构建 Highlight 配置 + boolQueryBuilder.must(multiMatchQuery); + // 3.2. 构建 term (filter) + if (Objects.nonNull(type)) { + boolQueryBuilder.filter(f -> f + .term(t -> t + .field(NoteIndex.FIELD_NOTE_TYPE) + .value(type) // .value() 会自动处理 Integer, Long, String 等 + ) + ); + } + BoolQuery boolQuery = boolQueryBuilder.build(); + // --- 4. 构建排序 (Sort) 和 FunctionScore --- + Query finalQuery; + List sortOptions = CollUtil.newArrayList(); + NoteSortTypeEnum noteSortTypeEnum = NoteSortTypeEnum.valueOf(sort); + if (Objects.nonNull(noteSortTypeEnum)) { + // 4.1. CASE 1: 按字段排序 + finalQuery = boolQuery._toQuery(); // 查询主体就是 bool 查询 + + switch (noteSortTypeEnum) { + // 按笔记发布时间降序 + case LATEST -> sortOptions.add(SortOptions.of(s -> s + .field(f -> f.field(NoteIndex.FIELD_NOTE_CREATE_TIME).order(SortOrder.Desc)) + )); + // 按笔记点赞量降序 + case MOST_LIKE -> sortOptions.add(SortOptions.of(s -> s + .field(f -> f.field(NoteIndex.FIELD_NOTE_LIKE_TOTAL).order(SortOrder.Desc)) + )); + // 按评论量降序 + 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 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 .preTags("") .postTags("") ); - // 4. 构建最终的 SearchRequest - SearchRequest searchRequest = SearchRequest.of(r -> r + Highlight highlight = Highlight.of(h -> h.fields(NamedValue.of(NoteIndex.FIELD_NOTE_TITLE, titleHighlight))); + // --- 6. 构建最终的 SearchRequest --- + SearchRequest searchRequest = SearchRequest.of(s -> s .index(NoteIndex.NAME) - .query(functionScoreQuery) // 设置 function_score 查询 - - // 排序:按 _score 降序 - .sort(s -> s.score(d -> d.order(SortOrder.Desc))) - - // 分页 - .from(from) - .size(pageSize) - // 高亮 - .highlight(h -> h - .fields(NamedValue.of(NoteIndex.FIELD_NOTE_TITLE, titleHighlight)) - ) + .query(finalQuery) // 设置查询 + .sort(sortOptions) // 设置排序 + .from(from) // 设置分页 + .size(pageSize) // 设置分页 + .highlight(highlight) // 设置高亮 ); - // 返参 VO 集合 - List searchNoteRspVOS = null; + // --- 7. 执行查询和解析响应 --- + List searchNoteRspVOS = new ArrayList<>(); long total = 0; - try { - log.info("==> NoteSearchRequest: {}", searchRequest.toString()); + log.info("==> SearchRequest: {}", searchRequest.toString()); - // ⭐️ 执行查询请求,并自动反序列化文档源到 SearchNoteRspVO - SearchResponse searchResponse = - client.search(searchRequest, SearchNoteRspVO.class); - - total = searchResponse.hits().total() != null ? searchResponse.hits().total().value() : 0; + // 执行查询请求 + SearchResponse searchResponse = client.search(searchRequest, SearchNoteRspVO.class); + if (searchResponse.hits().total() != null) { + total = searchResponse.hits().total().value(); + } log.info("==> 命中文档总数, hits: {}", total); + List> hits = searchResponse.hits().hits(); + for (Hit hit : hits) { + // 获取source + SearchNoteRspVO source = hit.source(); + // 7.3. 获取高亮字段 + String highlightedTitle = null; + Map> 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) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - } catch (Exception e) { - log.error("==> 查询 Elasticsearch 异常: ", e); + } + } + } catch (IOException e) { + log.error("==> 搜索笔记异常: {}", e.getMessage()); } - return PageResponse.success(searchNoteRspVOS, pageNo, total); } - /** - * 辅助方法:处理 Hit,合并 Source 和 Highlight 数据 - */ - private SearchNoteRspVO processNoteHit(Hit hit) { - SearchNoteRspVO rspVO = hit.source(); - - if (rspVO == null) { - return null; - } - - // 2. ⭐️ 处理高亮字段 - Map> highlights = hit.highlight(); - - if (CollUtil.isNotEmpty(highlights)) { - List 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; - } }