一、导入依赖
<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 {@Resourceprivate AppConfig appConfig;@Resourceprivate 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 {@Resourceprivate ElasticSearchComponent elasticSearchComponent;@Overridepublic void run(ApplicationArguments args) throws Exception {elasticSearchComponent.createIndex();}
}
五、使用
如果,我们需要使用elasticSearchComponent里面的功能。我们只需要先注入elasticSearchComponent ,然后直接调用里面的方法就行了。
比如说,搜索功能:
@Slf4j
@RestController
@RequestMapping("/search")
public class SearchController {@Autowiredprivate ElasticSearchComponent elasticSearchComponent;@GetMappingpublic 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;}
}
以上功能,我的项目中是可以用的。如果有什么地方不对的地方,谢谢大家的指教。