feat(search): 实现用户搜索功能

- 添加 Elasticsearch 客户端配置
- 创建用户搜索接口和实现类
- 定义搜索请求和响应 VO 类
- 集成分页查询和关键字匹配逻辑
- 添加 Elasticsearch 测试用例
- 更新 pom.xml 依赖和版本管理
- 添加 HTTP 客户端测试脚本
This commit is contained in:
2025-10-30 22:21:06 +08:00
parent 3437c2bff4
commit 4e00542371
8 changed files with 209 additions and 16 deletions

View File

@@ -59,6 +59,10 @@
<groupId>co.elastic.clients</groupId> <groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId> <artifactId>elasticsearch-java</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,31 +1,40 @@
package com.hanserwei.hannote.search.config; package com.hanserwei.hannote.search.config;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class ElasticsearchConfig { public class ElasticsearchConfig {
@Value("${elasticsearch.host}") @Value("${elasticsearch.host}")
private String host; private String host;
@Value("${elasticsearch.port}") @Bean
private int port; public ElasticsearchClient elasticsearchClient() {
// 1. 创建底层 RestClient低级客户端
RestClient restClient = RestClient
.builder(HttpHost.create(host))
.build();
@Value("${elasticsearch.scheme:http}") // 2. 创建 JSON 映射器
private String scheme; ObjectMapper mapper = JsonMapper.builder().build();
JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(mapper);
@Value("${elasticsearch.username:}") // 3. 构建传输层
private String username; ElasticsearchTransport transport = new RestClientTransport(restClient, jsonpMapper);
@Value("${elasticsearch.password:}")
private String password;
@Value("${elasticsearch.api-key:}")
private String apiKey;
@Value("${elasticsearch.use-api-key:false}")
private boolean useApiKey;
// 4. 创建高层次的 Elasticsearch 客户端
return new ElasticsearchClient(transport);
}
} }

View File

@@ -0,0 +1,30 @@
package com.hanserwei.hannote.search.controller;
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.hannote.search.model.vo.SearchUserReqVO;
import com.hanserwei.hannote.search.model.vo.SearchUserRspVO;
import com.hanserwei.hannote.search.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/search")
@Slf4j
public class UserController {
@Resource
private UserService userService;
@PostMapping("/user")
@ApiOperationLog(description = "搜索用户")
public PageResponse<SearchUserRspVO> searchUser(@RequestBody @Validated SearchUserReqVO searchUserReqVO) {
return userService.searchUser(searchUserReqVO);
}
}

View File

@@ -1,5 +1,7 @@
package com.hanserwei.hannote.search.model.vo; package com.hanserwei.hannote.search.model.vo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@@ -9,11 +11,13 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@Builder @Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class SearchUserRspVO { public class SearchUserRspVO {
/** /**
* 用户ID * 用户ID
*/ */
@JsonProperty("id")
private Long userId; private Long userId;
/** /**
@@ -29,16 +33,19 @@ public class SearchUserRspVO {
/** /**
* 小憨书ID * 小憨书ID
*/ */
@JsonProperty("han_note_id")
private String hanNoteId; private String hanNoteId;
/** /**
* 笔记发布总数 * 笔记发布总数
*/ */
@JsonProperty("note_total")
private Integer noteTotal; private Integer noteTotal;
/** /**
* 粉丝总数 * 粉丝总数
*/ */
@JsonProperty("fans_total")
private Integer fansTotal; private Integer fansTotal;
} }

View File

@@ -1,14 +1,104 @@
package com.hanserwei.hannote.search.service.impl; package com.hanserwei.hannote.search.service.impl;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
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.Hit;
import com.google.common.collect.Lists;
import com.hanserwei.framework.common.response.PageResponse; import com.hanserwei.framework.common.response.PageResponse;
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;
import com.hanserwei.hannote.search.service.UserService; import com.hanserwei.hannote.search.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class UserServiceImpl implements UserService { 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 @Override
public PageResponse<SearchUserRspVO> searchUser(SearchUserReqVO searchUserReqVO) { public PageResponse<SearchUserRspVO> searchUser(SearchUserReqVO searchUserReqVO) {
return null; // 查询关键字
String keyword = searchUserReqVO.getKeyword();
// 当前页码
Integer pageNo = searchUserReqVO.getPageNo();
int pageSize = 10;
// 构建SearchRequest指定索引
SearchRequest searchRequest = new SearchRequest.Builder()
.index(UserIndex.NAME)
.query(query -> query
.multiMatch(multiMatch -> multiMatch
.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)
.size(pageSize)
.build();
// 返参 VO 集合
List<SearchUserRspVO> searchUserRspVOS = null;
// 总文档数,默认为 0
long total = 0;
try {
log.info("==> SearchRequest:{}", searchRequest);
// 执行查询请求
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();
}
for (Hit<SearchUserRspVO> hit : hits) {
log.info("==> 文档数据: {}", hit.toString());
if (hit.source() != null) {
SearchUserRspVO searchUserRspVO = getSearchUserRspVO(hit);
searchUserRspVOS.add(searchUserRspVO);
}
}
} catch (Exception e) {
log.error("==> 查询 Elasticsearch 异常: ", e);
}
return PageResponse.success(searchUserRspVOS, pageNo, total);
} }
} }

View File

@@ -0,0 +1,38 @@
package com.hanserwei.hannote.search;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import com.hanserwei.hannote.search.model.vo.SearchUserRspVO;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ElasticsearchClientTest {
@Autowired
private ElasticsearchClient client;
@Test
@SneakyThrows
public void testClient() {
SearchResponse<SearchUserRspVO> response = client.search(s -> s
.index("user")
.query(q -> q
.multiMatch(mm -> mm
.query("Han")
.fields("nickname", "han_note_id")
)
)
.sort(so -> so
.field(f -> f.field("fans_total").order(SortOrder.Desc))
)
.from(0)
.size(10),
SearchUserRspVO.class
);
response.hits().hits().forEach(hit -> System.out.println(hit.source()));
}
}

View File

@@ -279,3 +279,12 @@ Content-Type: application/json
} }
} }
} }
### 搜索用户(搜索服务)
POST http://localhost:8092/search/user
Content-Type: application/json
{
"keyword": "憨",
"pageNo": 1
}

View File

@@ -69,6 +69,7 @@
<buffertrigger.version>0.2.21</buffertrigger.version> <buffertrigger.version>0.2.21</buffertrigger.version>
<xxl-job.version>3.2.0</xxl-job.version> <xxl-job.version>3.2.0</xxl-job.version>
<elasticsearch-java.version>9.2.0</elasticsearch-java.version> <elasticsearch-java.version>9.2.0</elasticsearch-java.version>
<elasticsearch.version>9.2.0</elasticsearch.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
@@ -308,6 +309,11 @@
<artifactId>elasticsearch-java</artifactId> <artifactId>elasticsearch-java</artifactId>
<version>${elasticsearch-java.version}</version> <version>${elasticsearch-java.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>