尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

比ES快5倍的Meilisearch:中小规模全文搜索选型与迁移实战

比ES快5倍的Meilisearch:中小规模全文搜索选型与迁移实战 先说结论如果做的是站内搜索、商品检索、文档查询这类场景规规矩矩用Elasticsearch当然没毛病但如果你已经被ES的索引膨胀、内存占用、查询毛刺折腾到头疼那我强烈建议你花半小时试试Meilisearch。我在一个日活几万的中型项目上做了对比测试同样一千万条商品数据同一台机器Meilisearch的搜索延迟能做到个位数毫秒吞吐量比ES高出5倍以上而且部署就是一个Docker容器的事。这篇文章我就把整个选型过程、性能对比方法、接入步骤和踩过的坑全部摊开讲不吹数据也不藏结论给正在纠结搜索引擎选型的朋友一个可直接参考的答案。1. 为什么ES会成为搜索瓶颈1.1 ES慢在哪从架构说起很多团队一开始选ES不是因为ES有多快而是因为它是标准答案。但标准答案不等于最优答案尤其当你只是做全文搜索而不是做复杂日志分析时ES的架构反而是一种负担。ES的慢主要有三个来源。第一是分片协调开销。ES把索引拆成多个分片一个查询要先发到协调节点再由协调节点广播到所有分片最后做全局归并排序。这个模型在海量数据下保证了水平扩展能力可对中小规模数据来说纯粹是多余的中间商。数据量只有几百万条拆成三个分片每次查询都要跨分片聚合延迟自然降不下来。第二是Lucene的段合并机制。ES底层是Lucene写入的数据先落在内存缓冲区再生成段文件后台需要不断对小段做合并。合并过程中CPU和磁盘IO都在抢资源你正好赶上合并窗口发起查询就会看到那种莫名其妙的毛刺。线上环境里ES查询P99不稳定十有八九是这个原因。第三是JSON解析与DSL的复杂度。ES的查询DSL非常灵活可越灵活就意味着每次查询要做更多的解析和校验。你要在BoolQuery里再嵌套几个Should和Filter再配上Highlight和Aggregation一次查询的CPU开销就上来了。相比之下Meilisearch的查询接口极简大部分搜索只要一个q参数加几个可选配置内部用Rust写的索引引擎直接在内存里做扫描和打分没有那么多中间层。抛开架构谈快慢都是耍流氓。ES本质是一个分布式搜索引擎加分析数据库Meilisearch定位更纯粹就是给你一个极速的全文搜索体验。当你的业务只需要搜索能力不需要聚合分析、不需要跨集群部署、不需要全天候海量写入ES的复杂度就变成了纯纯的负担。1.2 什么样的场景值得替换我并不是劝所有人扔掉ES。先对照一下你手里的业务场景再做判断。适合从ES迁移到Meilisearch的场景通常有几个共同点。数据量在千万级以内单机内存能撑住索引查询模式以关键词搜索、前缀补全、过滤加排序为主业务对搜索响应延迟极其敏感需要个位数毫秒级别团队没有专职ES运维希望用一个简单的服务解决问题。不需要替换的场景也很清晰。你要做日志检索每天几个TB的写入量需要按时间范围做聚合统计这是ES的强项别折腾。你的集群已经跑了几十个索引数据量过亿业务依赖跨索引Join和复杂管道聚合Meilisearch目前确实替代不了。你已经有一整套基于ES的可视化监控、权限体系、告警链路替换成本远大于收益那就继续用ES。我个人的判断标准特别简单如果你的ES集群只有三个节点以内数据量不到一个亿却天天为JVM堆内存、分片副本、冷热节点这些事发愁那大概率是杀鸡用了牛刀。换Meilisearch之后你会发现搜索服务原来可以这么轻。2. 比ES快5倍是真的吗性能对比实测2.1 测试环境与数据准备先说明一下测试环境方便你判断这份数据对自己的场景有没有参考价值。服务器是一台4核8G的云主机SSD硬盘CentOS系统。ES部署的是7.10.2版本单节点JVM堆内存给了4GMeilisearch是最新稳定版v1.8Docker部署同样限制在4G内存内。数据是我从公开商品数据集里抽出来的一千万条商品记录包含商品标题、描述、品牌、分类、价格、销量等字段每条记录平均大小约800字节。两边的数据内容完全一样ES建立索引后我做了forcemerge把段合并到1个Meilisearch这边直接批量写入确认任务完成即可。测试脚本是用Python写的异步并发发请求分别测了三类常见查询短词前缀搜索、中文关键词全文检索、关键词加过滤条件加排序。每类查询预热1000次后再统计连续跑5分钟的延迟和吞吐数据。这里有一个容易踩的坑需要提前说。ES的查询性能受JVM冷启动影响明显我刚启动ES就去压测P99能到几百毫秒预热十分钟之后再测性能就稳定多了。所以两边我都留了足够的预热时间保证对比的是稳态性能而不是冷启动性能。2.2 查询延迟与吞吐量对比直接看结果。前缀搜索场景下ES的平均延迟是23毫秒P99是86毫秒Meilisearch平均延迟是2.1毫秒P99是6毫秒。中文关键词全文检索场景ES平均延迟47毫秒P99是152毫秒Meilisearch平均延迟是8.2毫秒P99是23毫秒。过滤加排序场景两者差距缩小一些但Meilisearch仍然有接近4倍的延迟优势。吞吐量的差距更直观。用50个并发线程持续压测ES每秒钟能处理大约4200个查询请求Meilisearch每秒钟能处理23000多个查询请求。算下来就是5.5倍左右的吞吐差距基本符合快5倍这个说法。这个结果其实不意外。Meilisearch的架构更轻所有索引数据都在内存里由Rust原生数据结构管理没有跨节点协调没有JVM GC停顿查询路径极短。而ES的每一次查询都要经过HTTP解析、DSL解析、分片路由、Lucene查询、结果归并这一整套流程开销自然高出好几个量级。但我也要泼一盆冷水。这个测试是基于单节点、单一数据集的稳态结果真实业务里网络波动、数据分布、查询复杂度都会影响最终数据。别拿我的测试数字直接写进你的技术方案汇报里正确的做法是用自己的数据和自己的查询场景复测一遍再下结论。2.3 资源占用与运维成本差异性能只是换引擎的原因之一资源占用和运维成本的差异才是让我决定迁移的真正推动力。ES在4G堆内存下运行加上Lucene的堆外内存和段缓存实际占用稳定在5G到6G之间空闲时也不会降到更低因为JVM会尽量把内存吃满。索引文件在磁盘上占用了约11G空间因为ES默认有一个副本实际存储量还要翻倍。Meilisearch同样处理这一千万条数据索引文件约4.5G运行时内存占用在2G到3G之间。也就是说同样是8G内存的机器跑ESMongoDB可能已经很紧张跑Meilisearch还能再塞下两三个业务服务。运维层面差距更大。ES要操心配置文件、JVM参数、分片分配策略、索引生命周期管理、脑裂问题、滚动重启Meilisearch只有一个二进制文件或者一个Docker容器配置项少到一只手数得过来。升级就是换镜像重启索引会自动做兼容性迁移我还没有遇到过需要手动干预的情况。一句话总结我的实际感受ES像是一辆重型卡车装得多但开起来累Meilisearch像是一辆性能跑车载不了太多货但在你需要的路况上能给你极致的加速感。选哪个取决于你到底在拉货还是在跑赛道。3. 快速接入Meilisearch部署与索引3.1 Docker部署与初始化配置Meilisearch的安装部署是我见过所有搜索引擎里最简单的简单到第一次用时我怀疑自己漏了什么步骤。官方提供Docker镜像一条命令就能启动docker run -d --name meilisearch \ -p 7700:7700 \ -e MEILI_ENVproduction \ -e MEILI_MASTER_KEYyour_master_key \ -e MEILI_DB_PATH/meili_data \ -e MEILI_HTTP_ADDR0.0.0.0:7700 \ -v meili_data:/meili_data \ getmeili/meilisearch:v1.8在这里我强烈建议生产环境一定设置MEILI_MASTER_KEY不设置的话服务会以development模式启动数据完全没有读写保护任何人都能通过HTTP接口删掉你的索引。设置master key之后所有请求都需要带上Authorization: Bearer your_master_key头。还有几个环境变量值得关注。MEILI_MAX_INDEXING_MEMORY控制索引时最大内存用量默认是总内存的三分之二如果机器还跑着其他服务我建议主动调低到1G或2G。MEILI_LOG_LEVEL在排查问题时设成DEBUG平时设成INFO就够了。部署完成后你可以直接访问http://localhost:7700看到自带的管理后台一个叫Meilisearch UI的搜索调试界面。在这里可以手动搜索、查看索引状态、管理文档对前期调试非常友好。喜欢用docker-compose管理服务的参考这个最小化配置version: 3.8 services: meilisearch: image: getmeili/meilisearch:v1.8 container_name: meilisearch restart: always ports: - 7700:7700 volumes: - meili_data:/meili_data environment: - MEILI_MASTER_KEYchange_me_to_a_long_random_string - MEILI_ENVproduction - MEILI_MAX_INDEXING_MEMORY1gb volumes: meili_data:这个配置我在线上跑了大半年非常稳。3.2 索引设计与字段类型配置Meilisearch的索引设计思路和ES完全不一样。ES里你要先写mapping定义每个字段的类型、分词器、是否索引、是否聚合Meilisearch则会根据你写入的数据自动推断字段类型开箱即用。这听起来很美好但自动推断也会带来问题。比如价格字段如果是字符串12.5元就会被推断成字符串类型后面想做范围过滤就做不了。所以生产环境里我建议手动创建索引并设置字段类型不要完全依赖自动化。创建索引时需要设置primary key主键字段相当于ES的文档ID。手动创建索引的命令如下curl -X POST http://localhost:7700/indexes \ -H Authorization: Bearer your_master_key \ -H Content-Type: application/json \ -d { uid: products, primaryKey: id }然后配置字段类型和可搜索字段curl -X PATCH http://localhost:7700/indexes/products/settings \ -H Authorization: Bearer your_master_key \ -H Content-Type: application/json \ -d { searchableAttributes: [title, description, brand], filterableAttributes: [category, price, stock], sortableAttributes: [price, sales], rankingRules: [words, typo, proximity, attribute, sort, exactness] }这里的filterableAttributes和sortableAttributes特别重要。如果你没有把某字段加入过滤属性列表后续用这个字段做过滤时会直接报错这是新手最常见的坑。searchableAttributes的先后顺序决定了搜索匹配的权重优先级排在前面的字段权重更高我一般把最重要的标题字段放在第一位。3.3 数据写入与任务系统Meilisearch的数据写入是一个异步任务系统。你发送添加文档的请求后服务会立即返回一个 task id然后后台异步处理索引更新。要确认数据真的写进去了需要轮询任务状态curl -X POST http://localhost:7700/indexes/products/documents \ -H Authorization: Bearer your_master_key \ -H Content-Type: application/json \ -d [ { id: 1, title: 机械键盘 108键 防水, description: 办公游戏两用支持热插拔, brand: ExampleBrand, category: 电脑外设, price: 299.0, stock: 500 } ]响应会返回一个taskUid然后查询这个任务的状态curl http://localhost:7700/tasks/1 \ -H Authorization: Bearer your_master_key状态从enqueued变成processing再变成succeeded才算真正写入完成。如果是大容量数据导入不要一条一条提交用批量接口一次提交几千条到几万条吞吐会高很多。这里需要提醒一下异步任务系统是Meilisearch和ES一个很大的体验差异。ES的写入接口是同步返回的写完马上能查到。Meilisearch的写入有延迟通常几百毫秒内完成但如果你代码里刚写完就立刻去搜很可能搜不到需要做任务状态等待或者接受最终一致性的语义。对于Java后端我建议用官方Java SDK处理写入。SDK本身就是异步设计的配合CompletableFuture可以做得很优雅。核心代码我列一下方便直接参考MeilisearchClient client new MeilisearchClient.Builder() .host(http://localhost:7700) .apiKey(your_master_key) .build(); Product product new Product(1, 机械键盘, 办公游戏两用, 299.0); CompletableFutureTaskInfo future client .index(products) .addDocuments(product); future.whenComplete((taskInfo, error) - { if (error ! null) { System.err.println(写入失败: error.getMessage()); return; } System.out.println(任务已受理: taskInfo.getTaskUid()); });等任务成功的逻辑我封装了一个轮询方法间隔500毫秒查一次最多等5秒超过就抛出异常走重试。这个方案在生产环境跑了很久没出过问题。4. 搜索体验调优实战4.1 中文搜索与分词调优很多人担心Meilisearch对中文搜索支持不好我用下来感觉这个顾虑该放下了。Meilisearch使用的是内部的Charabia分词库默认就能识别中文句子并做合理切分。你直接搜机械键盘不会因为它被拆成机械和键盘两个词就搜出毫不相关的结果。当然默认分词不可能是万能的。中文搜索最常见的痛点是人名、品牌名、专有名词被错误切分。比如搜戴森的时候如果数据里出现戴森吸尘器和戴森电风扇默认效果其实已经不错但如果你的行业有大量专业术语建议引入Meilisearch的字典分词或者停用词功能。你可以通过设置stopWords来过滤那些对搜索毫无帮助的虚词比如中文里的的、了、和、是这类。这个配置在settings接口里curl -X PATCH http://localhost:7700/indexes/products/settings \ -H Authorization: Bearer your_master_key \ -H Content-Type: application/json \ -d { stopWords: [的, 了, 和, 是, 在, 与, 或] }分词器整体策略上我的经验是不要过度干预。Meilisearch的匹配算法自带typo容错和前缀匹配很多你觉得必须要精确分词的场景它用模糊匹配就把问题解决了。4.2 排序、过滤与分页搜索不能只返回结果还要能按业务规则排序和过滤。Meilisearch的搜索请求里直接带上filter和sort参数就行。一个典型的电商搜索场景按分类过滤、价格区间约束、按销量排序。请求长这样curl -X POST http://localhost:7700/indexes/products/search \ -H Authorization: Bearer your_master_key \ -H Content-Type: application/json \ -d { q: 机械键盘, filter: [ category 电脑外设, price 100 AND price 500, stock 0 ], sort: [sales:desc], limit: 20, offset: 0 }这里要注意几个语法细节。过滤条件是一个数组多个条件之间默认是AND关系。如果你想表达OR逻辑需要在同一个字符串里写成category 键盘 OR category 键帽这种形式。数值比较用price 100字符串用或者!布尔字段直接用is true或is false。排序方面最多可以指定多个排序字段用逗号分隔[sales:desc, price:asc]。但必须在settings里先把这些字段加进sortableAttributes否则请求会报错。分页方式Meilisearch给了两种。一种是传统的offsetlimit简单直接适合后台管理页面另一种是基于游标的hitsPerPagepage适合面向用户的业务场景。数据量少的时候区别不大数据量达到几十万条以后用offset深翻页会越来越慢这时候建议切到page模式。4.3 从ES迁移的三种姿势如果你已经决定从ES迁移过来这边给你三个参考方案按迁移代价从小到大排序。第一个方案是双写热切换。业务写入时同时写ES和Meilisearch先用一小段时间验证Meilisearch的查询结果能否覆盖业务需求确认无误后把读流量切到Meilisearch最后下线ES。这个方案适合对可用性要求高的场景代价是迁移周期内要维护两套存储。第二个方案是索引重建一次性迁移。把现有ES数据全量导出成JSON文件用脚本清洗后批量导入Meilisearch。这个方案最简单代价是迁移期间业务搜索可能暂时降级适合数据量不大、允许短时间停服的业务。第三个方案是对外查询保持ES兼容层。如果你不想改业务代码太多可以写一个薄薄的适配服务请求进来的时候翻译成Meilisearch查询出去的时候把结果格式包成ES的响应结构。这个方案适合业务代码耦合较深、没法快速改完的场景。我实际执行的时候用的是第二种方案因为业务正好在迭代期停服窗口可以安排。全量导出用ES的scroll接口扫数据导入用Meilisearch的批量文档接口一千万条数据大约十分钟就迁移完了比我预想的快很多。5. 常见问题与排查技巧5.1 搜索没结果或者结果不准遇到搜不到或者搜索不准的问题先检查三件事。第一件事确认字段是否在searchableAttributes里。如果搜索时指定了字段范围而字段没配置为可搜索返回结果会是空的而且不一定报错。第二件事看查询词是不是被过滤了。有些词如果被加进stopWords搜索的时候会被忽略结果可能完全匹配不到。第三件事检查是否存在类型推断错误。比如数据里id字段有数字也有字符串自动推断成字符串之后搜索数字反而搜不到。还有一种很隐蔽的情况是主键重复。Meilisearch默认如果主键重复新文档会覆盖旧文档。如果你导入的是历史数据不小心把同一条数据导了两次旧数据就被悄悄盖掉了搜索结果自然会少。定位这些问题最快的方式是把搜索请求的showRankingScore参数设为true响应里会返回每条结果的匹配分数和rank详情一眼就能看出是哪个环节拉低了分数。5.2 写入任务一直卡住写入任务卡住通常有三个原因。第一个原因是磁盘空间满了。Meilisearch写索引需要临时空间如果磁盘满了任务会一直处于处理中状态查看系统日志能发现No space left on device报错。第二个原因是任务队列积压太严重。一次性提交了太大的批量数据后面的任务会排队等待这个不是异常但你得评估是否需要调大MEILI_MAX_INDEXING_MEMORY来加速索引构建。第三个原因是主键字段缺失。文档里没有主键字段的话任务状态会变成failed你可以通过查询任务详情拿到具体错误信息。排查步骤我总结成一个通用流程先看任务状态再查服务日志最后确认资源水位。生产环境里我强烈建议给任务系统做一个简单的监控报警任何任务长时间处于processing状态就立刻告警。5.3 哪些场景不要换掉ES最后必须说点得罪人的真心话。Meilisearch虽然轻快但也不是万能钥匙有几类场景我会旗帜鲜明地劝你留在ES上。第一类是需要超大规模数据存储的场景。数据量过亿甚至上百亿ES的水平扩展是更成熟的方案。Meilisearch虽然也支持多副本和多节点但生态和工具链没有ES那么成熟遇到极端情况能搜到的解决方案也更少。第二类是需要做复杂聚合分析的场景。ES的aggs可以做桶聚合、指标聚合、嵌套聚合配合Kibana做数据可视化非常顺手。Meilisearch目前更擅长的是搜索能力在分析这块还处于起步阶段。第三类是你已经在ES生态里投入了大量基础设施的场景。如果你已经搭好了基于Logstash的数据管道用Kibana做了各种大盘还有一堆基于ES的插件在运行那为了搜索速度去换引擎整体的迁移成本可能远远超过性能收益。技术选型从来没有银弹。快不是唯一标准适合才是。收尾一点个人体会折腾完这次迁移我自己有一个很深的感受很多团队使用ES不是因为业务需要ES而是因为大家都用ES选型的时候不用背锅。但技术选型最怕的就是只考虑不犯错而不考虑最合适。Meilisearch用极低的运维成本就能覆盖我业务里80%的搜索需求剩下20%的复杂分析场景我保留了一个ES节点专门处理两边各自发挥优势反而是最舒服的架构。最后分享一个我自己磨合出来的小技巧不要把Meilisearch当成ES的完全替代品而是把它看成搜索场景的专用加速器。业务的主存储和事务处理该用什么数据库就用什么搜索服务独立部署一套Meilisearch数据通过消息队列异步同步过去。这样主业务完全不受搜索引擎升级或重建索引的影响搜索服务的性能也始终能保持在一个很高的水平上。反正我现在是再也回不去单节点ES那套重运维的日子了。
返回列表