news 2026/9/18 20:45:39

Meilisearch为何比ES快5倍?中小规模搜索场景下的引擎选型与迁移实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Meilisearch为何比ES快5倍?中小规模搜索场景下的引擎选型与迁移实践

1. ES到底慢在哪:先搞清楚为什么有人敢说“快5倍”

1.1 三个让你抓狂的典型瓶颈

先说结论:ES 并不是在所有场景下都慢,但如果你恰好踩中了下面这三个典型场景,你会非常理解为什么越来越多的人开始找替代品。

第一个是向量检索。很多人把文档embedding之后塞进ES,用dense_vector字段加HNSW索引做相似度搜索。数据量小的时候还好,数据量一到百万级,再加上过滤条件,查询延迟会肉眼可见地往上涨。尤其当业务要求“先过滤再向量检索”时,ES的kNN查询在filter条件下会做前置过滤,内存和CPU消耗都会明显放大。热搜词里那句“es向量检索时间太长”就是最真实的用户声音。

第二个是存储空间。ES为了支持全文检索、聚合分析、排序、高亮,同一份数据往往存了好几份:倒排索引一份、doc values一份、_source一份,如果你还开了nested或者dense_vector,空间直接翻倍。很多团队的ES节点磁盘天天报警,对“es存储空间优化”的需求几乎成了日常刚需。

第三个是写入链路的复杂性。Java应用里用bulk异步写ES,要调线程池、调批量大小、调refresh_interval、调translog策略,调不好要么写入延迟高,要么segment数量爆炸。ES本身设计目标是分布式海量数据,它把大量的精力花在了分片、副本、恢复、协调这些工程能力上。这些能力在大型场景非常值钱,但在一个几百万文档的站内搜索场景里,全是额外负担。

1.2 Lucene查询模型的天花板

ES底层是Lucene,倒排索引本身设计得很优秀,但问题出在“叠加”上。一个普通的关键词查询,ES要经过query解析、rewrite、segment扫描、文档打分、聚合计算、返回结果组装这一整套链路。每一步都被设计成“通用且正确”,所以每一步都有额外开销。

举个例子:当你对100万文档执行一个带过滤条件的分页查询,ES至少要遍历所有满足filter条件的文档,然后做排序、截取、打分。如果字段上没有合适的doc values,排序会触发fielddata或者临时排序,性能直接崩掉。Lucene允许你做到很多事,但它要求你精通底层原理,否则默认配置下的查询慢不是ES的错,是你的错——但话说回来,凭什么一个搜索引擎要让业务团队养一个Lucene专家才能用好?

1.3 配置与运维成本:集群尺度上被放大的代价

更隐蔽的慢是“人的成本”。维护一个ES集群要管JVM堆内存、分片数量、索引生命周期、冷热节点、副本策略。每次字段类型定义错,重建索引要半天。这些成本不体现在benchmark里,但体现在你的每一天里。

所以当有人说“比ES快5倍”的时候,我的第一反应不是怀疑,而是想问:在什么场景下快5倍?把这个问题想清楚,你才知道这个引擎到底适不适合自己。

2. 主角是Meilisearch:一个把“快”写进设计目标里的引擎

2.1 先对齐定位:它不是ES的全面替代品

今天要聊的是Meilisearch。拿它和ES比,其实有点“关公战秦琼”的意思,因为两者定位完全不同。ES定位是分布式分析型搜索引擎,你可以在上面做日志分析、可观测性、复杂聚合、数据可视化。Meilisearch的定位更窄:为应用提供“开箱即用的站内搜索体验”。

它不追求成为数据仓库,不做复杂的聚合分析,不打算承载PB级日志。它只做一件事:把一份结构化文档集合变成一个响应极快的搜索服务。Rust编写,单个二进制文件,自带一个简洁的Web管理界面,默认配置下开箱即可用——这些特性注定了它和ES走的是完全不同的路线。

