feat(search): 实现用户搜索昵称高亮与粉丝数格式化- 添加昵称高亮字段 highlightNickname 到 SearchUserRspVO
- 修改粉丝总数字段类型为 String,支持格式化显示 - 引入 NumberUtils 工具类,实现数字转“万”单位格式 - 配置 Elasticsearch 查询高亮规则,支持昵称关键词高亮 - 新增 mergeHitToRspVO 方法,合并原始数据与高亮结果 - 优化搜索请求构建逻辑,增强可读性与扩展性
This commit is contained in:
@@ -25,6 +25,11 @@ public class SearchUserRspVO {
|
|||||||
*/
|
*/
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 昵称:关键词高亮
|
||||||
|
*/
|
||||||
|
private String highlightNickname;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 头像
|
* 头像
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +51,6 @@ public class SearchUserRspVO {
|
|||||||
* 粉丝总数
|
* 粉丝总数
|
||||||
*/
|
*/
|
||||||
@JsonProperty("fans_total")
|
@JsonProperty("fans_total")
|
||||||
private Integer fansTotal;
|
private String fansTotal;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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._types.query_dsl.TextQueryType;
|
||||||
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.HighlightField;
|
||||||
import co.elastic.clients.elasticsearch.core.search.Hit;
|
import co.elastic.clients.elasticsearch.core.search.Hit;
|
||||||
|
import co.elastic.clients.util.NamedValue;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.hanserwei.framework.common.response.PageResponse;
|
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.index.UserIndex;
|
||||||
import com.hanserwei.hannote.search.model.vo.SearchUserReqVO;
|
import com.hanserwei.hannote.search.model.vo.SearchUserReqVO;
|
||||||
import com.hanserwei.hannote.search.model.vo.SearchUserRspVO;
|
import com.hanserwei.hannote.search.model.vo.SearchUserRspVO;
|
||||||
@@ -17,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@@ -25,80 +29,130 @@ public class UserServiceImpl implements UserService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ElasticsearchClient client;
|
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
|
@Override
|
||||||
public PageResponse<SearchUserRspVO> searchUser(SearchUserReqVO searchUserReqVO) {
|
public PageResponse<SearchUserRspVO> searchUser(SearchUserReqVO searchUserReqVO) {
|
||||||
// 查询关键字
|
// 查询关键词
|
||||||
String keyword = searchUserReqVO.getKeyword();
|
String keyword = searchUserReqVO.getKeyword();
|
||||||
// 当前页码
|
// 当前页码
|
||||||
Integer pageNo = searchUserReqVO.getPageNo();
|
Integer pageNo = searchUserReqVO.getPageNo();
|
||||||
|
|
||||||
int pageSize = 10;
|
int pageSize = 10; // 每页展示数据量
|
||||||
|
int from = (pageNo - 1) * pageSize; // 偏移量
|
||||||
|
|
||||||
// 构建SearchRequest,指定索引
|
HighlightField nicknameHighlight = HighlightField.of(hf -> hf
|
||||||
SearchRequest searchRequest = new SearchRequest.Builder()
|
.preTags("<strong>")
|
||||||
|
.postTags("</strong>")
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
SearchRequest searchRequest = SearchRequest.of(r -> r
|
||||||
.index(UserIndex.NAME)
|
.index(UserIndex.NAME)
|
||||||
.query(query -> query
|
|
||||||
.multiMatch(multiMatch -> multiMatch
|
// 1. 构建 Query: multiMatchQuery (RHL 风格的匹配)
|
||||||
|
.query(q -> q
|
||||||
|
.multiMatch(m -> m
|
||||||
.query(keyword)
|
.query(keyword)
|
||||||
.fields(UserIndex.FIELD_USER_NICKNAME, UserIndex.FIELD_USER_HAN_NOTE_ID)
|
.fields(UserIndex.FIELD_USER_NICKNAME, UserIndex.FIELD_USER_HAN_NOTE_ID)
|
||||||
.type(TextQueryType.PhrasePrefix)))
|
// 默认使用 MatchQuery 行为,如果要模糊匹配,请添加 .fuzziness("AUTO")
|
||||||
.sort(sort -> sort
|
.type(TextQueryType.PhrasePrefix)
|
||||||
.field(filedSort -> filedSort.field(UserIndex.FIELD_USER_FANS_TOTAL).order(SortOrder.Desc)))
|
)
|
||||||
.from((pageNo - 1) * pageSize)
|
)
|
||||||
|
|
||||||
|
// 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)
|
.size(pageSize)
|
||||||
.build();
|
);
|
||||||
|
|
||||||
|
|
||||||
// 返参 VO 集合
|
// 返参 VO 集合
|
||||||
List<SearchUserRspVO> searchUserRspVOS = null;
|
List<SearchUserRspVO> searchUserRspVOS = Lists.newArrayList();
|
||||||
// 总文档数,默认为 0
|
// 总文档数,默认为 0
|
||||||
long total = 0;
|
long total = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info("==> SearchRequest:{}", searchRequest);
|
log.info("==> SearchRequest: {}", searchRequest.toString());
|
||||||
|
|
||||||
// 执行查询请求
|
// 执行查询请求
|
||||||
SearchResponse<SearchUserRspVO> searchResponse = client.search(searchRequest, SearchUserRspVO.class);
|
SearchResponse<SearchUserRspVO> searchResponse = client.search(searchRequest, SearchUserRspVO.class);
|
||||||
|
|
||||||
searchUserRspVOS = Lists.newArrayList();
|
|
||||||
|
|
||||||
// 处理搜索结果
|
// 处理搜索结果
|
||||||
List<Hit<SearchUserRspVO>> hits = searchResponse.hits().hits();
|
List<Hit<SearchUserRspVO>> hits = searchResponse.hits().hits();
|
||||||
if (searchResponse.hits().total() != null) {
|
if (searchResponse.hits().total() != null) {
|
||||||
total = searchResponse.hits().total().value();
|
total = searchResponse.hits().total().value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchUserRspVOS = Lists.newArrayList();
|
||||||
|
|
||||||
for (Hit<SearchUserRspVO> hit : hits) {
|
for (Hit<SearchUserRspVO> hit : hits) {
|
||||||
log.info("==> 文档数据: {}", hit.toString());
|
// 1. 获取原始文档数据 (source)
|
||||||
if (hit.source() != null) {
|
SearchUserRspVO source = hit.source();
|
||||||
SearchUserRspVO searchUserRspVO = getSearchUserRspVO(hit);
|
|
||||||
|
// 2. 获取高亮数据 (highlight)
|
||||||
|
Map<String, List<String>> highlights = hit.highlight();
|
||||||
|
|
||||||
|
if (source != null) {
|
||||||
|
// 3. 调用辅助方法合并数据和高亮
|
||||||
|
SearchUserRspVO searchUserRspVO = mergeHitToRspVO(source, highlights);
|
||||||
searchUserRspVOS.add(searchUserRspVO);
|
searchUserRspVOS.add(searchUserRspVO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("==> 查询 Elasticsearch 异常: ", e);
|
log.error("==> 查询 Elasticsearch 异常: ", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PageResponse.success(searchUserRspVOS, pageNo, total);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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万
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -285,6 +285,6 @@ POST http://localhost:8092/search/user
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"keyword": "憨",
|
"keyword": "憨憨",
|
||||||
"pageNo": 1
|
"pageNo": 1
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user