文章目录
-
- 前言
- 一、先搞懂:为什么项目必须引入ES?MySQL不行吗?
-
- [1. MySQL检索存在的致命短板](#1. MySQL检索存在的致命短板)
- [2. Elasticsearch核心优势(贴合短视频业务)](#2. Elasticsearch核心优势(贴合短视频业务))
- [3. 抖粉智算中ES真实使用场景](#3. 抖粉智算中ES真实使用场景)
- 二、ES基础核心概念
- 三、项目前置环境与技术栈(抖粉智算配套方案)
- 四、实战完整流程:素材从生成到可检索全链路
- 五、ES与MySQL数据同步方案(新手最容易忽略)
- 六、新手开发高频踩坑点&解决方案
- 七、ES适用场景&不适用场景(项目选型参考)
- 八、项目落地总结
前言
做短视频AI营销平台,会产生海量数据:用户生成的营销文案、AI图片、短视频素材、3D模型、商品推广脚本,全部是非结构化文本数据。
如果只用MySQL存储,根据关键词检索素材、按标签聚合筛选、模糊搜索会巨慢,多条件查询直接拖垮数据库。
在我的抖粉智算SaaS平台中,Elasticsearch(简称ES)专门承接全平台素材检索、内容统计、标签聚合功能。本文避开晦涩底层原理,直接讲清楚:什么时候用ES、基础操作、项目落地流程、真实业务代码思路、线上踩坑解决方案,零基础也能看懂,直接复用在自己的项目。
一、先搞懂:为什么项目必须引入ES?MySQL不行吗?
1. MySQL检索存在的致命短板
- 模糊查询性能极差
like '%关键词%'不走索引,百万级素材数据查询直接超时; - 无法分词检索
搜索"女装短视频",MySQL不能拆分词语,搜"女装"匹配不到"女装短视频"; - 多标签、多维度聚合统计弱
想统计"美妆类AI视频、近7天生成量、高转化素材排行",MySQL多表联查、分组聚合效率极低; - 海量非结构化文本存储不友好
长篇营销文案、视频描述、商品种草文本存入数据库,索引维护成本极高。
2. Elasticsearch核心优势(贴合短视频业务)
- 全文分词检索:自动拆分中文词语,支持模糊搜索、同义词、短语匹配;
- 毫秒级查询:底层倒排索引,亿级素材检索响应几十毫秒;
- 强大聚合统计:一键按素材类型、生成时间、用户标签、商品类目分组统计;
- 支持海量非结构化数据:文本、图片描述、视频标题、3D模型备注全部适配;
- 分布式横向扩容:素材量上涨只需新增节点,不用重构存储架构。
3. 抖粉智算中ES真实使用场景
- 创作者素材库检索:根据文案关键词、素材类型、生成时间查找AI作品;
- 商家营销素材筛选:按商品类目、短视频风格、播放标签批量过滤素材;
- 平台数据看板统计:每日图文/视频/3D素材生成数量、热门关键词排行;
- 智能推荐:根据用户搜索历史,匹配同类营销短视频素材;
- 违规内容检索:批量检索包含违规词的AI生成内容,快速审核。
二、ES基础核心概念
类比MySQL理解,瞬间看懂:
| Elasticsearch | MySQL 对应概念 | 通俗解释 |
|---|---|---|
| Index(索引) | Database 数据库 | 单独一块业务数据,项目中创建 ai_resource 索引存放所有AI素材 |
| Type(类型) | Table 数据表 | ES7.0后废弃,一个索引只存一类数据 |
| Document(文档) | Row 一行数据 | 单条AI素材,一条JSON数据,包含标题、文案、类型、创建时间等字段 |
| Field(字段) | Column 列 | 素材ID、用户ID、素材文本、素材类型、生成时间、标签等属性 |
| Mapping(映射) | 表结构Schema | 定义每个字段类型:文本、数字、日期、关键词,决定分词规则 |
| Analyzer(分词器) | 无对应概念 | 把长句子拆分成单个词语,中文必须用ik分词器 |
两个关键分词类型
text文本类型:会分词,用于全文搜索(视频文案、标题、种草描述);keyword关键词类型:不分词,精确匹配(素材类型、商品分类、用户ID、标签)。
举个例子:素材标题「夏季连衣裙带货短视频」
- text分词后:夏季、连衣裙、带货、短视频;搜连衣裙就能匹配这条数据;
- keyword只会把整句话当成一个完整值,只能完整精确匹配。
三、项目前置环境与技术栈(抖粉智算配套方案)
- 后端:Python FastAPI + SQLAlchemy 异步ORM
- ES客户端:
elasticsearch-async异步客户端,适配项目全异步架构 - 分词插件:IK分词器(中文检索必备,无插件中文拆分错乱)
- 数据同步方案:MySQL变更通过RabbitMQ异步同步到ES,保证数据一致性
- 部署:单节点开发环境,生产3节点集群,分片+副本保障数据不丢失
四、实战完整流程:素材从生成到可检索全链路
整体业务流程
AI生成图片/视频/3D素材 → MySQL存储素材基础数据 → MQ发送同步消息 → 消费消息写入ES索引 → 前端检索接口查询ES返回素材列表
#mermaid-svg-uZwqzzHb9dfUMYgK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uZwqzzHb9dfUMYgK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uZwqzzHb9dfUMYgK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uZwqzzHb9dfUMYgK .error-icon{fill:#552222;}#mermaid-svg-uZwqzzHb9dfUMYgK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uZwqzzHb9dfUMYgK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uZwqzzHb9dfUMYgK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uZwqzzHb9dfUMYgK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uZwqzzHb9dfUMYgK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uZwqzzHb9dfUMYgK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uZwqzzHb9dfUMYgK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uZwqzzHb9dfUMYgK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uZwqzzHb9dfUMYgK .marker.cross{stroke:#333333;}#mermaid-svg-uZwqzzHb9dfUMYgK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uZwqzzHb9dfUMYgK p{margin:0;}#mermaid-svg-uZwqzzHb9dfUMYgK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uZwqzzHb9dfUMYgK .cluster-label text{fill:#333;}#mermaid-svg-uZwqzzHb9dfUMYgK .cluster-label span{color:#333;}#mermaid-svg-uZwqzzHb9dfUMYgK .cluster-label span p{background-color:transparent;}#mermaid-svg-uZwqzzHb9dfUMYgK .label text,#mermaid-svg-uZwqzzHb9dfUMYgK span{fill:#333;color:#333;}#mermaid-svg-uZwqzzHb9dfUMYgK .node rect,#mermaid-svg-uZwqzzHb9dfUMYgK .node circle,#mermaid-svg-uZwqzzHb9dfUMYgK .node ellipse,#mermaid-svg-uZwqzzHb9dfUMYgK .node polygon,#mermaid-svg-uZwqzzHb9dfUMYgK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uZwqzzHb9dfUMYgK .rough-node .label text,#mermaid-svg-uZwqzzHb9dfUMYgK .node .label text,#mermaid-svg-uZwqzzHb9dfUMYgK .image-shape .label,#mermaid-svg-uZwqzzHb9dfUMYgK .icon-shape .label{text-anchor:middle;}#mermaid-svg-uZwqzzHb9dfUMYgK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uZwqzzHb9dfUMYgK .rough-node .label,#mermaid-svg-uZwqzzHb9dfUMYgK .node .label,#mermaid-svg-uZwqzzHb9dfUMYgK .image-shape .label,#mermaid-svg-uZwqzzHb9dfUMYgK .icon-shape .label{text-align:center;}#mermaid-svg-uZwqzzHb9dfUMYgK .node.clickable{cursor:pointer;}#mermaid-svg-uZwqzzHb9dfUMYgK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uZwqzzHb9dfUMYgK .arrowheadPath{fill:#333333;}#mermaid-svg-uZwqzzHb9dfUMYgK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uZwqzzHb9dfUMYgK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uZwqzzHb9dfUMYgK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uZwqzzHb9dfUMYgK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uZwqzzHb9dfUMYgK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uZwqzzHb9dfUMYgK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uZwqzzHb9dfUMYgK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uZwqzzHb9dfUMYgK .cluster text{fill:#333;}#mermaid-svg-uZwqzzHb9dfUMYgK .cluster span{color:#333;}#mermaid-svg-uZwqzzHb9dfUMYgK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uZwqzzHb9dfUMYgK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uZwqzzHb9dfUMYgK rect.text{fill:none;stroke-width:0;}#mermaid-svg-uZwqzzHb9dfUMYgK .icon-shape,#mermaid-svg-uZwqzzHb9dfUMYgK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uZwqzzHb9dfUMYgK .icon-shape p,#mermaid-svg-uZwqzzHb9dfUMYgK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uZwqzzHb9dfUMYgK .icon-shape .label rect,#mermaid-svg-uZwqzzHb9dfUMYgK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uZwqzzHb9dfUMYgK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uZwqzzHb9dfUMYgK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uZwqzzHb9dfUMYgK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户生成AI素材
MySQL保存素材主数据
RabbitMQ发送同步消息
异步消费者接收消息
组装文档JSON写入ES ai_resource索引
前端检索请求
后端调用ES异步客户端
分词检索+聚合筛选素材
返回分页素材数据给前端
步骤1:创建索引+Mapping(项目初始化一次性执行)
针对AI素材设计映射,区分text/keyword字段,开启ik中文分词,伪代码示例:
python
# 索引名称:ai_resource
mapping = {
"mappings": {
"properties": {
"resource_id": {"type": "keyword"}, # 素材ID,精确匹配
"user_id": {"type": "keyword"}, # 用户ID
"title": {"type": "text", "analyzer": "ik_max_word"}, # 标题分词检索
"content": {"type": "text", "analyzer": "ik_max_word"}, # 营销文案
"res_type": {"type": "keyword"}, # 素材类型:image/video/3d
"create_time": {"type": "date"}, # 创建时间,用于范围筛选
"tags": {"type": "keyword"} # 素材标签,多标签筛选
}
}
}
# 异步客户端创建索引,仅首次启动执行
await es_client.indices.create(index="ai_resource", mappings=mapping)
步骤2:新增素材,异步同步数据到ES
不直接同步写入ES,避免阻塞主业务流程,依靠MQ异步解耦:
- AI任务完成,存入MySQL素材记录;
- 发送消息到
es_sync_queue队列,携带素材完整字段; - 消费者监听队列,组装ES文档执行新增/更新;
优势:就算ES服务短暂宕机,消息堆积不会影响用户生成素材,恢复后自动同步。
步骤3:核心实战:关键词分页检索(平台最常用接口)
需求:用户输入关键词,筛选视频类素材,按创建时间倒序分页展示
简化伪代码:
python
# 检索条件
search_key = "美妆带货"
res_type = "video"
page = 1
size = 10
# ES查询DSL语句
dsl = {
"query": {
"bool": {
"must": [
# 全文检索标题+文案,分词匹配关键词
{"multi_match": {"query": search_key, "fields": ["title", "content"]}},
# 精确匹配素材类型
{"term": {"res_type": res_type}}
]
}
},
"sort": [{"create_time": "desc"}], # 创建时间倒序
"from": (page - 1) * size,
"size": size
}
# 异步执行查询
res = await es_client.search(index="ai_resource", query=dsl)
# 解析结果,封装素材列表返回前端
data_list = parse_es_result(res)
步骤4:聚合统计实战(后台数据看板)
需求:统计近7天,图文、视频、3D各类素材生成总量
python
dsl = {
"query": {"range": {"create_time": {"gte": "now-7d"}}},
"aggs": {
"group_by_type": {
"terms": {"field": "res_type"}
}
},
"size": 0 # 不需要返回素材文档,只需要聚合结果
}
执行后直接拿到分类统计数字,无需复杂MySQL分组查询。
五、ES与MySQL数据同步方案(新手最容易忽略)
方案选型:MQ异步同步(项目最终采用)
- 新增/修改/删除素材时,MySQL业务完成后发送MQ消息;
- 消费者监听消息,根据操作类型执行ES新增、更新、删除文档;
- 定时补偿任务:每小时校验MySQL与ES数据总量,缺失数据增量同步。
为什么不选用binlog同步?
- 中小型SaaS项目数据量适中,MQ同步轻量化,部署简单;
- binlog同步对数据库有一定压力,且需要额外中间件Canal,增加维护成本;
- 业务可控性更强,可以自定义需要同步的字段,过滤无效测试数据。
六、新手开发高频踩坑点&解决方案
坑1:中文搜索匹配不到数据
原因:没有安装IK分词器,默认英文分词,中文直接作为完整字符匹配。
解决:部署ES后安装IK插件,text字段指定ik_max_word分词器。
坑2:模糊查询性能差,大量数据查询超时
原因:使用wildcard通配符模糊匹配,不走倒排索引。
解决:全部使用multi_match全文检索,依靠分词实现模糊匹配。
坑3:数据和MySQL不一致,ES存在脏数据
原因:MQ消息丢失、ES写入失败没有重试。
解决:
- MQ开启消息持久化、手动ACK确认;
- ES写入失败捕获异常,消息重新入队重试;
- 定时补偿任务定时校对双库数据。
坑4:分页越深查询越慢(from+size深度分页问题)
场景:用户翻第100页素材,查询卡顿。
解决:前端不支持深度分页,大数据量检索改用search_after游标分页。
坑5:Mapping字段类型随意定义,检索失效
比如把素材标题设置为keyword,只能完整输入标题才能搜到,关键词匹配失效。
规范:描述、文案、标题用text;ID、分类、标签、状态统一用keyword。
坑6:大量高频写入ES,引发服务卡顿
AI批量生成素材时一次性同步上千条文档,IO压力暴增。
优化:使用bulk批量接口,合并多条文档一次性写入,减少网络请求。
七、ES适用场景&不适用场景(项目选型参考)
适合使用ES
- 全文关键词检索、模糊内容搜索;
- 多维度标签、时间、分类联合筛选;
- 海量文本数据分组聚合、数据统计看板;
- 内容审核、违规文本批量检索。
不适合使用ES,直接用MySQL
- 简单单条数据精确查询(根据ID查素材详情);
- 强事务资金业务(额度、订单、支付流水);
- 数据量很小、无检索需求的基础配置表。
八、项目落地总结
- ES核心定位:负责检索与统计,不替代MySQL做主存储,素材基础数据仍存在MySQL;
- 中文检索必备IK分词器,区分text/keyword字段是检索生效关键;
- 采用RabbitMQ异步同步MySQL与ES数据,兼顾性能与数据一致性;
- 日常开发优先使用
multi_match全文检索,避免低效通配符查询; - 聚合统计替代复杂MySQL分组,大幅降低数据库压力;
- 上线配套补偿定时任务,解决消息丢失、数据不一致问题。