ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Elasticsearch电商搜索实战:从MySQL到倒排索引的中文分词与性能优化

Elasticsearch电商搜索实战:从MySQL到倒排索引的中文分词与性能优化 把电商平台的商品搜索从 MySQL 的 LIKE 查询切到 Elasticsearch是我做过最值当的一次技术升级。之前线上商品一多搜索响应直接飙到几百毫秒甚至超时好不容易查出来分词还是一团糟——搜红色连衣裙结果里全是红色或连衣裙的凑合匹配品牌词、价格区间、销量排序全都靠硬编码。换到 Elasticsearch 之后倒排索引 中文分词 聚合过滤的组合拳把搜索从能出结果提升到结果能用。这整个排查和优化的过程基本就是一套标准的电商搜索落地路径。这篇文章我会把从零搭建的完整过程拆开讲包括数据怎么同步进 ES、索引映射怎么设计、中文分词器怎么装、bulk 批量导入怎么写、搜索 DSL 怎么调以及我在 Windows 环境下踩过的一堆坑。内容偏实操适合正在做电商搜索、商品检索相关需求的开发同学参考。如果你只是听说过 ES想看看它到底能解决什么问题这篇也可以当一份入门到实战的路线图来看。1. 电商搜索的痛点和 Elasticsearch 的选型思路1.1 电商搜索到底难在哪先说说为什么 MySQL 撑不住商品搜索。最直接的场景用户在前端搜索框输入华为手机 白色 256G这种多词组合查询用 MySQL 的LIKE %华为手机%做模糊匹配数据量到了几十万条就开始明显变慢到了百万级基本就是灾难。更要命的是LIKE语句根本没法用索引一次全表扫描跑几百毫秒用户早就没耐心了。第二个痛点是中文分词。搜索连衣裙你还好处理但如果用户搜红色连衣裙MySQL 只能做整串匹配或者靠LIKE %红% OR LIKE %连衣裙%这种粗暴方案结果就是召回了一堆红袜子绿裙子之类完全不相关的商品。中文没有空格分词切得太粗召回率低切得太细精准度差这个平衡没有专业分词器根本做不好。第三个痛点是相关性排序。电商搜索不是查出来就行用户搜手机你得把华为、小米、苹果这些主流品牌排在前面把手机壳手机支架这些边角料排到后面同时还要兼顾销量高、评价好的商品优先。这种多因素加权排序靠 SQL 写ORDER BY基本无能为力。最后是过滤和聚合。用户搜完手机之后往往还要继续勾选价格区间、品牌、屏幕尺寸、内存版本页面侧边栏要实时显示每个品牌、每个价格段有多少商品。这种多条件组合过滤 实时统计在 MySQL 里要用一堆COUNTGROUP BY拼出来性能极差而且代码复杂。所以电商搜索本质上是四件事召回、分词、排序、聚合过滤。这四件事恰好都是 Elasticsearch 的强项。1.2 为什么最终选了 Elasticsearch选型的时候我也对比过其他方案。最早考虑过 MySQL 自带的全文索引但它的分词能力对中文支持太弱性能也撑不住并发查询。也认真看过 Solr功能上其实和 ES 差不多但 ES 的分布式能力更顺手——索引分片、副本、集群扩容都是开箱即用的Solr 在这块配置要繁琐不少。云厂商的搜索服务我也试过省心是省心但数据量一上去价格不便宜而且有被绑定的风险。Elasticsearch 的几个核心优势我在实际用下来之后感受很直接倒排索引天生为搜索设计查询速度在千万级数据量下依然能保持毫秒级响应这一点和 MySQL 的 BTree 有本质区别。有成熟的中文分词插件生态IK、pinyin 这些社区插件直接装就能用还支持自定义词典电商里的品牌名、网络热词都能加进去。分布式架构非常成熟数据量大了把分片数一调、节点一扩就完事不需要改业务代码。配套的 Kibana 调试工具太好用了写查询、看索引、测分词全在浏览器里搞定排查问题效率比 Solr 高一大截。社区活跃任何问题在搜索引擎里基本都能找到答案这对做技术选型的人来说是很重要的隐性优势。当然ES 也不是没有缺点。写入延迟相对较高不适合当主数据库内存吃得很凶集群规模大了运维成本不低。但我当时的业务场景就是商品数据从 MySQL 同步过来只负责搜索和聚合这些缺点完全可以接受。2. 整体架构设计从商品数据到可搜索索引2.1 数据流转链路设计确认用 ES 之后首先要解决的是数据从哪来、怎么进去的问题。电商平台的主数据都在 MySQL 里商品信息变更、价格调整、上下架操作都在 MySQL 完成ES 只是数据的消费方。我当时设计的数据链路是商品服务MySQL 主库 → 定时任务全量/增量同步 → Elasticsearch → 搜索接口服务 → 前端搜索页同步方式我选了定时任务因为当时商品量在几十万级别变更频率不算高用定时任务每 5 分钟拉一次增量数据足够了。如果你做的是大促场景、要求秒级同步建议换成 Canal 监听 MySQL binlog 的方式这个后续可以单独写一篇。这里有个经验一开始就要把updated_at字段设计好。不管是定时任务还是 Canal增量同步都需要一个时间戳字段来判断哪些数据变了。我见过不少项目因为这个字段没加导致每次只能全量同步数据量一大 ES 集群就被拖垮。同步的逻辑也不复杂大致三步先查 MySQL 取增量数据然后做字段映射和清洗比如把数据库里的分类 ID 翻译成分类名称、把上下架状态翻译成 ES 要用的枚举值最后调 ES 的 bulk 接口批量写入。清洗这一步容易忽略但非常关键——ES 里存的应该是直接面向搜索和展示的数据而不是把 MySQL 表结构原封不动搬过来。2.2 索引映射设计的核心字段索引映射决定了数据在 ES 里怎么存储、怎么被检索这个设计对了后面能少走很多弯路。我贴一个当时用的 mapping 精简版{ mappings: { properties: { id: { type: keyword }, name: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart, fields: { keyword: { type: keyword } } }, category: { type: keyword }, brand: { type: keyword }, price: { type: double }, stock: { type: integer }, sales: { type: integer }, status: { type: keyword }, tags: { type: keyword }, attributes: { type: nested, properties: { name: { type: keyword }, value: { type: keyword } } }, created_at: { type: date } } } }每个字段的设计都有讲究。id用keyword而不是text因为 ID 不需要分词只需要精确匹配。name是最核心的搜索字段用text类型配合 IK 分词器我还给它加了一个.keyword子字段这样既能分词搜索又能拿原始商品名做精确排序或聚合。category、brand这类字段用keyword是因为它们只做精确过滤和分组统计分词反而会让聚合结果变得很怪。price用double主要是为了方便范围和排序查询如果对精度敏感建议用scaled_float。attributes这种 SKU 属性字段我特意用了nested嵌套类型这样每个属性的 name 和 value 才能成对匹配——比如颜色白色和内存256G要各自对应不会出现交叉匹配的脏数据。这里有一个新手容易犯的坑字段类型一旦建好索引就不能直接改了要改只能重建索引。所以 mapping 务必在正式造数据之前设计好多花点时间把搜索场景理清楚后面能省下大量返工的时间。2.3 中文分词器的选择与安装分词是中文搜索绕不开的坎。ES 自带的 standard 分词器对中文来说就是灾难它会把一段话切成单个汉字比如华为手机会被切成华为手机搜出来的结果根本没有语义可言。所以必须装专门的中文分词插件。我选的是 IK 分词器这是目前最主流的方案。安装方式很简单在 ES 的 bin 目录下执行elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.6/elasticsearch-analysis-ik-7.17.6.zip版本号一定要和你的 ES 版本严格对应我因为这个踩过坑版本不匹配插件会加载失败ES 可能直接启动不了。装完插件之后可以用两个命令验证是否生效。第一个是elasticsearch-plugin list在命令行列出所有已安装的插件第二个是在 Kibana 里执行GET /_cat/plugins?v返回结果里能看到 analysis-ik 就说明装好了。IK 有两种分词模式ik_max_word会尽量切出最多的词召回率高适合索引阶段ik_smart只保留最合理的粗粒度分词准确率高适合查询阶段。所以我在 mapping 里的 name 字段配置了analyzer: ik_max_word和search_analyzer: ik_smart就是这个考虑。分词器装好后还有一件重要的事配置自定义词典。IK 默认词典对电商场景的支持不够品牌名、型号、网络热词经常被切错。配置文件在IKAnalyzer.cfg.xml把自定义词典的路径配好然后在custom.dic里一行一个词重启 ES 就生效了。比如我把Mate60红米连衣裙这些词都加进了自定义词典切词效果立刻提升一个档次。3. 实操过程Windows 环境搭建与核心接口实现3.1 Windows 环境安装与启动很多教程默认都是 Linux 环境但我是 Windows 开发机就在 Windows 上把整套流程过了一遍这里专门讲一下 Windows 的坑。首先去 Elastic 官网下载对应版本的 ZIP 包解压到一个不带空格的目录比如D:\elasticsearch-7.17.6。然后用管理员权限打开 PowerShell进入 bin 目录改成127.0.0.1对外的局域网机器需要改成实际 IP。http.port默认 9200 一般不用改。cluster.name: my-es-cluster node.name: node-1 path.data: D:/elasticsearch-7.17.6/data path.logs: D:/elasticsearch-7.17.6/logs network.host: 0.0.0.0 http.port: 9200启动方式是运行elasticsearch.bat注意不是elasticsearch.exebat 脚本会正确设置环境变量。启动过程如果看到started日志就说明成功了浏览访问 http://localhost:9200 应该能看到 JSON 格式的版本信息。Windows 上最常见的启动失败原因有三个一是内存不够如果只有 8G 内存建议把 JVM 堆调小一点二是 9200 端口被占用用netstat -ano | findstr 9200查一下把占用进程清掉三是 JDK 版本冲突ES 7.17 内置了 JDK但如果系统 JAVA_HOME 指向的是一个过老版本可能会启动报错。还有一个官方明确不建议的操作不要用elasticsearch-service.bat install把 ES 注册成 Windows 服务因为服务的运行环境会引发各种奇怪权限问题。3.2 用 bulk 接口批量导入商品数据索引和分词器都准备好了接下来就是灌数据。我见过有同学用循环调_index接口逐条插入10 万条数据跑了半个多小时这是典型的错误姿势。ES 提供 bulk 批量接口专门用来解决大批量写入的性能问题。这里要先澄清一个热门词经常有人搜elasticsearch bulk 插件实际上 bulk 不是一个独立插件而是 ES 内置的 REST API。只需要往/_bulk端点发送特定格式的数据就行了。bulk 的格式比较特殊不是标准 JSON 数组而是一行一个操作。每个操作占两行第一行是元数据index、create、update、delete 等第二行是文档数据。拿商品数据举例{index: {_index: products, _id: 1}} {id: 1, name: 华为Mate60 Pro 5G手机, category: 手机, brand: 华为, price: 6999, stock: 100, sales: 12345, status: on_sale} {index: {_index: products, _id: 2}} {id: 2, name: 苹果iPhone 15 128G, category: 手机, brand: 苹果, price: 5999, stock: 50, sales: 23456, status: on_sale}注意每行都必须是紧凑的 JSON最后一个文档后要有一个空行结束。用 curl 上传一个文件curl -H Content-Type: application/json -XPOST http://localhost:9200/_bulk --data-binary products.json批量大小的选择有讲究。我试过一批 1000 条、5000 条、10000 条在单机 Windows 环境下5000 条一批的性能比较理想一次请求耗时几十毫秒10 万条数据大概两三分钟就能导完。如果你遇到大批量导入超时可以先把索引的refresh_interval调成-1导入完成后恢复成1s写入速度会明显提升。另外一个细节bulk 响应会返回一个 JSON 对象里面有个items数组每个元素对应一条操作的执行结果。要检查里面有没有error字段很多新手只看 HTTP 状态码返回 200 就以为全部成功了实际上可能有部分文档写入失败了。3.3 搜索接口的核心 DSL 实现数据导进去之后真正的重头戏是写搜索查询。ES 的搜索 DSL 功能非常丰富我挑一个电商场景最常用的综合查询来讲它基本包含了搜索的核心要素。{ query: { function_score: { query: { bool: { must: [ { multi_match: { query: 华为手机, fields: [name^3, brand^2, category] } } ], filter: [ { term: { status: on_sale } }, { terms: { brand: [华为, 荣耀] } }, { range: { price: { gte: 1000, lte: 8000 } } } ] } }, field_value_factor: { field: sales, modifier: log1p, factor: 0.5 } } }, sort: [ { _score: desc } ], from: 0, size: 20, highlight: { fields: { name: { pre_tags: [em], post_tags: [/em] } } } }拆开看这几个关键部分。multi_match同时在 name、brand、category 三个字段里搜索华为手机通过name^3这种 boost 语法提高了商品名匹配的权重这样搜华为手机时商品名里含华为和手机的结果比品牌栏里只有华为的结果排得更靠前。bool里的filter部分承担过滤功能品牌、价格区间、库存状态这些条件都放这里。filter 不会参与相关度打分而且会被 ES 缓存性能非常高所以凡是条件性质的约束都应该放在 filter 里而不是塞进must。排序是整个查询里最体现电商特色的部分。我没有直接用sort按销量硬排序而是用function_score把销量作为相关性打分的一部分field_value_factor读取 sales 字段通过log1p做对数运算来平滑数值——因为头部商品的销量可能是尾部商品的成千上万倍如果直接做乘法其它所有因素都被销量淹没了。log1p也就是ln(1sales)可以把销量差异从千倍差距压缩到几十倍的差距配合factor: 0.5控制权重让销量高的商品排在前面同时又不至于让新上架的高相关商品完全没有出头机会。highlight是电商搜索的标配它会在返回结果中自动把命中的词加上.Query标签前端拿到之后渲染成红色或加粗用户一眼就能看到自己搜的词在商品名里的位置这个体验提升非常直观。3.4 用 Kibana 验证索引和搜索效果数据导入完成、DSL 写完之后强烈建议用 Kibana 做验证和调试。Kibana 和 ES 版本必须一致我用的 7.17.6 配套的 Kibana 也是 7.17.6版本不一致会出现数据格式兼容问题。Windows 下启动 Kibana 很简单解压后进 bin 目录运行kibana.bat默认端口 5601浏览器访问 http://localhost:5601。在左侧菜单里找到Dev Tools这个工具的 Console 面板就是我日常调试 ES 的主战场。几个常用的调试命令# 查看全部索引列表关注健康状态、文档数、存储大小 GET /_cat/indices?v # 查看某个索引的映射结构 GET /products/_mapping # 查看索引里已安装的分词器插件 GET /_cat/plugins?v # 测试分词效果 POST /products/_analyze { field: name, text: 华为Mate60 Pro }_analyze这个 API 太好用了写完分词器配置之后先用它验证一遍切词结果能直接看到华为“Mate60”“Pro”这些词有没有被正确切出来。如果发现分词不符合预期马上调整自定义词典而不是等搜索出来不对劲了再回头排查。4. 常见问题与排查技巧实录4.1 启动失败内存、端口、JDKES 启动失败是新手遇到最多的拦路虎我把最常见的三种情况和排查思路列出来。第一个是 JVM 内存问题。如果日志里报could not reserve enough space for object heap说明config/jvm.options里设置的堆内存超过物理机可用内存了。桌面开发机 16G 内存的话-Xms2g -Xmx2g比较稳妥8G 内存就调成-Xms1g -Xmx1g。注意这两个值最好设成一样的避免 JVM 运行中动态扩容引发性能抖动。第二个是端口被占用。ES 启动会绑定 9200HTTP 和 9300节点通信 两个端口我用netstat -ano | findstr 9200查一下如果看到 PID 再进任务管理器把占用进程结束。9300 端口被占用不一定直接报错但节点会无法组集群排查起来更隐蔽。第三个是 JDK 版本冲突。ES 7.17 内置了 OpenJDK但如果你系统里配了JAVA_HOME且版本比较老ES 启动时可能优先用系统 JDK 导致失败。解决办法是把 JAVA_HOME 指到 JDK 11或者干脆在启动脚本里强制让 ES 用自己的内置 JDK。Windows 下还有一个细节ES 不要装在路径含中文或空格的目录下文件权限问题会产生各种诡异 BUG。4.2 搜索不出来或分词不生效索引里明明有数据但搜索就是查不出来这是我被问得最多的问题。排查思路按顺序走先用GET /products/_count确认索引里到底有没有文档然后在 Kibana 里用_analyze测试查询词的分词结果看它被切成了哪些词再对比映射中analyzer和search_analyzer的配置看是不是索引和查询用了不同模式导致匹配不上。最常见的原因是建索引时字段的类型搞错了。比如你把 name 字段设成了keyword而不是textES 会把整个华为Mate60 Pro当成一个不可分割的词存进倒排索引用户搜华为时当然匹配不到。这就是为什么我在前面强调 mapping 设计要提前想清楚keyword 和 text 表面看都是字符串实际语义千差万别。另一个高频坑是改了分词器配置但不重建索引。IK 自定义词典更新或者 analyzer 调整之后必须对已有数据执行 reindex 才能让旧文档按新配置重新分词。很多人以为重启 ES 就完事了结果新的搜索词依然查不出老商品其实就是索引里的旧分词信息没更新。4.3 bulk 导入超时或性能差bulk 导入性能差多半是没掌握批量处理的正确姿势。我给的参数是 5000 条一批每批大概 1~2MB实测导入 10 万条文档在 Windows 单机环境下跑进 3 分钟没问题。如果你导入时 CPU 占用不高但是耗时很长可以尝试调大批量到 10000 条如果报 429 被限流了就适当调小。导入过程中如果追求极致速度建议先调大refresh_interval的间隔甚至暂停刷新因为每次 refresh 都会有写入磁盘和构建倒排索引的开销。等全部导入完成后再恢复正常刷新间隔PUT /products/_settings { refresh_interval: -1 } # 导入完成后 PUT /products/_settings { refresh_interval: 1s }还有一个新手很容易忽略的点bulk 请求的Content-Type一定要带application/json否则 ES 可能返回格式解析错误。以及我前面提过的响应里的items数组要逐条检查 error 字段因为 ES 的 bulk 不是事务性的记录 A 写成功、记录 B 写失败是同时发生且互不影响的。4.4 相关性排序不对排序是电商搜索里最需要反复调的部分。最典型的反馈是搜华为手机结果里销量几万的老款手机排在前面刚上架的新款反而排到十几页去了——用户根本翻不到。遇到这种问题第一步用explain看打分细节GET /products/_search { explain: true, query: { multi_match: { query: 华为手机, fields: [name^3, brand^2] } } }返回结果里会详细列出每个文档得分怎么来的哪部分命中、哪部分加了分、哪部分被降权一目了然。我排查过不少排序异常最后基本都是这几个原因一是品牌词和商品词权重没拉开比如搜华为时品牌字段命中的文档反而比商品名精确匹配的文档得分低二是没有用 function_score 把销量纳入打分相关性和商业指标脱节三是没做同义词扩展导致手机和移动电话这种同义词无法互相召回。解决方式就是在 query 外面套 function_score用field_value_factor把销量、评价数等字段转换成打分因子。这个技巧我上面已经写了一段示例代码实际项目中我就是那么调的上线后搜索转化率确实有明显提升。4.5 常见问题速查表我把上面讲到的坑整理成一个速查表方便在运维和开发时对照排查问题现象可能原因解决方案ES 启动失败报内存不足JVM 堆设置过大调整 jvm.options降低堆内存端口被占用9200/9300 被其他进程占用netstat 查找 PID结束进程或改端口分词不生效mapping 中 analyzer 未配置更新映射并 reindex 重建索引搜索查不出中文结果类型误用为 keyword 或未装分词器改成 text 类型并安装 IK 分词器bulk 导入超时批量过大或 refresh 频繁调整批量大小导入时暂停 refresh排序不合理未把销量纳入打分使用 function_score field_value_factor索引文档数为 0数据同步逻辑有问题检查 MySQL 同步任务和 ES 写入日志5. 进阶从搜索到分析的边界思考5.1 Elasticsearch 能做 OLAP 吗做搜索做到后面经常有人问既然 ES 聚合功能这么强能不能直接用 ES 替代数仓里的 OLAP 引擎做数据分析我在项目里也尝试过用 ES 做轻量级报表这里说说实际感受。ES 的聚合分析能力确实不弱指标聚合算均值、求和桶聚合做分组统计日期直方图看趋势配合 Kibana 的 Lens 面板能做出挺好看的可视化图表。我们当时就用 ES 做过一个商品销售 Top100 的实时看板数据从订单系统同步到 ES聚合查询在百万级数据量下响应速度很快比从 MySQL 查了再写报表代码方便太多。但如果你把它当真正的 OLAP 引擎用会遇到几个硬伤。首先是多表关联基本没法用ES 的 nested 对象虽然是嵌套结构但类型还是一个文档包含多个子对象的逻辑不支持真正意义上的 JOIN。如果你的分析模型是星型模型需要事实表和维度表做关联ES 会非常吃力。其次是数据更新成本高ES 的 shard 内部是有状态的数据结构频繁的大规模更新会导致性能下降。我当时的结论是ES 适合搜索 简单聚合的组合场景比如商品搜索页的分面统计、订单数据的实时看板、日志系统的趋势分析。如果要做复杂的多维分析、千亿级数据的离线计算还是用 ClickHouse、Doris 这类专门的分析引擎更合适。5.2 搜索系统的后续演进方向把基础搜索功能跑通之后你大概率会遇到新的需求我列几个真实会碰到的演进方向。第一个是搜索建议和纠错。用户在输入框里敲到一半前端要弹下拉提示这个在 ES 里可以用 suggesters 功能实现。另外用户经常会打错字比如华伪想搜华为靠拼音分词器和编辑距离纠错就能兜住一部分。第二个是同义词扩展。连衣裙和裙子手机和移动电话笔记本和笔记本电脑这类同义词如果不处理召回率会低很多。IK 分词器支持同义词词典配置或者你在查询层做同义词映射效果都还不错。第三个是个性化排序。电商场景的排序很看用户群体新用户可能更看重价格和销量老会员可能更看重品牌和评价。你可以在搜索接口里把用户标签、会员等级作为 function_score 的加权因子传进来比如高等级用户搜索时提高品牌旗舰店商品的权重。第四个是集群化部署。单机 ES 数据量到了几亿条磁盘和 CPU 都会吃不消。这时候要考虑把节点拆成 master、data、client 三类角色给核心索引配置副本分片引入冷热数据分离——热数据放 SSD冷数据放普通机械盘。6. 写在最后的实操心得回到开头说的那个问题——电商搜索为什么难因为用户期待的是一个懂他的搜索框而不是一个机械的匹配工具。Elasticsearch 给了我们一套完整的工具链来解决这个问题但工具终归只是工具真正决定搜索体验的还是你对业务场景的理解和对数据细节的把控。我实际做下来最大的体会是不要急着写代码先把索引映射和分词策略设计好这比什么都重要。一个好的 mapping 设计能让后续的开发顺畅无比一个草率的 mapping 设计后面要拿无数次 reindex 来填坑。最后再分享一个实用技巧在 Kibana 的 Dev Tools 里把常用的搜索模板保存下来每次调试新需求时基于模板修改比从零写 DSL 效率高很多。搜索系统上线后记得定期看 ES 的慢查询日志把那些经常超时的查询捞出来优化——搜索是一个持续迭代的过程首版上线只是开始。
返回列表