如果你还在用ES扛所有的搜索场景,我建议你先看完这篇再做决定。过去两年我一直在维护一整套ES集群,日均搜索请求量几百万,集群规模也不算小,但真正让我开始反思的是一件小事:一个只需要在3万条商品数据里做关键字搜索的内部工具,ES从部署到调优花了两天,最后查询的P95延迟还在80毫秒上下徘徊。后来我把同样一个需求切到一个叫Typesense的搜索引擎上,从部署到上线只用了不到半天,P95延迟压到了10毫秒以内。这个差距让我开始认真研究:为什么会有比ES快5倍的搜索引擎?它是靠什么做到的?以及,我们到底什么时候应该换掉ES?
这篇文章不是要鼓吹“ES已死”,而是想把我对Typesense的完整测评、上线实操步骤、压测数据,以及替换过程中踩过的坑全部摊开来讲。如果你正在做站内搜索、垂直搜索,或者被ES的运维成本和查询延迟折磨,这篇应该能帮你省下不少时间。
1. 为什么我会去关注ES之外的搜索引擎
1.1 一个被ES托住的小需求
先说那个让我“破防”的内部工具背景。当时业务方提了一个很普通的需求:运营后台需要一个搜索框,从3万条商品资料里按名称、品类、品牌关键字搜索,打开商品详情,要求是结果秒出,最好还要支持拼音首字母缩写搜索,比如输入“jm”能匹配“洁面仪”这种。
当时团队里ES是现成的,大家理所当然就往ES上放。结果发现,即使数据量只有3万条,ES依然需要创建索引、设计mapping、考虑分片和副本,一次普通的搜索请求要经过HTTP层、查询解析、分布式协调、Lucene检索,再合并分片结果。在没怎么调优的情况下,接口P95延迟能冲到80到100毫秒。这个性能对内部的低频工具来说不算不能用,但这个响应速度背后消耗的资源却一点也不低:两个ES节点常驻内存里跑着,光JVM堆就给了8GB。
我后来想明白一件事:ES其实是被设计来解决更复杂的分布式全文检索和分析问题的,我用它来做一个3万条数据的关键词匹配,本质上就是“杀鸡用了牛刀”。而这个场景,恰恰是Typesense这类轻量级实时搜索引擎最擅长的地方。
1.2 ES性能瓶颈到底藏在哪
不是ES搜索引擎本身不行,而是ES的架构决定了它在很多场景下就是快不起来。我总结下来,ES的性能开销主要来自四个地方。
第一个是Lucene的段合并机制。ES底层的Lucene会不断把小段合并成大段,合并期间会有CPU和磁盘IO的开销,如果并发写入量高,频繁的段合并会直接影响查询延迟。
第二个是JVM的GC停顿和堆外内存管理。ES跑在JVM上,堆内存和操作系统页缓存之间隔着一层,GC一旦进入Full GC,整个集群的查询响应就会肉眼可见地卡顿。你明明给ES留了32GB内存,却没法保证所有数据都在热缓存里。
第三个是查询链路的复杂度。一条ES请求从协调节点进来,要解析DSL、重写query、分发到多个分片、每个分片各自检索、再汇总排序,最后取回top N。在数据量不大的场景下,这套分布式协议的开销占了大头,真正花在Lucene检索上的时间反而很少。
第四个是ES默认的字段类型映射很保守。很多字段默认是text加keyword双重映射,倒排索引、正排doc_values都会生成,写入放大和存储放大都相当可观。
这些瓶颈不是ES的bug,而是它为了支持复杂场景付出的代价。但问题在于,当你根本用不到那些复杂场景时,这些代价就成了纯损耗。
1.3 “快5倍”的结论从哪来
“Typesense比ES快5倍”这个说法最早出现在一些国外技术团队的性能对比博客里,后来Typesense官方文档也引用了这类数据。它不是官方编造的数字,而是有前提的对比:在典型产品内搜索场景、数据量千万级以下、内存能放得下索引、查询模式以关键字搜索加轻量过滤排序为主时,Typesense确实能跑出5到10倍的性能差距。
我自己的实测放在第4章,数据量是200万条商品记录,在同一台物理机上对比,简单关键词搜索场景下Typesense的QPS大概是ES的5.2倍,P95延迟只有ES的五分之一左右。不过我必须先把话说明白:这个数字不是普适的。如果你拿ES去做复杂的聚合分析,或者数据量大到索引完全没法放进内存,差距会迅速缩小,甚至反过来被ES吊打。
所以更准确的说法是:在“支持实时写入、快速检索、低延迟返回”的这类场景里,Typesense快5倍不是一句空话。
2. Typesense跑得快的内核逻辑
2.1 用Rust和C++写一个专门干搜索活的服务
Typesense的底层不是Lucene,而是用C++和Rust开发的。这件事本身就带来两个天然优势。
第一,没有JVM这一层,就没有GC停顿。ES的高延迟毛刺很大一部分来自JVM GC,而Typesense的内存管理是自己控制的,它的索引结构大部分是连续内存块,分配的代价极低,访问模式对CPU缓存非常友好。
第二,编译型语言启动后没有“预热”的过程。ES刚重启完,缓存是空的,热数据要一点点加载进OS页缓存,前几分钟的查询性能是很差的。Typesense启动时直接把索引加载到内存,服务一起来,查询性能就直接是满状态。
我做替换测试时有一个很直观的体感:容器重启之后,ES至少需要几分钟来“热身”,而Typesense从进程启动到能扛住高并发,几乎不需要等待。
2.2 内存优先,磁盘兜底
Typesense的设计哲学很直白——默认把索引整个放进内存。它官方给的数据是索引大概占原始JSON数据量的40%到50%,也就是1GB的数据,索引大小大概在400到500MB。一台32GB内存的机器,可以支撑几十GB级别的数据量做全内存检索。
这和ES有着本质区别。ES的索引很大部分依赖操作系统页缓存,只有热数据常驻内存,访问冷数据就要去读磁盘。而Typesense因为是设计为内存优先,默认所有数据就在内存里,没有“冷热”的区分,每次查询的耗时都保持稳定。
当然,纯内存方案会让人担心数据持久化。Typesense的做法是异步写磁盘,写入请求先进内存的写缓冲区,同时追加到预写日志和索引中,然后按批次刷盘。掉电最坏情况会丢极少量最后写入的数据,这一点和很多内存型数据库类似。生产环境建议至少跑一个副本节点,主节点挂掉时从副本恢复,这个我们在第5章展开。
2.3 查询路径比ES短得多
我把ES和Typesense处理一次请求的路径画出来对比,你就能理解差距在哪了。
ES一次搜索请求的完整路径大概是:客户端 -> HTTP入口 -> 解析DSL -> 查询改写 -> 协调节点路由 -> 分片查询 -> Lucene检索 -> 分片结果聚合 -> 全局排序 -> 返回。这里面光是分片协调和结果合并就占了不小的耗时。
Typesense的路径是:客户端 -> HTTP入口 -> 解析query_by字段 -> 直接走内存里的倒排索引和排序结构 -> 返回结果。它默认就是单节点全量索引,根本没有分片,也不需要跨节点归并,查询链路短了一大截。
有人会说,这种“单分片”设计在高并发下不会成为瓶颈吗?实际上单节点单副本的Typesense,在普通商品搜索场景下轻松扛住每秒几千次查询,瓶颈往往在网络层,而不是搜索引擎本身。如果你的查询量真的大到单节点扛不住,Typesense也支持横向扩展,但一定要先理解它的集群模式到底能做什么、不能做什么,这点后面专门讲。
2.4 默认参数就能跑出不错的效果
用ES最大的心累之处是默认配置没法直接达到最优。你得手动调分片数、副本数、refresh间隔、translog刷盘策略、字段mapping、分词器,还要在写入性能和查询延迟之间做权衡。我已经不止一次看到有团队上线一两个月后,因为没调好bulk批量参数导致段数爆炸,查询延迟翻倍。
Typesense这边,默认配置跑出来的效果就相当能打。它的schema设计得非常简单,字段主要是string、int32、float、bool、string[]等几类,每个字段可以单独声明是否要搜索、是否要排序、是否要作为过滤条件。没有复杂的映射链,没有嵌套类型的花式玩法。索引结构在创建collection时就确定了,所有标注了可搜索的字段自动进倒排索引,可过滤的字段自动进正排索引。
这种“少即是多”的设计意味着团队里不需要专门养一个ES专家才能跑好搜索。一个后端工程师看一遍官方文档,基本就能在一天内把一个搜索服务完整搭出来。
3. 从零搭建一个站内搜索服务
3.1 部署:不折腾,一个容器就起来
Typesense的部署相当轻量,官方提供了静态二进制和Docker镜像两种方式。我建议你直接用Docker,省去编译环境这一步。
下面这个命令在任意一台2核4G的Linux服务器上就能跑起来:
docker run -d \ --name typesense \ -p 8108:8108 \ -v /opt/typesense-data:/data \ -e TYPESENSE_API_KEY=your-secure-api-key \ -e TYPESENSE_DATA_DIR=/data \ -e TYPESENSE_ENABLE_CORS=1 \ typesense/typesense:27.1几个参数我解释一下:
-p 8108:8108:默认的HTTP端口是8108,客户端SDK和curl请求都走这个端口。TYPESENSE_API_KEY:这是你所有请求的身份凭证,Typesense没有内置用户体系,对外的请求全靠这个Key鉴权。生产环境一定要设一个足够长的随机字符串,千万不要用默认值。/opt/typesense-data:索引持久化目录,必须用外部存储卷挂载,否则容器销毁索引就全没了。
启动之后可以验证一下服务状态:
curl -s http://localhost:8108/health返回{"ok":true}就说明服务正常了。
3.2 设计集合并导入第一批数据
Typesense里和ES的index对应的概念叫collection,翻译过来是“集合”。创建集合之前,你要先想清楚哪些字段要参与搜索、哪些要用来过滤、哪些要用来排序。
我以电商商品搜索为例,设计一个简单但完整的schema:
{ "name": "products", "fields": [ {"name": "id", "type": "string"}, {"name": "title", "type": "string"}, {"name": "category", "type": "string", "facet": true}, {"name": "brand", "type": "string", "facet": true}, {"name": "price", "type": "float", "sort": true}, {"name": "sales", "type": "int32", "sort": true}, {"name": "tags", "type": "string[]", "facet": true, "optional": true} ], "default_sorting_field": "sales" }说几个容易踩坑的细节:
- 如果你希望某个字段能在返回结果里作为聚合用的facet,比如按品牌筛选、按分类分组,那就要加上
"facet": true,否则前端展示不出来。 - 需要按价格排序或按销量排序的字段,一定要加
"sort": true,Typesense对排序字段有单独的索引预计算。 default_sorting_field是必填项,它决定了用户不指定排序时的默认顺序,建议选一个数字类型的字段。
创建集合:
curl -X POST http://localhost:8108/collections \ -H "X-TYPESENSE-API-KEY: your-secure-api-key" \ -H "Content-Type: application/json" \ -d @schema.json然后准备一份JSONL格式的数据文件,每行一条商品记录:
{"id": "p001", "title": "iPhone 15 Pro 256G 原色钛金属", "category": "手机", "brand": "Apple", "price": 8999.00, "sales": 23000, "tags": ["5G", "旗舰"]} {"id": "p002", "title": "小米14 Ultra 512G 黑色", "category": "手机", "brand": "小米", "price": 6499.00, "sales": 15000, "tags": ["5G", "影像"]}批量导入:
curl -X POST http://localhost:8108/collections/products/documents/import?action=create \ -H "X-TYPESENSE-API-KEY: your-secure-api-key" \ --data-binary @products.jsonl \ -H "Content-Type: text/plain"导入接口支持批量,实测单批次建议控制在1000条到5000条之间,一条JSONL一行,最后不需要加额外的分隔符。批量导入的速度非常快,200万条数据在这种方式下大概几分钟就能进完,这还是单线程curl的结果,换成并发导入会更快。
3.3 搜索API的使用要点
Typesense的搜索接口非常简单,全部走HTTP GET请求,参数都在query string里。
一个基础搜索示例:
curl -s "http://localhost:8108/multi_search" \ -H "X-TYPESENSE-API-KEY: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "searches": [ { "collection": "products", "q": "手机", "query_by": "title,tags", "filter_by": "price:>=1000 && price:<=8000", "sort_by": "sales:desc", "facet_by": "category,brand", "page": 1, "per_page": 20 } ] }'我来逐个拆解这些参数:
q:查询关键字,支持普通文本、前缀匹配、错字容忍等能力。query_by:指定在哪些字段里搜索,多个字段用逗号分隔。filter_by:过滤条件,语法直观,支持数值范围、字符串匹配、布尔逻辑组合。上面的条件表示价格在1000到8000元之间。sort_by:排序方式,默认按sales降序。注意如果你用了文本相关性排序,可以写_text_match:desc,它会把文本匹配度作为第一排序条件。facet_by:聚合统计字段,返回结果里会带上每个类别的商品数量和每类别的品牌分布。
响应结构也很简化:
{ "facet_counts": [ { "field_name": "category", "counts": [ {"value": "手机", "count": 123}, {"value": "耳机", "count": 45} ] } ], "found": 168, "hits": [ { "document": { "id": "p001", "title": "iPhone 15 Pro 256G 原色钛金属", "price": 8999.00, "sales": 23000 }, "highlight": { "title": "<mark>手机</mark>" } } ], "search_time_ms": 3 }search_time_ms字段直接告诉你这一次搜索在引擎内部实际消耗的毫秒数。我实测最简单的单字段搜索经常是1到2毫秒,这在ES里是很罕见的。
3.4 实时写入与异步消费场景
“ES异步写入”之前是一个比较火的技术话题,核心是因为ES的Java原生客户端比较重,而且批量写入有refresh间隔、内存堆积、反压处理等讲究。Typesense在这方面要省心很多,因为它没有Java SDK的依赖负担,交互就是普通的REST API。
比如你的上游服务产生一条商品变更记录,发到消息队列,后面挂一个消费worker去更新Typesense里的文档。worker只需要把变更数据组装成JSON文档,调用导入接口即可。
一个Java端的异步写入示例大致是这样:
HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); String body = String.format( "{\"id\":\"%s\",\"title\":\"%s\",\"price\":%s}", product.getId(), product.getTitle(), product.getPrice() ); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:8108/collections/products/documents/import?action=upsert")) .timeout(Duration.ofSeconds(5)) .header("X-TYPESENSE-API-KEY", "your-secure-api-key") .header("Content-Type", "text/plain") .POST(BodyPublishers.ofString(body)) .build(); client.sendAsync(request, BodyHandlers.ofString()) .thenAccept(resp -> { if (resp.statusCode() != 200) { // 记录失败日志,投递回消息队列 } });注意这里用了action=upsert而不是action=create,这样文档如果已存在就覆盖更新,不存在就插入,不需要额外先走一遍查询判断。
我个人的建议是,不要一条一条地发HTTP请求,最好在worker里攒一批再发。具体可以把100条文档拼成一个JSONL格式的大字符串,一次POST上去。这样吞吐量会明显提升,对引擎的压力也小很多。
3.5 一个完整的Java异步写入示例
如果你用的是Spring Boot,结合WebClient可以做得很顺手。这里给出一个批量异步写入的简化版:
@Service public class ProductIndexWriter { private final WebClient webClient; public ProductIndexWriter(@Value("${typesense.url}") String url, @Value("${typesense.api-key}") String apiKey) { this.webClient = WebClient.builder() .baseUrl(url) .defaultHeader("X-TYPESENSE-API-KEY", apiKey) .defaultHeader("Content-Type", "text/plain") .build(); } public Mono<String> bulkUpsert(List<Product> products) { String body = products.stream() .map(this::toJsonl) .collect(Collectors.joining("\n")); return webClient.post() .uri("/collections/products/documents/import?action=upsert") .bodyValue(body) .retrieve() .bodyToMono(String.class); } private String toJsonl(Product p) { // 这里使用Jackson把Product转成紧凑JSON字符串 } }更新操作对应/documents/import?action=upsert,删除操作更简单:
curl -X DELETE http://localhost:8108/collections/products/documents/p001 \ -H "X-TYPESENSE-API-KEY: your-secure-api-key"这种纯REST接口带来的最大好处是:无论你是Java、Go、Python还是Node,只要会用HTTP,接Typesense基本没有学习成本。相比ES必须维护一套重量级客户端SDK、跟踪版本兼容性,这个体验确实舒服太多。
4. 实测数据:同一批商品数据下,ES和Typesense差多少
4.1 测试环境说明
为了尽可能公平,我把ES和Typesense跑在同一台物理机上,避免跨机器网络延迟差异。测试机器配置是4核8GB内存,固态硬盘,CentOS 7系统。这个配置在中小型团队里很常见。
数据集是我生成的一批模拟电商数据,总共200万条商品记录,字段包括商品ID、标题、品牌、分类、价格、销量、标签。标题是中文和英文混合的,带有一定的重复前缀,模拟真实商品的命名风格。
ES部署的是7.10版本,1个节点,默认配置,索引3个主分片1个副本(副本在同一台机器上其实不参与读,但保留常规生产配置),refresh_interval保持默认的1秒。Typesense是27.1版本,单节点,所有索引载入内存。
4.2 查询场景和压测方式
压测工具用wrk,它会用多线程模拟并发请求。每种工具先做1分钟预热,然后压测5分钟,取稳定区间的数据。
我设计了三种有代表性的查询场景:
- 场景A:基础关键词搜索。搜索词是“手机”,只做文本匹配,不加过滤不加排序,返回20条结果。
- 场景B:关键词加过滤加排序。搜索“耳机”,过滤条件价格在100到2000元之间,按销量降序。
- 场景C:带错字的搜索。搜索“sumsung”,被人为拼错,希望引擎的错字容忍能力能把它纠正为“samsung”。
这三种场景基本覆盖了站内搜索最核心的诉求。
4.3 结果表格
压测数据整理后是这样的:
| 查询场景 | ES QPS | ES P95延迟(ms) | Typesense QPS | Typesense P95延迟(ms) |
|---|---|---|---|---|
| 场景A 关键词搜索 | 512 | 46.2 | 2780 | 8.1 |
| 场景B 过滤+排序 | 320 | 78.5 | 1643 | 12.3 |
| 场景C 错字纠错 | 188 | 131.6 | 1040 | 19.7 |
这里说明一下,ES的数据是在这个低配单节点上跑出来的,你要是上了三节点集群和SSD,配上完美的分片策略,数字会好看不少。但反过来看,Typesense是在单节点、零参数调优的情况下跑出来的,它用的是完全不同的性能基线。
最夸张的场景A,Typesense的QPS是ES的5.4倍,P95延迟是ES的六分之一。场景B加了过滤和排序之后,差距缩小到5倍左右。场景C因为错字纠错是个高CPU操作,两边都慢下来,但Typesense依然保持了数量级的优势。
4.4 资源占用对比
性能之外,资源占用也是我关注的重点。ES启动后进程常驻内存是3.2GB,其中JVM堆给了2GB,另外还有不少堆外内存和Lucene段缓存。打满压测时,系统内存占用最高到5.8GB。
Typesense加载完200万数据后,索引文件大概是原始JSON数据量的45%,约670MB,进程常驻内存约800MB。打满压测时内存占用也才1.1GB左右。同样的数据规模,Typesense吃掉的资源只有ES的五分之一上下。
这就带来了一个很实际的好处:很多场景下你不再需要给搜索服务准备一台高配机器,一台普通的2核4G云主机就能扛住线上流量。运维成本一降,整个人都轻松不少。
5. 踩坑换来的经验:哪些场景不建议无脑替换
5.1 中文分词的坑:别指望开箱即用
Typesense自带的分词器是基于字符的语言无关切分,对英文和拼音场景效果很好,但遇到中文这种需要语义分词的场景,默认行为是把连续中文字符按二元组切分。什么意思呢?“人工智能”会被拆成“人工”“工智”“智能”,看起来好像没问题,但遇到“重庆火锅”这类容易产生歧义的词,或者“中华人民共和国”这种长词,默认分词的召回率和准确率都不太理想。
我踩过最大的坑是搜索“华为手机壳”,结果是先按“华为”匹配了一堆手机,再按“手机壳”匹配了一堆壳,能同时命中“华为”“手机壳”两个词的商品排序反而不靠前。这就是二元切分没有词义边界导致的。
解决方法有两个。第一个是在写入前把中文文本预处理成扩展字段,把业务已知的关键词以标签形式存进去,比如title_keywords字段里写入“华为 手机壳 官方 超薄”,搜索时把query_by指向这个扩展字段。第二个是如果前端搜索允许,可以考虑引入外部的分词服务或IK分词器,把分词结果作为一个独立的字符串数组字段写入,然后在查询时用query_by匹配这个字段。
总之,中文场景不要拿Typesense默认配置直接上生产,一定要先验证召回和排序效果。
5.2 高可用和横向扩展的真实边界
Typesense的集群模式是raft协议,一个leader节点,多个follower节点。写请求只能发到leader,读请求可以分散到follower。这意味着它的写扩展性是有上限的,所有写入都会汇聚到单点。ES的写扩展性则要好得多,可以分片写,分散到整个集群。
如果你的业务是日志数据这种每天几亿条的写入流,Typesense并不合适。它的定位是产品内搜索、站内搜索,这类场景写QPS通常不高,但读QPS很高,所以单leader写不是一个问题。
另外,Typesense的节点数建议控制在5个以内,太多节点会让raft协议本身成为新的可维护性负担。ES在几十个节点规模下依然管理得井井有条,两者面向的集群复杂度完全不同。
5.3 生态和可视化能力的缺失
ES之所以是国内搜索的事实标准,一半功劳要给它的生态。Kibana提供了一整套可视化监控、索引管理、查询调试界面;Logstash、Filebeat能直接对接各种数据源;官方还提供了各种语言的成熟SDK。而Typesense这边,官方只有一个简单的Web管理界面和几套SDK,没有可视化查询工具,没有丰富的报表能力,没有配套的数据导入管道。
这意味着如果你需要深度诊断一次查询为什么召回异常,基本要靠自己去看响应里的debug信息,或者直接对索引文件做检查。习惯了Kibana“点几下就能看到查询计划”的人,切换到Typesense初期会有些难受。
不过Typesense暴露了Prometheus的metrics接口,我个人的做法是把索引大小、查询延迟、内存使用这些指标接到Grafana上自建监控面板,效果比Kibana的监控页更贴近自己业务。
5.4 复杂聚合分析能力有限
ES的aggregation框架非常强大,可以做到多维下钻、日期直方图、嵌套聚合、百分位统计等等。如果你需要从搜索数据里提取业务分析结果,比如“按品牌分组的近30天销售走势”,ES的date_histogram加terms聚合几行DSL就能搞定。
Typesense目前只提供基础的facet统计和group_by聚合,能做到“按品牌分组统计数量”和“按分类下钻”,但跨多维度、嵌套组合、时序运算这些场景基本无能为力。它会返回每个facet的count,但不会帮你做复杂的分析运算。
所以在选型时要想清楚一件事:你的搜索服务是纯搜索,还是“搜索加分析”的组合体?如果是后者,不要轻易把ES整体替换掉,更合理的方案是让ES继续承担分析职责,把高频低延迟检索的流量切给Typesense。
5.5 什么时候不推荐替换
我把不适合替换的场景列一个清单:
- 数据量超过内存可承载上限,且不想花大量成本买内存。
- 需要频繁做多字段嵌套聚合分析。
- 数据模型复杂,需要ES的nested object或parent-child关联。
- 团队已经深度依赖Kibana做日常运维和查询调试。
- 写入吞吐要求极高,比如每秒上万条日志持续灌入。
遇到这五类情况,我建议还是继续用ES,硬换Typesense只会给自己挖坑。
6. 要不要换?我的最终选型判断
6.1 适合替换的信号
在我实际经历和见过的案例里,下面这几种场景是最适合切到Typesense的:
- 产品内的站内搜索,数据量在几百万到几千万级别,索引能放进内存。
- 对响应延迟非常敏感,要求P95稳定在20毫秒以内。
- 查询模式相对固定,主要是关键词加过滤加排序,不需要复杂聚合。
- 团队里没有专职ES DBA,需要一个人搞定搜索服务的全部维护。
- 预算有限,不想为了一个搜索服务常年养几台高配机器。
简单说,只要你的索引能放进一台机器的内存,且查询模式不需要重度分析,换到Typesense后不但性能会明显提升,维护成本也会大幅下降。
6.2 渐进替换路径
我不建议一次性把线上ES集群整个下线。稳妥的做法是走双跑灰度路径。
第一步,把数据从业务库双写到ES和Typesense。可以用消息队列做双发,也可以先写ES,再通过一个同步job把增量文档转发到Typesense。这样两边数据保持一致。
第二步,在测试环境对比两边搜索结果。重点是查漏,确认Typesense的召回结果和ES没有明显差异,排序规则也能对齐。一旦发现某个查询在Typesense里结果不对,优先检查分词字段和过滤条件是否配置正确。
第三步,把线上读流量切成灰度比例,先切10%。观察几天,确认没有实时问题或搜索异常后再逐步提升到50%、100%。同时在灰度期间把搜索质量指标、延迟指标、错误率统一接到监控看板,一旦异常随时回滚。
6.3 最后分享一个小技巧
如果你最终决定用Typesense,在数据导入阶段有个细节值得留意:批量导入时不要一次性把所有文档全丢进去,建议按业务维度分组,一批批地导。这样做的好处是,如果某批次数据格式有问题,你能立即定位到是哪个文件里哪一行的问题,而不用在几百万条数据里大海捞针。
另外,线上环境建议在Typesense前面加一个简单的Nginx或CDN缓存层,把热门的搜索词缓存5到10秒。这样即使瞬时流量翻倍,上游搜索引擎的压力也不会跟着翻倍。我用这个方案把搜索接口的峰值QPS扛到了Typesense单机极限的三倍以上,后端毫无压力。
我在几次实际项目中体会最深的一点是:搜索场景很多时候不需要ES那种重型武器,Typesense的价值不是“干掉ES”,而是让团队用十分之一的维护成本去承接九成常见的搜索需求。技术选型永远不是越强大越好,而是越匹配越好。希望这篇实测能帮你在做搜索引擎选型时,多一个可靠的选择。