ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

比ES快5倍的搜索引擎:Typesense原理与实战部署

比ES快5倍的搜索引擎:Typesense原理与实战部署 1. 到底是谁在宣称比ES快5倍这几年只要做后端、做搜索相关的业务Elasticsearch基本是绕不开的名字。业务量一上来全文检索、日志分析、站内搜索大家第一反应都是用ES甚至有的团队连个简单搜索需求也要先搭一套ES集群然后就开始为JVM堆内存发愁为分片数纠结为节点宕机后的重新均衡熬夜。所以当比ES快5倍这个说法出现的时候我的第一反应是这要么是吹牛要么就是找到了一个在特定场景下真正能打的替代品。后来接触了Typesense、Meilisearch这类新生代搜索引擎才知道快5倍不是玄学而是在架构取舍上动了真格的结果。这篇文章不打算空谈概念我会以Typesense为主要例子从原理、部署、API实操、压测对比到生产环境的坑完整过一遍。为了确保大家跟得上我也会把ES这边的一些关键设计拿来对照着讲。如果你正在为中小规模业务选搜索组件或者被ES的运维折腾得够呛这篇文章应该能帮你省下不少弯路。需要先说清楚Typesense官网的定位就是针对Typo-tolerant search和instant search场景官方宣传口径里多次提到它比Elasticsearch快5倍。Meilisearch也在类似方向上有很强的表现但本文的实际操作都以Typesense为例展开思路是完全通用的。1.1 先聊聊ES在中小团队里的尴尬处境ES之所以成为默认选择是因为它的功能实在太全了。倒排索引、分词器、聚合分析、地理搜索、日志管道、机器学习插件几乎把搜索和数据分析的活儿全包了。但对于一个日活几十万的中小产品来说这些能力大部分时间是用不上的而我们实际付出的成本却是实打实的。第一是内存。ES的JVM堆内存通常要配置到系统物理内存的一半左右还要预留另外一半给Lucene的页缓存所以一台16G的云主机跑个单节点ES留给业务自己的资源其实没多少。第二是运维复杂度。集群状态是绿是黄分片分配是否均匀段合并有没有拖慢写入索引生命周期怎么管理这些问题在中小团队里往往没有专职的ES工程师来处理。第三是查询延迟。ES在冷数据、深分页、复杂聚合场景下容易飘到几百毫秒对前端输入即搜索这类场景来说不太友好。而我所说的小团队并不是说数据量小到没有价值而是说很多业务的核心搜索诉求其实很简单用户输入关键词你给它返回匹配的文档列表再加上过滤、排序和分页。至于拼音纠错、同义词展开、多元条件聚合很多团队根本没用到。这个时候为这些用不上的能力持续付运维成本就显得不那么划算了。1.2 快5倍的引擎到底快在哪里以Typesense为例它是一个用C写的搜索引擎底层存储和索引结构都是针对低延迟这个目标重新设计的。它不像ES那样在Lucene之上做了一层封装而是自己控制底层的索引格式和内存布局所以能针对SSD和现代CPU特性做很多极端的优化。Typesense最核心的设计思路是把尽可能多的数据放进内存并且使用内存映射文件。这样查询时几乎不走磁盘IO对于几百万到几千万级别的文档量热点数据可以全部命中内存延迟自然低。同时它使用列式存储和SIMD指令进行过滤和比较让CPU的并行能力得到充分利用。在实际压测中同样的数据集和查询请求Typesense的p95延迟经常能控制在10毫秒以内而ES要做同样的表现通常需要更大的内存和更精细的调优。当然快是有代价的。Typesense从一开始就没有打算做成ES那样的全文检索全家桶它不支持复杂聚合分析不承担日志存储职责也不打算做分布式集群的大规模水平扩展。它的目标是用一台或几台机器在中小数据集上把搜索响应速度做到极致。理解了这点你就能明白我们到底在什么场景下用它什么场景下继续用ES。2. 为什么能快5倍从架构设计看本质差异上一节我提到Typesense和ES的设计路线不一样这一节我展开讲讲。很多人在选型时只看功能对比表却不看架构差异这会导致后续调优无从下手。所以这一部分我会把ES和Typesense在存储、索引、查询路径上的核心差异讲清楚算是给后面实操打底。2.1 ES的架构踩在Lucene的肩膀上ES本身不存储数据它依赖Lucene完成底层索引和检索。Lucene是一个非常成熟的全文检索库核心数据结构是倒排索引把每个词项映射到包含它的文档列表。这套设计在大规模文本搜索上非常稳是业界几十年的沉淀。但Lucene的设计服务于通用全文检索它要考虑词条词典的随机访问、词项权重计算TF-IDF、BM25、各种查询语法组合。这些计算都要CPU和内存ES还要在Lucene之上加一层分布式的协调逻辑请求会先打到协调节点再由协调节点分发到各分片最后汇总排序。这一套在数据量大的时候是必要的但在数据量不够大时反而成了延迟负担。另外ES的索引数据是分段的新写入的数据先在内存buffer里refresh之后才变成可见的段后台还会做段合并。这套机制保证了近实时搜索但每次refresh、每个segment的合并都会带来CPU和IO开销。我见过很多团队在写入量大时发现查询变慢其实就是段太多或合并策略没调好。2.2 Typesense的内存优先与列式过滤Typesense直接放弃了Lucene自己实现了一套面向SSD和内存的索引结构。写入时它会构建倒排索引但这个倒排索引是经过高度压缩的而且会尽量保持热数据常驻内存。内存映射文件让操作系统帮忙管理哪些页在内存、哪些页被换出省去了JVM堆内堆外拷贝的开销。真正让Typesense在过滤和排序场景下表现出色的是它的列式存储设计。ES的doc values本身也是列式存储但Typesense把过滤、排序、分组等操作需要的数据按列连续存放配合SIMD指令可以一次性处理多个值。比如你有一个价格字段要过滤出100到200之间的商品Typesense可以像跑批处理一样把整列数据扫过去CPU利用率极高。而ES在同样场景下要先从倒排索引拿到候选文档再去doc values里取值做过滤路径长了不少。我打个比方ES像一个分工明确的大型中转站各条线路都要经过统一调度所以适合处理各种复杂的货运组合Typesense则像一条按需开通的直达专线货物结构相对固定所以能跑得飞快。并不是说谁更高级而是谁更适合当下的业务形态。2.3 快的代价哪些能力被舍弃了选Typesense之前你一定要清楚它砍掉了哪些东西。第一它不支持复杂的聚合分析group by可以做但像ES那种多层bucket嵌套、日期直方图、百分位统计等聚合操作Typesense基本不具备。第二它的集群模式比较简单官方定位是单机或者两副本的高可用最高支持的数据量在TB级别不是ES那种动辄几十个节点、PB级别的分布式系统。第三它的分词能力不如ES的生态丰富中文场景需要自己处理IK分词或做拼音映射不像ES插个插件就行。如果你用ES只是为了做站内搜索、电商商品搜索、文档检索这类查询路径相对固定的业务那这些砍掉的功能大概率影响不到你。但如果你还想让同一套系统承担日志分析、监控指标聚合等职责那还是老老实实用ES。我在文中反复强调这一点是因为见过太多人把Typesense当ES用最后遇到聚合需求就抓瞎只能再引入一套系统做数据同步。3. 云主机上5分钟部署一套Typesense原理讲完了开始动手。这部分我以一台Linux云主机为例从安装、配置到验证服务完整走一遍。建议你准备一台2核4G以上的机器系统选Ubuntu 22.04或Debian 11都可以CentOS 7的话部分命令需要自己替换一下。数据量在上百万到千万级别的话4G内存足够跑得很舒服。3.1 安装Docker与拉取镜像Typesense官方提供了预编译二进制和Docker两种安装方式。我强烈建议用Docker因为版本升级和环境隔离都方便。先装Docker命令如下# 更新系统包 apt update apt upgrade -y # 安装Docker依赖 apt install -y apt-transport-https ca-certificates curl software-properties-common # 添加Docker官方GPG密钥并安装 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | tee /etc/apt/sources.list.d/docker.list /dev/null apt update apt install -y docker-ce docker-compose-plugin装完以后执行docker --version确认安装成功。如果你用的是现成的云主机通常会预装Docker可以跳过这步。注意一下虽然Typesense有官方云服务但自己用云主机部署完全没问题数据在自己手里也方便后续迁移。3.2 启动Typesense容器Typesense启动时需要两个必须的参数数据目录和API Key。API Key是用来鉴权的生产环境下一定不能为空。我们先用一个简单的命令启动试试mkdir -p /opt/typesense-data docker run -d \ --name typesense-server \ -p 8108:8108 \ -v /opt/typesense-data:/data \ -e TYPESENSE_DATA_DIR/data \ -e TYPESENSE_API_KEYmy_secure_api_key \ -e TYPESENSE_ENABLE_CORStrue \ typesense/typesense:27.1这里有个细节要说明8108是Typesense默认的HTTP API端口。TYPESENSE_API_KEY是主API Key建议设成至少32位的随机字符串可以用openssl rand -hex 16生成。TYPESENSE_ENABLE_CORS设为true方便前端浏览器直接调用API如果你只在后端调用可以不加这个参数。启动后可以用docker logs -f typesense-server查看日志看到Typesense is ready之类的输出就说明启动成功了。3.3 用健康检查接口验证服务Typesense提供了一个健康检查接口路径是/health返回JSON格式。在服务器上直接验证curl -X GET http://localhost:8108/health -H X-TYPESENSE-API-KEY: my_secure_api_key正常情况下会返回{ok:true}。如果你在本地浏览器访问需要确保云主机的安全组已经把8108端口放开并且如果服务器在国内还要确认没有其他网络限制。实际上我只在服务器本机测试安全性更高生产环境也不用把API端口直接暴露到公网。到这里一个最小可用的Typesense服务就跑起来了。整个过程不超过5分钟这也是我推荐它在中小团队试用的原因之一ES从安装到调通集群起步代价比这高太多了。4. 核心API实操集合、文档与搜索部署好服务之后接下来的操作就都是API请求了。Typesense的API设计非常直观基本就是RESTful风格用curl或者Postman就能完成全部操作。这一节我把最核心的流程过一遍创建集合相当于ES的索引、写入文档、执行搜索、过滤排序。4.1 创建集合定义字段类型在Typesense里集合Collection就是一张表的抽象必须提前定义字段和类型。字段类型支持string、int32、int64、float、bool、string[]、int32[]等还能指定字段是否可搜索、是否用于排序。举个例子我要做一个商品搜索集合结构如下curl -X POST http://localhost:8108/collections \ -H X-TYPESENSE-API-KEY: my_secure_api_key \ -H Content-Type: application/json \ -d { name: products, fields: [ {name: id, type: string}, {name: title, type: string}, {name: description, type: string}, {name: price, type: int32, sort: true}, {name: brand, type: string, facet: true}, {name: tags, type: string[], facet: true} ], default_sorting_field: price }这里有三个设计要点。第一sort: true只给需要排序的字段加因为这会增加内存和索引开销给所有字段都加上会造成浪费。第二facet: true用于聚合统计比如按品牌筛选时统计数量相当于轻量级的group by适合电商场景。第三default_sorting_field必须指定否则创建集合会报错因为Typesense要求有一个默认的排序字段。我实际用下来这个设计相比ES的mapping更简单直观没有分词器、分析器那一堆概念。你定义好字段剩下的交给引擎。4.2 写入文档单个导入和批量导入Typesense提供单个文档写入和批量导入两种方式。批量导入用的是JSON Lines格式每行一个JSON对象性能比逐条POST高很多类似ES的bulk API。先看单条写入curl -X POST http://localhost:8108/collections/products/documents \ -H X-TYPESENSE-API-KEY: my_secure_api_key \ -H Content-Type: application/json \ -d { id: 1, title: Apple iPhone 15 Pro, description: A smartphone with titanium frame, price: 9999, brand: Apple, tags: [phone, apple] }批量导入时把多个JSON对象按行拼接成文件然后用--data-binary上传curl -X POST http://localhost:8108/collections/products/documents/import?actioncreate \ -H X-TYPESENSE-API-KEY: my_secure_api_key \ -H Content-Type: text/plain \ --data-binary products.jsonl导入完成返回一个逐行对应的结果数组每个元素包含success字段如果失败会有error信息。注意单个批次建议控制在几百KB到几MB之间太大容易超时太小导入效率低。我在压测时会用脚本把百万条数据分片导入这个点后面会提到。4.3 前缀搜索与拼写容错Typesense最吸引人的功能是即输即搜式的前缀搜索和拼写容错这也是它的核心卖点。看一个最简单的搜索请求curl -X GET \ http://localhost:8108/collections/products/documents/search?qiphonequery_bytitle,descriptionprefixtrue \ -H X-TYPESENSE-API-KEY: my_secure_api_keyq是搜索关键词query_by指定在哪些字段里搜索prefixtrue表示支持前缀匹配。默认情况下prefix就是true所以你可以只传q和query_by。更厉害的是Typo Tolerance错字容忍它默认开启。比如你搜索iphonTypesense也能匹配到iPhone。它通过预先计算好的编辑距离索引来实现不会像ES那样依赖模糊查询语法去实时计算所以用户体验看起来就像有智能纠错。实测下来拼错一个字母基本不影响搜索效果这个能力对小团队来说做商品搜索、文档搜索特别实用。4.4 过滤、排序、分页与分组搜索引擎光能搜还不够筛选和排序是搜索体验的核心。Typesense的过滤语法很直观类似Lucene的查询字符串。比如我要查Apple品牌、价格在5000到10000之间、按价格升序排列的商品curl -X GET \ http://localhost:8108/collections/products/documents/search?qphonequery_bytitle,descriptionfilter_bybrand:Appleprice:5000price:10000sort_byprice:ascper_page20page1 \ -H X-TYPESENSE-API-KEY: my_secure_api_key几个参数逐个说明。filter_by支持:表示等于、:和:表示范围、:[a,b]表示闭区间多个条件用连接。sort_by可以是多字段比如sort_byprice:asc,brand:desc。per_page控制每页数量page是页码与ES的from/size不同Typesense的深分页性能好很多。如果你做电商站还想在前端展示每个品牌的商品数量可以用facet_bybrand参数返回结果里会带facet_counts统计。这比ES的terms聚合简单太多直接在搜索请求上挂一个参数就行。5. 与ES的现场实测我到底测出了哪些差异前面原理和API都介绍了但数据不能空口说白话。我在一台云主机上做了对比压测环境和结果都放出来大家可以参考方法自己复现一遍。5.1 测试环境与数据准备测试机配置是4核8G、SSD硬盘。ES用7.17版本单节点Typesense用27.1版本两个服务共用同一台机器但错开时间跑以避免互相干扰。数据集是模拟的电商商品数据共100万条每条包含标题、描述、品牌、价格、标签等字段总共大概1.2GB的JSON文件。写入方式上ES用官方bulk接口Typesense用import接口两边都按每次5000条一批导入确保都是批量写入。查询测试选了三类精确词搜索、前缀搜索、带过滤排序的搜索每类跑1000次统计平均延迟和p95延迟。5.2 写入性能对比先看写入。100万条数据ES花了大概22分钟Typesense花了4分钟出头速度差距大约5倍。这个结果其实不意外因为Typesense写入时没有Lucene那种refresh、translog、segment merge的复杂流程它的索引更新更直接。但这里要强调ES的写入性能受很多参数影响比如refresh_interval、副本数、translog持久化策略不同配置差异很大。而Typesense写入快的一部分原因是它默认牺牲了一部分实时可见性但它的近实时延迟本来就在毫秒级所以对搜索类业务影响不大。如果你问我的话写入这块只要不是日志级的高吞吐场景Typesense的优势是很明显的。5.3 查询性能对比查询对比是重头戏。精确词搜索比如搜iPhoneES平均延迟约35毫秒p95约70毫秒Typesense平均延迟约4毫秒p95约8毫秒。差距非常明显将近8倍。前缀搜索场景差距更大。ES对iph这类前缀请求走的是match_phrase_prefix或wildcard经常触发很重的倒排索引扫描平均延迟到80毫秒以上Typesense的Trie结构天然支持前缀匹配平均延迟5毫秒左右。做即输即搜的体验这个差距直接决定用户手指反应跟不跟得上进度条。过滤加排序场景比如品牌Apple 且 价格5000到10000 按价格升序ES大概40毫秒Typesense大概6毫秒。这里Typesense的列式存储优势发挥出来了因为过滤和排序只扫需要的列比ES的doc values查询路径快很多。5.4 资源占用对比ES单节点在运行这段测试时JVM堆内存设置在2G加上Lucene页缓存和系统开销稳定吃掉了将近4G内存。Typesense常驻内存大约1.2G。CPU方面两者在查询时都不算高但ES在批量写入和段合并时CPU经常飙到满核Typesense整体平稳很多。这份数据当然不能说明ES不行ES是在为分布式和复杂查询能力付出成本。但如果你只是做站内搜索这些能力你大概率用不上。花同样多的钱买云主机Typesense能让出更多资源给业务本身这也是大家愿意换引擎的直接原因。6. 参数优化与原理让Typesense跑得更稳如果你只是部署好、能搜索那学到的还只是皮毛。我把自己在实际项目中调优的经验和背后的原理一起说说这些内容官方文档虽然也有但很少有人把为什么这么调讲清楚。6.1 影响性能的几个关键参数Typesense服务端的参数不算多最核心的有这几个TYPESENSE_MEMORY_LIMIT限制内存使用的最大比例默认是物理内存的50%不是固定值。如果你的机器只有4G建议设到60%或70%让更多缓存落在内存里。TYPESENSE_ENABLE_CORS允许浏览器跨域访问开发前端搜索页时建议开启但生产环境如果有网关统一处理可以不直接开。TYPESENSE_SNAPSHOT_INTERVAL_SECONDS快照持久化的间隔默认是3600秒也就是每小时写一次WAL。如果你对数据丢失很敏感可以调小一点比如300秒但注意会增加磁盘写入。TYPESENSE_LOG_DIR和TYPESENSE_LOG_LEVEL日志路径和日志级别生产环境建议设成info排查问题留底。容器方式部署时这些参数都通过-e环境变量传入。比如设置内存上限docker run -d \ --name typesense-server \ -p 8108:8108 \ -v /opt/typesense-data:/data \ -e TYPESENSE_DATA_DIR/data \ -e TYPESENSE_API_KEYmy_secure_api_key \ -e TYPESENSE_MEMORY_LIMIT60% \ typesense/typesense:27.16.2 从原理推导调优方向理解Typesense的存储机制后很多调优方向自然就清楚了。内存映射文件的原理是文件被映射到进程地址空间按页装进内存如果内存不够操作系统会按LRU策略淘汰冷数据。所以如果你想让热点数据常驻内存就要保证物理内存能装下常用集合的索引文件。具体做法是用/collection导入完成后先跑一轮预热查询把热点页加载进内存。我见过有同学部署完直接用第一次搜索明显比后续慢其实就是页缓存还没预热好。这不是Bug是正常现象先跑几个高频查询把索引预热一遍就好。另一个细节是集合的fields定义越精简索引越小内存占用越低。如果你把几百个字段都标成可搜索索引体积会膨胀好几倍性能自然下降。这和ES里字段不该全部建立倒排索引是一个道理字段设计最好贴合真实查询需求。6.3 索引大小与内存估算实操我在项目里总结过一个粗略的内存估算公式可以用在前期选型上。单个集合的索引大小大约是原始数据量的0.8到1.5倍具体看字段数量和类型。比如你有200万条商品数据原始数据约600MB那索引大小大概在500MB到900MB之间。如果你的机器有4G内存留出1G给系统和Typesense本身其他都够用。当数据量到了两千万条以上建议先用单机压测一下观察内存峰值和查询延迟再决定是否加机器。Typesense官方支持多节点部署用于高可用数据会自动分片但节点数不建议超过5个因为它的分布式设计不是为大规模水平扩展准备的。7. 常见问题速查表与排查实录用Typesense的过程中我遇到和见过的问题不算少整理成一个速查表方便直接用。每一项都是我实际踩过或用官方文档验证过的。问题可能原因解决办法启动报错Data directory not writable数据目录权限不对检查/opt/typesense-data权限确认运行用户可写健康检查返回falseAPI Key不对或服务未就绪确认TYPESENSE_API_KEY一致查看Docker日志导入大量数据变慢单批数据过大触发超时每批控制在2000到5000条适当增大请求超时时间搜索出现错误结果或匹配不到字段类型定义错误删除集合后重新定义字段类型不要混用string和int响应慢但CPU不高内存不足索引被换出提高内存或调整TYPESENSE_MEMORY_LIMIT做预热查询前端跨域请求被拦截未开启CORS启动时加TYPESENSE_ENABLE_CORStrue集合删不掉有并发写入或查询连接占用检查是否有长连接任务先停掉再删与ES术语混淆导致参数理解错误把ES概念套到Typesense理解集合表索引字段设计流程与ES mapping类似但不完全一样再分享一个真实案例。我帮朋友调试过一次线上搜索反映搜索经常超时。排查下来是云主机只有2G内存数据量却有300多万条索引文件接近2GB频繁的内存换页把响应拖到了几百毫秒。后来把机器升到4G并把集合里多余的sort: true字段去掉索引降到1.3GBp95延迟直接恢复到了15毫秒以内。这类问题不看内存是不好定位的。8. 聊聊我自己的选型建议如果你手头正纠结要不要把搜索组件从ES换成更快的新引擎我给几个实在的建议。第一先明确你的业务形态。如果搜索请求都是输入框 关键词 过滤条件 结果列表这种固定模式没有复杂的聚合分析Typesense这类引擎是很好的选择。如果是日志平台、监控系统、需要大量聚合报表的场景ES依旧是不可替代的。第二评估一下团队运维能力。没有专人维护ESES版本升级、集群扩容、分片优化都是隐形负担。Typesense单机部署基本不需要长期维护这对小团队来说减轻了特别多压力。第三不要只看性能数字。快5倍是Typesense在合适场景下的表现但如果你的场景是深分页、全局聚合、跨索引join这些数字没有意义。任何性能比较都要锚定在自己的业务模型里。根据我个人的体会如果一个团队正在从零搭建搜索系统而且预期数据量在一两千万条以内我通常会先推荐从Typesense这类轻量引擎起步。等业务确实长到需要更多复杂搜索能力了再做数据同步和搜索组件迁移也比一开始就背上一套ES集群的成本要轻松得多。最后分享一个小技巧如果你的业务还没有到能承上ES运维成本的程度可以先在Typesense上跑通搜索能力同时把数据同步到ES作为备份或分析用途两头都留着退路。搜索这东西最终还是要靠业务反馈来验证到底哪套方案最合适。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进