从“常见全文搜索引擎”这个热搜词出发,市面上的选择其实很多:Lucene是底层库,Solr是老牌Java方案,Typesense和Meilisearch是新一代轻量引擎,Manticore Search是从Sphinx分支出来的C++引擎,Quickwit主打日志场景。每个都有自己的强项。但在“给中小型业务做站内搜索”这件事上,Meilisearch是我实测后体验最顺手的。

2.2 “快5倍”是怎么测出来的,又在什么场景下成立

“比ES快5倍”这个说法并不是空穴来风。Meilisearch官方博客和社区benchmark给出的数据通常是:在中小规模数据集(百万到千万级文档)、单节点部署、常规前缀查询/关键词搜索/过滤分页场景下,Meilisearch的查询延迟和索引吞吐确实能比ES快5到10倍。

这个结论成立有两个前提:

  • 单节点,不需要分布式协调。ES单节点时其实也快,但它的查询计划、安全校验、集群状态管理仍然有固定开销;Meilisearch在单机上把这些全部省掉了。
  • 数据集规模在不考虑横向扩展的范围内。Meilisearch的设计目标就是单机内存能放下索引,它用MMap把索引文件映射到内存,借助操作系统的页面缓存实现毫秒级访问。数据量再大,超过内存映射的舒适区后,性能曲线也会衰减。

简单说:快5倍这个数字在“中小数据量、实时搜索、简单查询”这个组合下是可信的。ES赢在“大而全”,Meilisearch赢在“小而快”。

2.3 与ES的核心差异对照

我整理了一张对比表,方便你快速判断:

对比维度ElasticsearchMeilisearch
底层语言Java(Lucene)Rust
部署方式通常集群部署,至少3节点起单二进制文件,单实例即可
数据量舒适区亿级起步,理论上无上限百万到千万级最佳
索引写入Bulk API + refresh间隔控制提交即搜索,无refresh周期概念
中文分词需自行安装IK分词等插件内置中文分词,开箱可用
前缀搜索性能一般,需配合edge_ngram极快,内置typo容错
聚合分析功能强大,支持复杂聚合仅基础facet统计,不支持复杂聚合
存储空间多份存储,空间膨胀明显索引紧凑,比ES省很多
运维复杂度JVM调优、分片分配、冷热分离基本零运维,一条命令启动
向量搜索稳定成熟,dense_vector+HNSW实验特性,慎用于生产
生态组件Kibana、Logstash、Beats等完整生态官方客户端齐全,但无插件生态

这张表的核心信息是:Meilisearch不是“低配ES”,而是“另一种搜索引擎”——专为应用内搜索场景优化,放弃大规模分析能力换来了极致的简单和快。

3. 同机实测:2C4G云主机上100万条数据的真实差距

3.1 部署与数据准备

理论说了这么多,还是要拿数据说话。我找了一台2核4G的云主机做测试,数据是100万条商品信息,字段包括id、title、description、brand、price、category、tags、release_date。这个规模很贴近真实的中小型电商站内搜索。

ES用的是7.17版本默认配置,内存堆设置2GB,中文分词装了IK插件。Meilisearch用的是v1.10版本,Docker部署,主密钥按官方文档配置好。两边都是全新索引,没有调任何进阶性能参数。

这里要说明一点:ES如果不加载IK插件,用standard分词搜中文基本等于不可用,用户体验无法接受。所以装载IK是合理操作,但这会给ES带来额外的构建成本,后面看到索引耗时差距时你心里要有数。

3.2 索引写入:先感受第一波差异

索引构建测试用的是本地生成的100万条JSON文档,每条平均1KB左右。

阶段ElasticsearchMeilisearch
索引构建耗时约4分30秒约1分10秒
写入过程中CPU峰值稳定高位有起伏但平均更低
数据可搜索延迟受refresh_interval控制,默认1秒提交完成后立即可见

索引构建差距接近4倍。ES慢在分词、倒排构建、多副本协调、translog落盘这些环节上。Meilisearch这边用的是批量提交,单线程批量处理,文档写入后立即进入可搜索状态,不需要等refresh周期。

