Springboot整合ElasticSearch实现搜索功能

发布于:2024-12-21 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、导入依赖

 <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;
    }
}

以上功能,我的项目中是可以用的。如果有什么地方不对的地方,谢谢大家的指教。