【Easylive】Elasticsearch搜索组件详解
【Easylive】项目常见问题解答(自用&持续更新中…) 汇总版
一、Elasticsearch基础介绍
Elasticsearch(简称ES)是一个分布式、RESTful风格的搜索和分析引擎,基于Apache Lucene构建。在视频平台中,它主要用于:
- 全文搜索:快速检索视频标题、标签等内容
- 结构化查询:支持多种条件组合查询
- 高亮显示:突出显示匹配的关键词
- 聚合统计:对播放量、弹幕数等进行统计分析
核心特性
• 近实时搜索:数据变更后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("<span class='highlight'>")
.postTags("</span>");
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("查询失败");
}
}
四、设计亮点分析
-
优雅的异常处理:
• 统一捕获异常并转换为业务异常
• 记录详细错误日志 -
智能的文档操作:
• 自动判断文档存在性
• 增量更新非空字段 -
高效的统计更新:
• 使用painless脚本实现原子操作 -
完整的分页支持:
• 支持自定义页码和大小
• 返回总页数等元信息 -
关联数据补充:
• 搜索后批量查询用户信息
• 减少N+1查询问题
五、潜在优化建议
-
批量操作支持:
BulkRequest bulkRequest = new BulkRequest(); // 添加多个操作 restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
-
缓存用户信息:
• 使用Redis缓存频繁访问的用户数据 -
搜索建议功能:
SearchSourceBuilder.suggest(new SuggestBuilder() .addSuggestion("video-suggest", SuggestBuilders.completionSuggestion("videoName.suggest")));
-
更复杂的高亮策略:
• 支持多字段高亮
• 自定义高亮片段长度 -
索引别名支持:
• 使用别名实现零停机索引重建
六、初始化流程(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); // 启动失败直接退出
}
}
}
启动验证逻辑:
- 数据库连接测试
- Redis连通性测试
- ES索引初始化
- 任一失败则终止应用启动
这个ES搜索组件为视频平台提供了完整、高效的搜索能力,从基础索引管理到复杂的搜索功能都有良好实现,是系统核心功能的重要支撑。