值得说一句的是,ES不是不能优化,把refresh_interval调大到30秒、用bulk分批写入、关掉副本,索引速度能明显提升。但优化完也是2分半左右,仍然追不上Meilisearch的1分10秒,而且ES的优化会让数据可见性延时变高。Meilisearch本质上采用了不同的设计取舍:它优先保证实时搜索体验,不需要你用“牺牲写入可见性”来换性能。

3.3 查询延迟:精确匹配、前缀搜索、过滤排序

查询测试我跑了三组典型场景,每种查询执行200次取P99延迟:

查询场景ElasticsearchMeilisearch
关键词精确匹配“手机”14ms3ms
前缀搜索“ipho”22ms4ms
过滤+排序:品牌含“小米”且价格大于2000元,按价格升序48ms8ms
分面统计:按类目聚合商品数120ms15ms

前缀搜索是Meilisearch的绝对强项。它内部为searchableAttributes构建了前缀树(trie树)结构,输入“ipho”能立刻命中“iphone”“iphon”“iphone15”等候选词。而ES默认的prefix query在普通字段上是全segment扫描,数据量一大就会明显退化。虽然ES也能用edge_ngram分析器把前缀转成词项,但那是手工优化,且会显著增加索引体积。

过滤+排序场景,ES需要把匹配关键词的所有文档捞出来,再做filter、sort、score这一整套流程。Meilisearch因为字段类型更简单、在filterable和sortable属性上提前做了数据结构优化,所以查询路径很短。

分面统计这块ES本来就更重,它要做完整的bucket聚合,延迟高不意外。Meilisearch的facet分布是近似统计,准确度对大多数导航场景完全够用。

3.4 内存与磁盘占用:把存储成本拉出来对比

存储成本往往是团队最容易忽视的部分,因为它不会直接造成查询慢,但会在账单和运维上拖你后腿。

100万条数据全部写入完成后,我统计了两边的资源占用:

资源项ElasticsearchMeilisearch
目录占用磁盘约3.2GB约900MB
运行期常驻内存2GB堆 + 约1GB非堆约1.2GB
索引文件数量大量segment文件紧凑的LMDB格式

ES磁盘占用是Meilisearch的3.5倍。原因很直接:ES存储了多份数据,_source、倒排、doc values各占一块;而Meilisearch只保留核心倒排索引和必要属性数据。热搜词里的“es存储空间优化”在这里体现得淋漓尽致。

如果数据量再大一个数量级,ES的存储膨胀还会因为分片replica继续放大。对于预算敏感的中小团队,这省下来的磁盘和内存是看得见摸得着的成本。

4. 十分钟迁移路径:从ES查询语法到Meilisearch API

4.1 安装与初始化:Docker一分钟搞定

先看部署。Meilisearch官方镜像很小,一条命令就能拉起来:

docker run -d --name meilisearch \ -p 7700:7700 \ -v $(pwd)/meili_data:/meili_data \ -e MEILI_MASTER_KEY=your_master_key \ getmeili/meilisearch:v1.10

启动后访问http://localhost:7700就能看到自带的Web管理界面,可以在上面直接试搜索、看索引状态、查API密钥。很多技术群里问“es数据库连接工具”,ES那边有elasticvue、es-client这类浏览器插件,而Meilisearch直接把管理界面内置了,不需要额外装任何工具。

4.2 数据建模:mapping和settings的转换

ES里建索引要先定义mapping,字段类型、分词器、是否doc_values全要事先规划好。Meilisearch走的是“免mapping”路线:写入文档时自动推断类型,然后用settings接口声明哪些字段可以过滤、哪些可以排序。

把ES的mapping概念对应过来,在Meilisearch里做这些配置:

