> 技术文档 > 【Easylive】Elasticsearch搜索组件详解

【Easylive】Elasticsearch搜索组件详解

【Easylive】项目常见问题解答(自用&持续更新中…) 汇总版

一、Elasticsearch基础介绍

Elasticsearch(简称ES)是一个分布式、RESTful风格的搜索和分析引擎,基于Apache Lucene构建。在视频平台中,它主要用于:

  1. 全文搜索:快速检索视频标题、标签等内容
  2. 结构化查询:支持多种条件组合查询
  3. 高亮显示:突出显示匹配的关键词
  4. 聚合统计:对播放量、弹幕数等进行统计分析

核心特性

近实时搜索:数据变更后1秒内可搜索
分布式架构:支持水平扩展
丰富的API:RESTful接口和多种客户端
强大的查询DSL:灵活的查询语法

二、组件配置解析

@Value(\"${es.host.port:127.0.0.1:9200}\")private String esHostPort;@Value(\"${es.index.video.name:easylive_video}\")private String esIndexVideoName;

esHostPort:ES服务器地址,默认本地9200端口
esIndexVideoName:视频索引名称,默认\"easylive_video\"

三、核心方法详解

1. 索引初始化方法 createIndex()

public void createIndex() { try { // 检查索引是否存在 Boolean existIndex = isExistIndex(); if (existIndex) { return; } // 创建索引请求 CreateIndexRequest request = new CreateIndexRequest(appConfig.getEsIndexVideoName()); // 设置分析器(处理逗号分隔的标签) request.settings(\"{\\\"analysis\\\": {\\\"analyzer\\\": {\\\"comma\\\": {\\\"type\\\": \\\"pattern\\\",\\\"pattern\\\": \\\",\\\"}}}}\", XContentType.JSON); // 定义字段映射 request.mapping(\"{\\\"properties\\\": {...}}\", XContentType.JSON); // 执行创建 CreateIndexResponse response = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT); if (!response.isAcknowledged()) { throw new BusinessException(\"初始化es失败\"); } } catch (Exception e) { log.error(\"初始化es失败\", e); throw new BusinessException(\"初始化es失败\"); }}

字段映射说明
videoId/userId:仅存储不索引
videoName:使用ik中文分词器
tags:使用自定义逗号分析器
• 数值/日期字段:仅存储不索引

2. 文档操作方法

(1) 保存文档 saveDoc()
public void saveDoc(VideoInfo videoInfo) { try { // 存在则更新,不存在则新增 if (docExist(videoInfo.getVideoId())) { updateDoc(videoInfo); } else { VideoInfoEsDto dto = CopyTools.copy(videoInfo, VideoInfoEsDto.class); // 初始化统计字段 dto.setCollectCount(0); dto.setPlayCount(0); dto.setDanmuCount(0); IndexRequest request = new IndexRequest(appConfig.getEsIndexVideoName()); request.id(videoInfo.getVideoId())  .source(JsonUtils.convertObj2Json(dto), XContentType.JSON); restHighLevelClient.index(request, RequestOptions.DEFAULT); } } catch (Exception e) { log.error(\"新增视频到es失败\", e); throw new BusinessException(\"保存失败\"); }}
(2) 更新文档 updateDoc()
private void updateDoc(VideoInfo videoInfo) { try { // 排除时间字段 videoInfo.setLastUpdateTime(null); videoInfo.setCreateTime(null); // 反射获取非空字段 Map<String, Object> dataMap = new HashMap<>(); Field[] fields = videoInfo.getClass().getDeclaredFields(); for (Field field : fields) { Method getter = videoInfo.getClass().getMethod(\"get\" + StringTools.upperCaseFirstLetter(field.getName())); Object value = getter.invoke(videoInfo); if (value != null && !(value instanceof String && ((String)value).isEmpty())) { dataMap.put(field.getName(), value); } } if (!dataMap.isEmpty()) { UpdateRequest updateRequest = new UpdateRequest(appConfig.getEsIndexVideoName(), videoInfo.getVideoId()); updateRequest.doc(dataMap); restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT); } } catch (Exception e) { log.error(\"更新视频到es失败\", e); throw new BusinessException(\"保存失败\"); }}
(3) 更新统计字段 updateDocCount()
public void updateDocCount(String videoId, String fieldName, Integer count) { try { // 使用painless脚本实现原子递增 UpdateRequest updateRequest = new UpdateRequest(appConfig.getEsIndexVideoName(), videoId); 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(\"更新数量到es失败\", e); throw new BusinessException(\"保存失败\"); }}
(4) 删除文档 delDoc()
public void delDoc(String videoId) { try { DeleteRequest deleteRequest = new DeleteRequest(appConfig.getEsIndexVideoName(), videoId); restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT); } catch (Exception e) { log.error(\"从es删除视频失败\", e); throw new BusinessException(\"删除视频失败\"); }}

3. 搜索方法 search()

public PaginationResultVO<VideoInfo> search(Boolean highlight, String keyword, Integer orderType, Integer pageNo, Integer pageSize) { try { // 1. 构建搜索请求 SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); // 2. 设置查询条件(多字段匹配) sourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, \"videoName\", \"tags\")); // 3. 设置高亮 if (highlight) { HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field(\"videoName\") .preTags(\"\") .postTags(\"\"); sourceBuilder.highlighter(highlightBuilder); } // 4. 设置排序 SearchOrderTypeEnum orderEnum = SearchOrderTypeEnum.getByType(orderType); if (orderType != null) { sourceBuilder.sort(orderEnum.getField(), SortOrder.DESC); } else { sourceBuilder.sort(\"_score\", SortOrder.DESC); // 默认按相关度排序 } // 5. 设置分页 pageNo = pageNo == null ? 1 : pageNo; pageSize = pageSize == null ? PageSize.SIZE20.getSize() : pageSize; sourceBuilder.from((pageNo - 1) * pageSize).size(pageSize); // 6. 执行查询 SearchRequest searchRequest = new SearchRequest(appConfig.getEsIndexVideoName()); searchRequest.source(sourceBuilder); SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 7. 处理结果 SearchHits hits = response.getHits(); List<VideoInfo> videoList = Arrays.stream(hits.getHits()) .map(hit -> { VideoInfo info = JsonUtils.convertJson2Obj(hit.getSourceAsString(), VideoInfo.class); // 应用高亮 if (hit.getHighlightFields().get(\"videoName\") != null) {  info.setVideoName(hit.getHighlightFields().get(\"videoName\").getFragments()[0].string()); } return info; }) .collect(Collectors.toList()); // 8. 补充用户信息 Map<String, UserInfo> userMap = userInfoMapper.selectList( new UserInfoQuery().setUserIdList( videoList.stream().map(VideoInfo::getUserId).collect(Collectors.toList()) ) ).stream().collect(Collectors.toMap(UserInfo::getUserId, Function.identity())); videoList.forEach(video -> { UserInfo user = userMap.get(video.getUserId()); if (user != null) video.setNickName(user.getNickName()); }); // 9. 返回分页结果 return new PaginationResultVO<>( (int)hits.getTotalHits().value, pageSize, pageNo, (int)Math.ceil((double)hits.getTotalHits().value / pageSize), videoList ); } catch (Exception e) { log.error(\"查询视频失败\", e); throw new BusinessException(\"查询失败\"); }}

四、设计亮点分析

  1. 优雅的异常处理
    • 统一捕获异常并转换为业务异常
    • 记录详细错误日志

  2. 智能的文档操作
    • 自动判断文档存在性
    • 增量更新非空字段

  3. 高效的统计更新
    • 使用painless脚本实现原子操作

  4. 完整的分页支持
    • 支持自定义页码和大小
    • 返回总页数等元信息

  5. 关联数据补充
    • 搜索后批量查询用户信息
    • 减少N+1查询问题

五、潜在优化建议

  1. 批量操作支持

    BulkRequest bulkRequest = new BulkRequest();// 添加多个操作restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
  2. 缓存用户信息
    • 使用Redis缓存频繁访问的用户数据

  3. 搜索建议功能

    SearchSourceBuilder.suggest(new SuggestBuilder() .addSuggestion(\"video-suggest\", SuggestBuilders.completionSuggestion(\"videoName.suggest\")));
  4. 更复杂的高亮策略
    • 支持多字段高亮
    • 自定义高亮片段长度

  5. 索引别名支持
    • 使用别名实现零停机索引重建

六、初始化流程(InitRun)

@Component(\"initRun\")public class InitRun implements ApplicationRunner { @Override public void run(ApplicationArguments args) { // 1. 测试数据库连接 try (Connection conn = dataSource.getConnection()) { // 2. 测试Redis连接 redisUtils.get(\"test\"); // 3. 初始化ES索引 esSearchComponent.createIndex(); logger.info(\"服务启动成功\"); } catch (Exception e) { logger.error(\"服务启动失败\", e); System.exit(0); // 启动失败直接退出 } }}

启动验证逻辑

  1. 数据库连接测试
  2. Redis连通性测试
  3. ES索引初始化
  4. 任一失败则终止应用启动

这个ES搜索组件为视频平台提供了完整、高效的搜索能力,从基础索引管理到复杂的搜索功能都有良好实现,是系统核心功能的重要支撑。