一、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>7.12.1</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.12.1</version>
</dependency>
二、编写实体类
在实现方法前,我们需要有一个和存入ES索引库里数据字段对应的实体类,方便从数据库里查询的对应字段的数据,并存入到ES中。
@Data
public class EsItemInfoDoc implements Serializable {
private String itemId;
private String title;
private String cover;
private String userId;
private String nickname;
private String avatar;
private String categoryId;
private String boardName;
private Integer viewCount;
private Integer goodCount;
private Integer commentCount;
private Integer collectCount;
private String createTime;
}
三、编写ES工具
在component文件夹下,创建一个ElasticSearchComponent.java 的组件类,提供了索引创建、文档保存、更新、删除以及搜索等功能,具体如下:
1.索引相关操作
isExistsIndex
方法用于检查指定名称(通过AppConfig
获取)的索引是否存在。createIndex
方法首先调用isExistsIndex
判断索引是否已存在,若不存在则创建索引,创建时使用了硬编码的MAPPING_TEMPLATE
字符串来定义索引的映射结构,若创建成功或索引已存在会记录相应日志,创建失败则抛出异常。
2.文档操作相关
saveEsItemInfoDoc
方法根据传入的EsItemInfoDoc
对象的itemId
判断文档是否存在,若存在则调用updateEsItemInfoDoc
方法更新文档,不存在则将其保存到 Elasticsearch 中。docExist
方法通过 Elasticsearch 的GetRequest
和GetResponse
判断指定docId
的文档是否存在。updateEsItemInfoDoc
方法用于更新已存在的文档,先将文档的createTime
字段设为null
,然后通过反射获取文档对象的非空字段及对应值,构建Map
后使用UpdateRequest
更新文档,若要更新的数据为空则直接返回。updateEsDocFieldCount
方法用于更新指定文档(通过docId
指定)中某个字段(通过fieldName
指定)的计数值(通过count
指定),通过编写Script
脚本实现字段值的增量更新操作。deleteEsItemInfoDoc
方法用于删除指定itemId
的文档,通过DeleteRequest
向 Elasticsearch 发起删除请求。
3.搜索功能
search
方法实现了根据关键词、排序类型、页码、每页大小等条件进行搜索的功能。它先构建 SearchSourceBuilder
,设置查询条件(使用 multiMatchQuery
对多个字段进行关键词匹配查询),根据传入参数决定是否添加高亮显示、设置排序规则以及分页信息,然后执行搜索请求并解析返回结果,将搜索到的文档数据转换为 EsItemInfoDoc
列表,最后封装成 PageResult
对象返回。
完整代码
@Component("elasticSearchComponent")
@Slf4j
public class ElasticSearchComponent {
@Resource
private AppConfig appConfig;
@Resource
private RestHighLevelClient restHighLevelClient;
private Boolean isExistsIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest(appConfig.getEsIndexName());
return restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
}
public void createIndex() throws Exception {
try {
if (isExistsIndex()) {
return;
}
CreateIndexRequest request = new CreateIndexRequest(appConfig.getEsIndexName());
//request.settings(MAPPING_TEMPLATE, XContentType.JSON);
request.source(MAPPING_TEMPLATE, XContentType.JSON);
CreateIndexResponse response = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
boolean acknowledged = response.isAcknowledged();
if (!acknowledged) {
throw new BaseException("创建索引失败");
} else {
log.info("创建索引成功");
}
} catch (Exception e) {
log.error("创建索引失败", e);
throw new BaseException("创建索引失败");
}
}
private static final String MAPPING_TEMPLATE = """
{
"mappings": {
"properties": {
"itemId": {
"type": "keyword",
"index": false
},
"title":{
"type": "text",
"analyzer": "ik_max_word"
},
"cover": {
"type": "text",
"index": false
},
"userId":{
"type": "keyword",
"index": false
},
"nickname":{
"type": "text",
"analyzer": "ik_max_word"
},
"avatar": {
"type": "text",
"index": false
},
"categoryId":{
"type": "keyword",
"index": false
},
"boardName":{
"type": "text",
"analyzer": "ik_max_word"
},
"viewCount":{
"type": "integer",
"index": false
},
"commentCount":{
"type": "integer",
"index": false
},
"goodCount":{
"type": "integer",
"index": false
},
"collectCount":{
"type": "integer",
"index": false
},
"createTime":{
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis",
"index": false
},
"database":{
"type": "keyword",
"index": false
}
}
}
}""";
public void saveEsItemInfoDoc(EsItemInfoDoc esItemInfoDoc) {
try {
if (docExist(esItemInfoDoc.getItemId())) {
log.info("esItemInfoDoc已存在,更新esItemInfoDoc");
updateEsItemInfoDoc(esItemInfoDoc);
} else {
log.info("esItemInfoDoc不存在,保存esItemInfoDoc");
IndexRequest request = new IndexRequest(appConfig.getEsIndexName())
.id(esItemInfoDoc.getItemId())
.source(JSONUtil.toJsonStr(esItemInfoDoc), XContentType.JSON);
restHighLevelClient.index(request, RequestOptions.DEFAULT);
}
} catch (Exception e) {
log.error("保存esItemInfoDoc失败", e);
throw new RuntimeException("保存esItemInfoDoc失败", e);
}
}
private Boolean docExist(String docId) throws IOException {
GetRequest request = new GetRequest(appConfig.getEsIndexName(), docId);
GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
return response.isExists();
}
private void updateEsItemInfoDoc(EsItemInfoDoc esItemInfoDoc) {
try {
esItemInfoDoc.setCreateTime(null);
Map<String, Object> dataMap = new HashMap<>();
Field[] fields = esItemInfoDoc.getClass().getDeclaredFields();
for (Field field : fields) {
String methodName = "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
Method method = esItemInfoDoc.getClass().getMethod(methodName);
Object object = method.invoke(esItemInfoDoc);
if (object != null && object instanceof String && !object.toString().isEmpty()
|| object != null && !(object instanceof String)) {
dataMap.put(field.getName(), object);
}
}
if (dataMap.isEmpty()) {
return;
}
UpdateRequest updateRequest = new UpdateRequest(appConfig.getEsIndexName(), esItemInfoDoc.getItemId());
updateRequest.doc(dataMap);
restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("更新esItemInfoDoc失败", e);
throw new RuntimeException("更新esItemInfoDoc失败", e);
}
}
public void updateEsDocFieldCount(String docId, String fieldName, Integer count) {
try {
UpdateRequest updateRequest = new UpdateRequest(appConfig.getEsIndexName(), docId);
// 创建Script对象,用于定义更新文档字段的脚本逻辑
Script script = new Script(ScriptType.INLINE, "painless", "ctx._source."
+ fieldName + " += params.count", Collections.singletonMap("count", count));
// 将脚本设置到更新请求中
updateRequest.script(script);
restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("更新数量到esDocFieldCount失败", e);
throw new RuntimeException("更新esDocFieldCount失败", e);
}
}
public void deleteEsItemInfoDoc(String itemId) {
try {
DeleteRequest request = new DeleteRequest(appConfig.getEsIndexName(), itemId);
restHighLevelClient.delete(request, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("删除esItemInfoDoc失败", e);
throw new RuntimeException("删除esItemInfoDoc失败", e);
}
}
public PageResult<EsItemInfoDoc> search(Boolean highLight, String keyword, Integer orderType, Integer pageNo, Integer pageSize) {
try {
//创建搜索请求
SearchOrderTypeEnum searchOrderTypeEnum = SearchOrderTypeEnum.getOrderType(orderType);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, "title", "boardName", "nickname"));
//高亮
if (highLight) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title");
highlightBuilder.field("boardName");
highlightBuilder.field("nickname");
highlightBuilder.preTags("<span style='color:red'>");
highlightBuilder.postTags("</span>");
searchSourceBuilder.highlighter(highlightBuilder);
}
//排序
searchSourceBuilder.sort("_score", SortOrder.ASC);
if (orderType != null) {
searchSourceBuilder.sort(searchOrderTypeEnum.getField(), SortOrder.DESC);
}
//分页
pageNo = pageNo == null ? 1 : pageNo;
pageSize = pageSize == null ? 24 : pageSize;
searchSourceBuilder.size(pageSize);
searchSourceBuilder.from((pageNo - 1) * pageSize);
//执行搜索
SearchRequest searchRequest = new SearchRequest(appConfig.getEsIndexName());
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//解析结果
SearchHits searchHits = searchResponse.getHits();
Integer totalCount = (int) searchHits.getTotalHits().value;
List<EsItemInfoDoc> esItemInfoDocList = new ArrayList<>();
for (SearchHit hit : searchHits.getHits()) {
EsItemInfoDoc esItemInfoDoc = JSONUtil.toBean(hit.getSourceAsString(), EsItemInfoDoc.class);
if (highLight) {
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField title = highlightFields.get("title");
HighlightField boardName = highlightFields.get("boardName");
HighlightField nickname = highlightFields.get("nickname");
if (title != null) {
esItemInfoDoc.setTitle(title.getFragments()[0].toString());
}
if (boardName != null) {
esItemInfoDoc.setBoardName(boardName.getFragments()[0].toString());
}
if (nickname != null) {
esItemInfoDoc.setNickname(nickname.getFragments()[0].toString());
}
}
esItemInfoDocList.add(esItemInfoDoc);
}
SimplePage page = new SimplePage(pageNo, pageSize, totalCount);
PageResult<EsItemInfoDoc> pageResult = new PageResult<>(totalCount,page.getPageSize(),page.getPageNo(),page.getPageTotal(),esItemInfoDocList);
return pageResult;
} catch (Exception e) {
log.error("搜索失败", e);
throw new RuntimeException("搜索失败", e);
}
}
}
四、创建初始化工具类
由于考虑到迁移性,我们可以定义一个初始化的工具类,让程序每次运行的时候调用elasticSearchComponent中创建索引的方法进行索引的创建,避免程序在一个新的环境运行时,ES中没有对应的索引库。
@Component
public class InitRun implements ApplicationRunner {
@Resource
private ElasticSearchComponent elasticSearchComponent;
@Override
public void run(ApplicationArguments args) throws Exception {
elasticSearchComponent.createIndex();
}
}
五、使用
如果,我们需要使用elasticSearchComponent里面的功能。我们只需要先注入elasticSearchComponent ,然后直接调用里面的方法就行了。
比如说,搜索功能:
@Slf4j
@RestController
@RequestMapping("/search")
public class SearchController {
@Autowired
private ElasticSearchComponent elasticSearchComponent;
@GetMapping
public Result search(@RequestParam String keyword,
@RequestParam Integer orderType,
@RequestParam Integer pageNo,
@RequestParam Integer pageSize) {
log.info("搜索词:{}, 排序方式:{}, 页码:{}, 页大小:{}", keyword, orderType, pageNo, pageSize);
//从es中搜索
PageResult<EsItemInfoDoc> pageResult = elasticSearchComponent.search(true, keyword, orderType, pageNo, pageSize);
return Result.success(pageResult);
}
为了大家可以使用,案例中涉及到的其他代码,如下:
分页结果代码
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer totalCount;
private Integer pageSize;
private Integer pageNo;
private Integer pageTotal;
private List<T> list = new ArrayList<T>();
}
ES搜索中分页数据填充代码
@Data
public class SimplePage {
private int pageNo;
private int pageSize;
private int countTotal;
private int pageTotal;
private int start;
private int end;
public SimplePage() {
}
public SimplePage(Integer pageNo, int pageSize, int countTotal) {
if (pageNo == null) {
pageNo = 0;
}
this.pageNo = pageNo;
this.pageSize = pageSize;
this.countTotal = countTotal;
action();
}
public SimplePage(int start, int end) {
this.start = start;
this.end = end;
}
public void action() {
if (this.pageNo <= 0) {
this.pageNo = 12;
}
if (this.countTotal >= 0) {
this.pageTotal = this.countTotal % this.pageSize == 0 ? this.countTotal / this.pageSize
: this.countTotal / this.pageSize + 1;
} else {
pageTotal = 1;
}
if (pageNo <= 1) {
pageNo = 1;
}
if (pageNo > pageTotal) {
pageNo = pageTotal;
}
this.start = (pageNo - 1) * pageSize;
this.end = this.pageSize;
}
}
以上功能,我的项目中是可以用的。如果有什么地方不对的地方,谢谢大家的指教。