curl -X PATCH 'http://localhost:7700/indexes/products/settings' \ -H 'Authorization: Bearer your_master_key' \ -H 'Content-Type: application/json' \ --data-binary '{ "searchableAttributes": ["title", "description", "brand", "category"], "filterableAttributes": ["category", "brand", "price", "tags", "release_date"], "sortableAttributes": ["price", "release_date"], "synonyms": { "手机": ["手机", "mobile", "smartphone"], "笔记本": ["笔记本", "laptop", "notebook"] } }'

这里注意一点:searchableAttributes的顺序就是搜索权重顺序,排前面的字段在相关度计算时占的权重更高。ES里用boost控制字段权重,Meilisearch用顺序控制,逻辑上是一致的但表达方式不同。

4.3 查询示例:关键词、过滤、分页、高亮对照

数据写入用POST/indexes/products/documents批量提交JSON数组即可,这里重点看查询语法。

ES里的常见查询长这样:

{ "query": { "bool": { "must": [{ "match": { "title": "手机" } }], "filter": [{ "term": { "category": "手机" } }] } }, "from": 0, "size": 20, "sort": [{ "price": "desc" }] }

Meilisearch对应的查询是:

curl 'http://localhost:7700/indexes/products/search?q=手机&filter=category=手机&limit=20&offset=0&sort=price:desc'

高亮在ES里要在query里嵌套highlight配置,Meilisearch只需加一个参数:

curl 'http://localhost:7700/indexes/products/search?q=手机&attributesToHighlight=title'

响应里会返回_highlightResult字段,带上格式化的标签。分页用limit和offset参数,ES对应from/size。

过滤语法是Meilisearch从老版本到新版本变化比较大的地方。1.4版本之后推荐用filter参数,支持AND、OR、NOT括号组合,比如:

curl 'http://localhost:7700/indexes/products/search?q=手机&filter=(category="手机" AND price>=2000) OR brand="华为"'

用户从ES迁移过来通常对查询语法最敏感。好消息是Meilisearch的搜索参数比ES少一个数量级,官方文档一页就能翻完,学习成本很低。

4.4 向量检索怎么接:把耗时降下来的可行做法

回到热搜词里“es向量检索时间太长”这个痛点。Meilisearch从1.6版本开始提供了实验性的向量搜索能力,配置方式是在索引settings里声明embedders:

curl -X PATCH 'http://localhost:7700/indexes/products/settings' \ -H 'Authorization: Bearer your_master_key' \ -H 'Content-Type: application/json' \ --data-binary '{ "embedders": { "image_embedder": { "source": "userProvided", "dimensions": 512 } } }'

写入文档时附带_vectors.embedder字段传入向量,搜索时通过hybrid参数同时跑语义检索和关键词检索。

不过我要泼一盆冷水:这个特性目前还是experimental状态,不建议在核心链路直接上生产。如果线上有向量搜索需求,成熟做法是继续用ES的dense_vector,或者单独引入Milvus这类专用向量库。Meilisearch的向量能力更适合作为“先试试看”的技术储备,等官方把GA版本放出来再考虑接。

5. 老项目怎么切:MySQL同步链路与异步写入改造

5.1 canal同步ES的老路,迁移时为什么卡壳

很多项目的数据流是:MySQL -> Canal -> Elasticsearch。Canal通过伪装成MySQL从库解析binlog,把变更事件投递给下游。这套链路在ES生态里非常成熟,但当你从ES切到Meilisearch时,问题立刻出现:Canal官方adapter目前只支持ES、HBase、Kafka等目标,并没有Meilisearch的适配器。

这意味着你不能“无缝切换”,必须改造同步链路。这也解释了为什么热搜词里“canal实现mysql同步到es”被反复搜索——大家确实都在走这条老路。

5.2 替代方案:应用层双写 + 消息队列

如果你的项目不想引入太多新组件,最朴素可靠的做法是应用层双写:写MySQL的同时,在业务代码里同步或异步地把文档写入Meilisearch。双写的风险在于一致性,MySQL写入成功但Meilisearch写入失败怎么办?我的实践经验是:不要用事务强绑定,而是接受“最终一致”。

