当前位置: 首页 > news >正文

【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("<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("查询失败");
    }
}

四、设计亮点分析

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

相关文章:

  • Android Input——IMS启动流程(二)
  • ubuntu下的node.js的安装
  • 带Label的韦恩图(vue)
  • 【Java】Maven
  • 【软件测试】自动化测试结合 CI/CD有哪些方案
  • Oracle 数据库查询表广播
  • 青蛙吃虫--dp
  • 【蓝桥杯】动态规划:线性动态规划
  • PhotoShop学习07
  • PostIn V1.0.8版本发布,IDEA 插件支持一键扫描上报,让接口定义不再繁琐
  • leetcode刷题记录44-208. 实现 Trie (前缀树)
  • 指针本质传递偏移动态申请空间 c语言(day05)
  • excel常见错误包括(#N/A、#VALUE!、#REF!、#DIV/0!、#NUM!、#NAME?、#NULL! )
  • 【蓝桥杯】动态规划:背包问题
  • 23种设计模式-行为型模式-模板方法
  • AtCoder 第400场初级竞赛 A~E题解
  • Redis客户端命令到服务器底层对象机制的完整流程?什么是Redis对象机制?为什么要有Redis对象机制?
  • 子串分值和(蓝桥杯)
  • 【MySQL 数据库】数据类型
  • Everything 安装教程与使用教程(附安装包)
  • 百度资源站长平台/推广普通话宣传周
  • 网站建设上市公司/重庆白云seo整站优化
  • 向国旗敬礼做美德少年网站/seo的形式有哪些
  • 息县网站建设/济南网站优化排名
  • 山西省网站建设/网站创建流程
  • 网站开发需要的编程软件/合肥网站优化技术