feat(search): 实现用户搜索昵称高亮与粉丝数格式化- 添加昵称高亮字段 highlightNickname 到 SearchUserRspVO

- 修改粉丝总数字段类型为 String,支持格式化显示
- 引入 NumberUtils 工具类,实现数字转“万”单位格式
- 配置 Elasticsearch 查询高亮规则,支持昵称关键词高亮
- 新增 mergeHitToRspVO 方法,合并原始数据与高亮结果
- 优化搜索请求构建逻辑,增强可读性与扩展性
This commit is contained in:
2025-11-01 13:50:18 +08:00
parent 4e00542371
commit 4b13e52a29
4 changed files with 140 additions and 42 deletions

View File

@@ -25,6 +25,11 @@ public class SearchUserRspVO {
*/
private String nickname;
/**
* 昵称:关键词高亮
*/
private String highlightNickname;
/**
* 头像
*/
@@ -46,6 +51,6 @@ public class SearchUserRspVO {
* 粉丝总数
*/
@JsonProperty("fans_total")
private Integer fansTotal;
private String fansTotal;
}

View File

@@ -5,9 +5,12 @@ import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.TextQueryType;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.HighlightField;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.util.NamedValue;
import com.google.common.collect.Lists;
import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.framework.common.utils.NumberUtils;
import com.hanserwei.hannote.search.index.UserIndex;
import com.hanserwei.hannote.search.model.vo.SearchUserReqVO;
import com.hanserwei.hannote.search.model.vo.SearchUserRspVO;
@@ -17,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@@ -25,80 +29,130 @@ public class UserServiceImpl implements UserService {
@Resource
private ElasticsearchClient client;
/**
* 获取 SearchUserRspVO
*
* @param hit 搜索结果
* @return SearchUserRspVO
*/
private static SearchUserRspVO getSearchUserRspVO(Hit<SearchUserRspVO> hit) {
SearchUserRspVO searchUserRspVO = new SearchUserRspVO();
SearchUserRspVO source = hit.source();
if (source != null) {
searchUserRspVO.setUserId(source.getUserId());
searchUserRspVO.setNickname(source.getNickname());
searchUserRspVO.setAvatar(source.getAvatar());
searchUserRspVO.setHanNoteId(source.getHanNoteId());
searchUserRspVO.setNoteTotal(source.getNoteTotal());
searchUserRspVO.setFansTotal(source.getFansTotal());
}
return searchUserRspVO;
}
@Override
public PageResponse<SearchUserRspVO> searchUser(SearchUserReqVO searchUserReqVO) {
// 查询关键
// 查询关键
String keyword = searchUserReqVO.getKeyword();
// 当前页码
Integer pageNo = searchUserReqVO.getPageNo();
int pageSize = 10;
int pageSize = 10; // 每页展示数据量
int from = (pageNo - 1) * pageSize; // 偏移量
// 构建SearchRequest指定索引
SearchRequest searchRequest = new SearchRequest.Builder()
HighlightField nicknameHighlight = HighlightField.of(hf -> hf
.preTags("<strong>")
.postTags("</strong>")
);
SearchRequest searchRequest = SearchRequest.of(r -> r
.index(UserIndex.NAME)
.query(query -> query
.multiMatch(multiMatch -> multiMatch
// 1. 构建 Query: multiMatchQuery (RHL 风格的匹配)
.query(q -> q
.multiMatch(m -> m
.query(keyword)
.fields(UserIndex.FIELD_USER_NICKNAME, UserIndex.FIELD_USER_HAN_NOTE_ID)
.type(TextQueryType.PhrasePrefix)))
.sort(sort -> sort
.field(filedSort -> filedSort.field(UserIndex.FIELD_USER_FANS_TOTAL).order(SortOrder.Desc)))
.from((pageNo - 1) * pageSize)
// 默认使用 MatchQuery 行为,如果要模糊匹配,请添加 .fuzziness("AUTO")
.type(TextQueryType.PhrasePrefix)
)
)
// 2. 构建 Sort
.sort(s -> s
.field(f -> f
.field(UserIndex.FIELD_USER_FANS_TOTAL)
.order(SortOrder.Desc)
)
)
.highlight(h -> h.fields(NamedValue.of(UserIndex.FIELD_USER_NICKNAME, nicknameHighlight)))
// 3. 分页 from 和 size
.from(from)
.size(pageSize)
.build();
);
// 返参 VO 集合
List<SearchUserRspVO> searchUserRspVOS = null;
List<SearchUserRspVO> searchUserRspVOS = Lists.newArrayList();
// 总文档数,默认为 0
long total = 0;
try {
log.info("==> SearchRequest:{}", searchRequest);
log.info("==> SearchRequest: {}", searchRequest.toString());
// 执行查询请求
SearchResponse<SearchUserRspVO> searchResponse = client.search(searchRequest, SearchUserRspVO.class);
searchUserRspVOS = Lists.newArrayList();
// 处理搜索结果
List<Hit<SearchUserRspVO>> hits = searchResponse.hits().hits();
if (searchResponse.hits().total() != null) {
total = searchResponse.hits().total().value();
}
searchUserRspVOS = Lists.newArrayList();
for (Hit<SearchUserRspVO> hit : hits) {
log.info("==> 文档数据: {}", hit.toString());
if (hit.source() != null) {
SearchUserRspVO searchUserRspVO = getSearchUserRspVO(hit);
// 1. 获取原始文档数据 (source)
SearchUserRspVO source = hit.source();
// 2. 获取高亮数据 (highlight)
Map<String, List<String>> highlights = hit.highlight();
if (source != null) {
// 3. 调用辅助方法合并数据和高亮
SearchUserRspVO searchUserRspVO = mergeHitToRspVO(source, highlights);
searchUserRspVOS.add(searchUserRspVO);
}
}
} catch (Exception e) {
log.error("==> 查询 Elasticsearch 异常: ", e);
}
return PageResponse.success(searchUserRspVOS, pageNo, total);
}
/**
* 将原始文档和高亮数据合并到 SearchUserRspVO
*
* @param source 原始文档数据 (已自动反序列化)
* @param highlights 高亮数据 Map
* @return SearchUserRspVO
*/
private SearchUserRspVO mergeHitToRspVO(SearchUserRspVO source, Map<String, List<String>> highlights) {
if (source == null) {
return null;
}
// 1. 复制原始文档字段 (假设 SearchUserRspVO 使用 Lombok @Data 或 Builder)
SearchUserRspVO searchUserRspVO = SearchUserRspVO.builder()
.userId(source.getUserId())
.nickname(source.getNickname())
.avatar(source.getAvatar())
.hanNoteId(source.getHanNoteId()) // 字段名应与您的 VO 保持一致
.noteTotal(source.getNoteTotal())
.build();
if (source.getFansTotal() != null) {
searchUserRspVO.setFansTotal(NumberUtils.formatNumberString(Long.parseLong(source.getFansTotal())));
}
// 2. ⭐️ 核心逻辑:处理并设置高亮字段
if (highlights != null) {
// 尝试从 highlights Map 中获取 nickname 字段的高亮结果
List<String> nicknameHighlights = highlights.get(UserIndex.FIELD_USER_NICKNAME);
if (nicknameHighlights != null && !nicknameHighlights.isEmpty()) {
searchUserRspVO.setHighlightNickname(nicknameHighlights.getFirst());
}
}
// 3. 如果高亮字段为空,默认使用原始 nickname
if (searchUserRspVO.getHighlightNickname() == null) {
searchUserRspVO.setHighlightNickname(source.getNickname());
}
return searchUserRspVO;
}
}

View File

@@ -0,0 +1,39 @@
package com.hanserwei.framework.common.utils;
import java.math.RoundingMode;
import java.text.DecimalFormat;
public class NumberUtils {
/**
* 数字转换字符串
*
* @param number 数字
* @return 字符串
*/
public static String formatNumberString(long number) {
if (number < 10000) {
return String.valueOf(number); // 小于 1 万显示原始数字
} else if (number < 100000000) {
// 小于 1 亿,显示万单位
double result = number / 10000.0;
DecimalFormat df = new DecimalFormat("#.#"); // 保留 1 位小数
df.setRoundingMode(RoundingMode.DOWN); // 禁用四舍五入
String formatted = df.format(result);
return formatted + "";
} else {
return "9999万"; // 超过 1 亿,统一显示 9999万
}
}
public static void main(String[] args) {
// 测试
System.out.println(formatNumberString(1000)); // 1000
System.out.println(formatNumberString(11130)); // 1.1万
System.out.println(formatNumberString(26719300)); // 2671.9万
System.out.println(formatNumberString(10000000)); // 1000万
System.out.println(formatNumberString(999999)); // 99.9万
System.out.println(formatNumberString(150000000)); // 超过一亿展示9999万
System.out.println(formatNumberString(99999)); // 9.9万
}
}

View File

@@ -285,6 +285,6 @@ POST http://localhost:8092/search/user
Content-Type: application/json
{
"keyword": "憨",
"keyword": "憨",
"pageNo": 1
}