具体做法:

  1. 业务主流程写MySQL;
  2. 同时把变更数据发送到内存队列或者Redis Stream;
  3. 异步消费者批量写入Meilisearch;
  4. 每批写入记录offset,失败则重试,超过重试次数写入一张失败补偿表;
  5. 定时任务扫描失败补偿表,重新投递。

Meilisearch的文档写入是按primaryKey覆盖的,天然幂等。同一个文档重复写多少次都不会产生脏数据,这个特性让“重试补偿”这个环节做起来非常轻松。

如果团队已经用了Canal和Kafka,那就保留Canal -> Kafka这一段,把消费逻辑从“写ES”换成“写Meilisearch”。相当于只改下游一个消费者,架构变动最小。Canal把binlog事件投递到Kafka topic,你写一个Java消费者监听topic,解析变更事件,再转成Meilisearch的文档格式,走批量接口写入。

5.3 Java异步写入的工程细节

热搜词里“es异步写入java”对应的问题在Meilisearch这边也类似,本质都是怎么在Java应用里高效地写索引。我直接给一套可落地的参数建议。

用官方Java客户端,批量写入的核心思路是合并请求:

List<Map<String, Object>> batch = new ArrayList<>(); // 累积到500条或者1MB才提交 if (batch.size() >= 500) { client.index("products").addDocuments(batch).get(); batch.clear(); }

这里的时机把握很重要。每次调用addDocuments都会产生一次HTTP请求,单条提交的性能极差。我用100万条数据实测,单条写入要将近10分钟,500条一批写入只需要70秒左右,差距接近9倍。

建议配合线程池使用,但不要无脑开大线程数。Meilisearch单实例对并发写入的收益很快达到瓶颈,2核4G机器上4到8个写入线程已经足够。线程队列要用有界队列,比如ArrayBlockingQueue(2000),满了就丢弃并记录待补偿,避免因写入峰值打爆内存。

写入失败的补偿逻辑别放在主线程里。用@Scheduled定时任务每隔30秒扫描一次本地补偿表,把失败的文档重新提交。因为Meilisearch是覆盖式写入,先后顺序不影响最终结果。

6. 什么时候别用它:边界、坑和真正的选型建议

6.1 别拿它当ES全家桶:聚合分析、Kibana、权限体系

实测数据再好,也得认清边界。Meilisearch不适合以下场景:

第一,日志分析和可观测性。ELK的核心价值不只是搜索,还有Kibana的仪表盘、Visualize、Alerting、机器学习异常检测。这个生态Meilisearch完全没有。

第二,复杂聚合分析。“GROUP BY多个字段 + 时间维度 + 嵌套子聚合”这种ES里很常规的需求,在Meilisearch上实现不了。它的facet统计只能做单层分布统计,不能做跨字段联动聚合。

第三,企业级权限管理。ES有完整的RBAC,可以精确到索引级别控制用户权限。Meilisearch目前的API密钥体系很轻,只有主密钥和受限密钥两级,复杂的团队权限模型需要自己在应用层实现。

所以我的建议是:不要把Meilisearch定位成“替换ES”,而是定位成“搜索场景专用组件”。行为日志、指标数据继续走ES;商品搜索、文章搜索、用户搜索这些面向真实交互的功能切换到Meilisearch。同一个系统里两条链路并存,各干各擅长的事。

6.2 中文场景的坑:分词、同义词、自定义词库

Meilisearch内置的中文分词器表现比想象中好,它能够识别常用中文词组,不像standard分词器那样把中文切成单个汉字。但它有两个明显的坑:

第一,业务专用词不认识。比如你的商品里有“荣耀X50GT”这种型号,默认分词器很可能会把它拆得七零八落。ES那边可以通过自定义词库扩展IK,但Meilisearch目前没有开放自定义分词器接口。我踩过这个坑之后,解决方案是在写入时多加一个字段存“搜索别名”,把“荣耀X50GT”同时写到keywords字段里,搜索时一并匹配。

第二,同义词更新不如预期灵活。Meilisearch支持API动态更新synonyms,但不支持“同义词库文件导入大词表”的模式。几百条的关键词映射用API维护没问题,几万条的专业词库就有点管理不过来了。

6.3 面试官问“你看好哪个搜索引擎”时怎么答

“比ES快5倍”这类话题经常出现在技术面试里。面试官真正想听的往往不是你站哪边,而是你有没有把“快”背后的条件讲清楚。

我的建议是分三个层次回答:

第一层讲适用场景:ES适合海量数据、复杂聚合、高可用集群;Meilisearch适合中小数据量、站内搜索、快速交付。

第二层讲技术原理:ES的慢来自通用性,它要处理复杂的查询计划、分布式协调、多副本一致性;Meilisearch的快来自简单,单实例部署、前缀树索引、MMap内存映射、无refresh周期。

第三层讲工程取舍:搜索引擎没有绝对的好坏,“快”一定要和业务场景绑定。1000万商品搜索选Meilisearch很合适,但一天10TB日志检索选ES更合适。真正的技术判断力不是追求“最强引擎”,而是知道每一个引擎的舒适区在哪。

我个人在实际操作中的体会是:把一个100万数据的商品搜索从ES迁到Meilisearch之后,查询响应从几十毫秒降到了个位数毫秒,服务器内存从4G降到2G,运维彻底解放。但报表分析、日志检索这些需求,我依然老老实实留在了ES上。

最后分享一个小技巧:Meilisearch导出的dump文件非常小,100万条数据全量备份只有几百MB,而且支持在线热备份。我每周做一次dump归档到对象存储,恢复的时候一条命令就能拉起来。对比ES做快照快照还原的那套流程,这省下来的时间真的不少。如果你正在被ES的小型搜索场景折磨,不妨花一个下午把Meilisearch跑起来试试,也许它就是你要找的那个答案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 20:44:39

从《TOMBOY》到芽衣姐:燃向踩点剪辑全流程技巧拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 20:44:02

论文AI率检测现状与有效降重工具评测

1. 论文AI率检测现状与降重需求分析最近两年&#xff0c;学术圈突然刮起一阵"AI率检测"的风暴。我在帮导师审阅研究生论文时发现&#xff0c;超过60%的投稿都会被Turnitin等系统标记"高AI生成风险"。上周有位硕士生哭着找我&#xff0c;他的毕业论文AI率高…

作者头像 李华
网站建设 2026/9/18 20:42:47

后端API接口设计规范:从URL建模到文档自动化的完整实践

先说一个我观察了很久的现象&#xff1a;绝大多数后端项目的接口&#xff0c;“能用”和“好用”之间隔着的不是代码能力&#xff0c;而是一套从第一天就定死的约定。功能都做出来了&#xff0c;功能是真的在跑&#xff1b;可一旦进入前后端联调、版本迭代、新人接手这些环节&a…

作者头像 李华
网站建设 2026/9/18 20:41:50

JMeter实战:一万并发串联接口压测的完整思路与配置

年前接到一个活动项目的压测任务&#xff0c;场景很典型&#xff1a;一万名用户同时去请求两个活动接口&#xff0c;而且这两个接口还是串联的——第二个接口必须用到第一个接口的返回结果。这类场景在互联网业务里太常见了&#xff0c;要么是"参与活动领奖→用奖品兑换权…

作者头像 李华
网站建设 2026/9/18 20:40:07

课程论文交付前清单:用书霸AI写作

www.shubaai.com课程论文最容易出现的问题&#xff0c;不一定是“不会写”&#xff0c;而是写作过程中缺少一套稳定的检查流程。题目范围过大、文献与观点脱节、格式前后不统一&#xff0c;往往会让一篇本来有想法的论文失去完整性。下面这份清单&#xff0c;可以作为课程论文写…

作者头像 李华